Compare commits

...

225 Commits

Author SHA1 Message Date
Martin Michelsen 47f97f357f add some undocumented client commands from PC, GC and BB 2022-06-26 16:52:19 -07:00
Martin Michelsen cf8dd69edc catch more client-specific exceptions 2022-06-26 11:41:53 -07:00
Martin Michelsen e0c44f8642 fix BB lobby server redirect bug 2022-06-26 11:41:53 -07:00
Martin Michelsen 9144257186 document symbol chat command format 2022-06-26 11:41:53 -07:00
Martin Michelsen ba1a25036b support chat commands on proxy server 2022-06-26 11:41:53 -07:00
Martin Michelsen fc078a5d51 make it possible to disable item tracking 2022-06-24 22:07:29 -07:00
Martin Michelsen c2b112db43 add to-do item about private lobbies 2022-06-24 22:07:29 -07:00
Martin Michelsen a3bfed6e42 fix item tracking bug on pickup 2022-06-23 21:45:17 -07:00
Martin Michelsen a3f219469c fix log level on DOL file message 2022-06-22 23:53:43 -07:00
Martin Michelsen edef53d3de suppress default color behavior in send_command's print_data call 2022-06-22 23:53:43 -07:00
Martin Michelsen 9082907468 remove debugging code in proxy server send_function_call handler 2022-06-22 23:53:43 -07:00
Martin Michelsen f5f2f91c6e use iovec form of print_data 2022-06-22 23:53:43 -07:00
Martin Michelsen e7d8345568 fix DNS server error message 2022-06-22 23:53:43 -07:00
clint-david 75856d1423 Add new subcommand A1. Update comments.
Update comments for revive to confirm reverser/moon atomizer use same subcommands.

Added new subcommand A1 which is part of revive process. Unsure of function (cannot find any in game effects from it).
2022-06-22 09:22:16 -07:00
Martin Michelsen 06bab57407 make quest index log message less confusing 2022-06-20 21:18:13 -07:00
Martin Michelsen 9d2f845418 add optin to delete saved license info 2022-06-04 22:56:18 -07:00
Martin Michelsen 35d9f12e0a refactor proxy destinations menu generation 2022-06-04 22:15:05 -07:00
Martin Michelsen a4f82cd821 fix some error strings 2022-06-04 22:14:52 -07:00
Martin Michelsen 82ff64b5d9 update some comments about ep3 data formats 2022-06-04 22:14:35 -07:00
Martin Michelsen 57afb59c96 fix lobby arrow consistency 2022-06-03 20:16:52 -07:00
Martin Michelsen 316e9533d8 update command notes with pso pc findings 2022-06-03 20:16:41 -07:00
Martin Michelsen e139745f51 support uncompressed episode 3 maps 2022-06-03 00:25:01 -07:00
Martin Michelsen 7efa6374ea generate ep3 map list on demand 2022-06-02 23:43:34 -07:00
Martin Michelsen fc7a9dcbc9 make set-save-files also save ep3 maps 2022-06-02 23:43:34 -07:00
Martin Michelsen a61e5fff56 fix incorrect menu ID error on download quest menu 2022-06-02 12:49:12 -07:00
Martin Michelsen af1b92969e don't use information menu command on GC 2022-06-02 12:49:12 -07:00
Martin Michelsen e16a79c3d8 minor format notes update 2022-06-02 12:49:12 -07:00
Martin Michelsen 5ae1fc78a9 don't show programs menu if client has already saved 2022-06-02 12:49:12 -07:00
Martin Michelsen 562bc4a40c add DOL file loader 2022-06-02 12:49:12 -07:00
Martin Michelsen 40aa08bd4f fix item pickup bug 2022-05-31 21:01:16 -07:00
Martin Michelsen e29e349a84 switch library install order in CI 2022-05-31 17:21:40 -07:00
Martin Michelsen 85d054fc3a implement send_function_call 2022-05-31 17:18:04 -07:00
Martin Michelsen dc53eacac7 fix incorrect list item in readme 2022-05-31 17:10:41 -07:00
Martin Michelsen 67b3590127 fix initialization in 04 command 2022-05-29 12:44:06 -07:00
Martin Michelsen 78bb791c26 make high client ID assignment optional 2022-05-29 12:43:57 -07:00
Martin Michelsen c9cdb21a8b add previously-unknown GC command descriptions 2022-05-29 12:29:14 -07:00
Martin Michelsen f1d10b7ff8 update some documentation 2022-05-24 16:32:15 -07:00
Martin Michelsen 371b5f1012 fix type domain error in proxy command handler 2022-05-23 23:24:20 -07:00
Martin Michelsen 1ff6a4c7e6 improve bb proxy robustness 2022-05-23 23:01:34 -07:00
Martin Michelsen 5a3a55b233 implement infinite hp/tp on proxy server 2022-05-23 00:10:41 -07:00
Martin Michelsen a50500a67d make sc ommand work on game server also 2022-05-22 10:50:50 -07:00
Martin Michelsen 37a7faf007 fix BB change ship option 2022-05-22 09:54:13 -07:00
Martin Michelsen 8f6ec2bed6 increase proxy session timeout to make BB proxying work 2022-05-22 09:54:13 -07:00
Martin Michelsen 22b69276dd fix EE command format 2022-05-22 09:54:13 -07:00
Martin Michelsen 767883214d add more info on unused command structures 2022-05-22 09:54:13 -07:00
Martin Michelsen 908671c55b add description of BB patch/checksum command 2022-05-22 09:54:13 -07:00
Martin Michelsen 641639a659 implement tfs1 2022-05-22 09:54:13 -07:00
Martin Michelsen 0378314733 more Ep3 structures 2022-05-21 11:48:59 -07:00
Martin Michelsen f67cffe636 add parray::is_filled_with 2022-05-21 10:13:22 -07:00
Martin Michelsen 228fedece1 fix ep3 meseta command 2022-05-19 00:20:50 -07:00
Martin Michelsen 9d13df4749 unset x bit on all files in system/ 2022-05-19 00:20:36 -07:00
Martin Michelsen ca3f65353c add all ep3 download quests 2022-05-19 00:15:17 -07:00
Martin Michelsen 0837234e4f add CLI option to decode SJIS 2022-05-18 23:58:21 -07:00
Martin Michelsen 43723887bb autogenerate ep3 map list, so new maps can be dropped in easily 2022-05-18 23:58:04 -07:00
Martin Michelsen 37348dc98e add appropriate includes on Episode3.hh 2022-05-18 19:06:36 -07:00
Martin Michelsen bbd58c3d71 add some basic Episode 3 structures 2022-05-18 18:59:17 -07:00
Martin Michelsen 45eabab958 parse episode 3 player data 2022-05-18 12:15:02 -07:00
Martin Michelsen 957987dcd2 fix category for CDFS quest 2022-05-18 01:05:27 -07:00
Martin Michelsen 7764ae7b03 make download quest menu work even after joining a lobby 2022-05-18 01:05:04 -07:00
Martin Michelsen 095eb23dab fix download quest info message 2022-05-18 01:04:44 -07:00
Martin Michelsen 2c9922cf33 allow duplicate quest names per game version 2022-05-18 01:04:23 -07:00
Martin Michelsen 9b0c294054 add CLI option to decode quest files 2022-05-18 01:03:54 -07:00
Martin Michelsen e87c73c1b7 add subcommand B4 2022-05-17 23:27:52 -07:00
Martin Michelsen 88549fbc2b update descriptions of some subcommands 2022-05-17 17:33:33 -07:00
Martin Michelsen 52d8fc2b13 add gba files for episode 3 2022-05-17 10:07:59 -07:00
Martin Michelsen e853ebf021 use game implementation for stack item limits 2022-05-15 21:01:15 -07:00
Martin Michelsen e9109a6877 fix some BB item bugs 2022-05-12 18:46:19 -07:00
Martin Michelsen cd53acc24a remove AC handler for DC/PC 2022-05-12 17:56:13 -07:00
Martin Michelsen 3170c89b72 update readme 2022-05-12 13:28:51 -07:00
Martin Michelsen ce075b4123 fix handling of uncleared after-string data from psobb 2022-05-12 11:54:58 -07:00
Martin Michelsen 44989c08fe move default files to system/players 2022-05-12 11:49:34 -07:00
Martin Michelsen 307eef88d0 fix unsafe memory access in PSOBBEncryption 2022-05-12 11:45:20 -07:00
Martin Michelsen 71d78839a4 fix PlayerBB struct length 2022-05-12 10:45:48 -07:00
Martin Michelsen ba03d70a2e use explicit-endian types in PSOCommandHeader 2022-05-12 10:31:11 -07:00
Martin Michelsen 58ffd473f9 fix $bbchar writing corrupt char files 2022-05-11 23:58:16 -07:00
Martin Michelsen 4fc3ce9f20 fix word wrap on $bbchar message 2022-05-11 23:55:28 -07:00
Martin Michelsen 2fdf88fa59 aggessively forbid using nullptr with parray/ptext 2022-05-11 23:27:19 -07:00
Martin Michelsen 7eaddd8faa add some debugging info for item tracking 2022-05-11 23:26:50 -07:00
Martin Michelsen 4af86e1a4d fix segfault during bb quest loading 2022-05-11 23:22:30 -07:00
Martin Michelsen 711a4b815c fix bb player choice bug 2022-05-11 23:19:30 -07:00
Martin Michelsen ea94c38598 support old format of 93 command 2022-05-11 23:08:31 -07:00
Martin Michelsen 2d3cd17692 add some files to gitignore 2022-05-11 23:07:55 -07:00
Martin Michelsen c06d7bdc70 update readme 2022-05-11 16:49:46 -07:00
Martin Michelsen d80f7c466c add some bb keys 2022-05-11 15:09:32 -07:00
Martin Michelsen 57179513b8 implement quest stats command 2022-05-11 00:50:48 -07:00
Martin Michelsen ceb0fbb849 show connection messages on proxy server 2022-05-10 22:24:45 -07:00
Martin Michelsen 8b413e45cc add some boss action subcommands 2022-05-10 22:24:25 -07:00
Martin Michelsen d0a410428f print remote guild card number in decimal, not hex 2022-05-10 18:44:31 -07:00
Martin Michelsen cc0b88cec9 fix notes about A0/A1 2022-05-10 18:44:19 -07:00
Martin Michelsen 3e784dc7e6 explain in config.json when welcome message is shown 2022-05-10 18:04:45 -07:00
Martin Michelsen 5fadf4bb46 move gba files to system/gba/ 2022-05-10 18:04:24 -07:00
Martin Michelsen e2eb5f0def add some ep3 command notes 2022-05-10 18:03:34 -07:00
Martin Michelsen 6a6241c037 remove unused constant 2022-05-10 01:08:28 -07:00
Martin Michelsen 5db6507b17 fix some dumb bugs in quest menu filters 2022-05-10 01:08:18 -07:00
Martin Michelsen a11b9f5b3e make another pass over command documentation 2022-05-10 01:07:46 -07:00
Martin Michelsen 2f72eb5a3c add manual data encryption options 2022-05-09 10:18:20 -07:00
Martin Michelsen 167becddcf fix some fields in BB 93 command 2022-05-09 10:18:20 -07:00
Martin Michelsen 6f0c5a6d73 add comments from BB subcommand handler table 2022-05-09 10:18:20 -07:00
Martin Michelsen cedb0c648e implement JSD1 2022-05-09 10:18:14 -07:00
Martin Michelsen e1e6ca1517 remove debugging code 2022-05-08 10:30:14 -07:00
Martin Michelsen dc7ec97c0c split bb team rewards field 2022-05-08 00:55:36 -07:00
Martin Michelsen 72810a19f0 fix initialization order in PlayerInventoryItem 2022-05-08 00:46:00 -07:00
Martin Michelsen 354fdb6163 fix remove_item bug 2022-05-08 00:42:36 -07:00
Martin Michelsen 5b17776fae fix ep3 game join command 2022-05-08 00:42:36 -07:00
Martin Michelsen 82d9385ea5 print version names instead of numbers 2022-05-08 00:42:36 -07:00
Martin Michelsen 30c4b5265d add missing header on linux 2022-05-08 00:42:36 -07:00
Martin Michelsen 562e808728 expand speculation on patch 0B command 2022-05-08 00:28:17 -07:00
Martin Michelsen 855d3616da remove some memcpy/memset calls in favor of default constructors 2022-05-08 00:28:06 -07:00
Martin Michelsen 1e3dd6a274 document patch server commands 2022-05-08 00:08:39 -07:00
Martin Michelsen 8ef256917c fix bb login, char creation, and some lobby/game behaviors 2022-05-08 00:04:11 -07:00
Martin Michelsen 4079400784 refactor player/account data handling 2022-05-06 13:52:37 -07:00
Martin Michelsen 839cbb2ee4 add short aliases for section ids in chat commands 2022-05-04 23:45:02 -07:00
Martin Michelsen ecddb8befc fix capitalization in some static game data 2022-05-04 23:45:02 -07:00
Martin Michelsen 342f819f50 add missing include on linux 2022-05-04 15:30:28 -07:00
Martin Michelsen 07fbfd6f75 minor wording changes in readme 2022-05-04 15:27:28 -07:00
Martin Michelsen d5c38c2bc5 automatically determine the correct BB private key for each client 2022-05-04 15:20:08 -07:00
Martin Michelsen 294c328e7a fix some basic things on BB proxy server 2022-05-04 13:09:09 -07:00
Martin Michelsen 2faf511e0d add some notes about PSOBB command handlers 2022-05-04 13:09:09 -07:00
Martin Michelsen a078c9f712 add support for modified BB encryption 2022-05-04 12:58:12 -07:00
Martin Michelsen 87b80b3c99 fix typo in static game data 2022-05-01 10:18:27 -07:00
Martin Michelsen 3572c53dd4 use colors for extra information with $what 2022-05-01 00:19:38 -07:00
Martin Michelsen b359bc0cce handle S-rank names in name_for_item 2022-04-30 18:59:15 -07:00
Martin Michelsen cab2cc6f97 write chat command documentation 2022-04-30 17:58:50 -07:00
Martin Michelsen b8f1b04bee add $what command 2022-04-30 17:58:44 -07:00
Martin Michelsen f7c7dda765 send item drop requests for final bosses 2022-04-23 21:35:12 -07:00
Martin Michelsen cf49a7a798 fix uninitialized field in split reconnect command 2022-04-23 18:58:23 -07:00
Martin Michelsen a7244b75b7 handle extra space after choice search result on proxy server 2022-04-23 10:23:59 -07:00
Martin Michelsen 81e8f3a88e fix uninitialized field in proxy 9E generation 2022-04-23 10:07:22 -07:00
Martin Michelsen 8c171826c8 update some notes in CommandFormats 2022-04-23 10:06:59 -07:00
Martin Michelsen abb76c142b clear uninitialized client memory in simple mail on proxy server 2022-04-21 16:01:09 -07:00
Martin Michelsen b0828a3dfe add set-block-function-calls proxy command 2022-04-21 16:00:40 -07:00
Martin Michelsen 1cc7a88528 don't link with event_pthread 2022-04-21 11:37:43 -07:00
Martin Michelsen 70b2b80fae remove debug build type override 2022-04-21 11:37:11 -07:00
Martin Michelsen 6158d28882 fix ep2/4 government quest indexing 2022-04-21 11:33:16 -07:00
Martin Michelsen 9f06964cec support qst format 2022-04-21 11:24:29 -07:00
Martin Michelsen 168cef747a fix download quests 2022-04-21 01:57:30 -07:00
Martin Michelsen 7469162ea8 document 13/44/A6/A7 client commands 2022-04-21 01:28:18 -07:00
Martin Michelsen 6ff00e46d5 add $next command 2022-04-20 22:21:32 -07:00
Martin Michelsen 226cb0bb8d update comments in protocol description 2022-04-18 10:25:43 -07:00
Martin Michelsen 0acdcdde0e document some more protocol details 2022-04-17 11:12:38 -07:00
Martin Michelsen 40a0433f81 decrease proxy server session timeout to 10 seconds 2022-04-17 11:12:38 -07:00
Martin Michelsen ba68212e37 fix segfault when sending command to disconnected session 2022-04-17 11:01:38 -07:00
Martin Michelsen 8d1d3e8638 update comment about 66 subcommand 2022-04-17 00:04:37 -07:00
Martin Michelsen bb4b495d2c add another subcommand 2022-04-17 00:01:16 -07:00
Martin Michelsen d960e98102 use PrefixedLogger in more places 2022-04-09 21:51:16 -07:00
Martin Michelsen aa9c8efd03 add some includes needed on linux 2022-04-03 23:44:53 -07:00
Martin Michelsen c23fe6211e add returns in static_assert(false) functions so gcc won't complain 2022-04-03 23:39:01 -07:00
Martin Michelsen 54f01713bc fix bug causing private subcommands to get truncated 2022-04-03 23:36:22 -07:00
Martin Michelsen 139ccb27c8 handle ptexts that actually use every byte 2022-04-03 23:35:55 -07:00
Martin Michelsen 028078925d use different colors for sent and received commands 2022-04-03 23:35:11 -07:00
Martin Michelsen be69f26af5 add some more subcommands 2022-04-03 23:34:02 -07:00
Martin Michelsen 88f0c90aba skip room unlock events in switch assist 2022-04-03 23:32:31 -07:00
Martin Michelsen 06fd71f7a6 use std::strings in places of c-strings in most places 2022-04-03 23:31:24 -07:00
Martin Michelsen 1d70933c17 define switch subcommand structure 2022-04-03 17:54:46 -07:00
Martin Michelsen fe9eceed5c bring subcommand abstraction in line with main command abstraction 2022-04-03 11:00:14 -07:00
Martin Michelsen 9c33c2de46 abstract command handlers away from proxy server 2022-04-03 10:59:30 -07:00
Martin Michelsen 522dac9a03 hide remote guild card number in licensed proxy sessions 2022-04-02 18:04:37 -07:00
Martin Michelsen d3ff50918f add safety for erroneous AC commands from clients 2022-04-02 15:14:21 -07:00
Martin Michelsen 091f3d4da4 improve proxy command documentation 2022-04-02 14:38:22 -07:00
Martin Michelsen 825437b145 document switch assist 2022-04-02 14:29:47 -07:00
Martin Michelsen d1cd27b6aa move fallthrough label to a place where gcc can understand it 2022-04-02 14:23:26 -07:00
Martin Michelsen 5afe3fb8d2 implement switch assist on proxy server 2022-04-02 10:47:05 -07:00
Martin Michelsen 76fd9c22bf get PSO PC login sequence working 2022-04-02 10:47:05 -07:00
Martin Michelsen 46add5fb74 fix PSO PC encryption 2022-04-02 10:47:01 -07:00
Martin Michelsen 3f5f2fc61d support multiple versions on proxy server 2022-04-02 10:47:01 -07:00
Martin Michelsen 37b8f1cffa move flags enums into the structs they're scoped to 2022-04-01 21:21:46 -07:00
Martin Michelsen aa1a2e852b fix uneven command size on ep3 card list update 2022-04-01 11:27:13 -07:00
Martin Michelsen 4cff7105fd fix uninitialized memory in ep3 game join 2022-04-01 11:26:47 -07:00
Martin Michelsen dfa087b606 write more comments in command structs header 2022-04-01 10:59:12 -07:00
Martin Michelsen 4f67c70239 clear client config by default in 9E constructor 2022-04-01 10:41:29 -07:00
Martin Michelsen b7cf7df4ef remove another memset 2022-04-01 10:40:34 -07:00
Martin Michelsen 583925045e clean up memory handling for client configs 2022-04-01 10:39:04 -07:00
Martin Michelsen 07a6e40b18 remove some now-unneeded memcpys 2022-04-01 10:24:31 -07:00
Martin Michelsen e8823819a7 remove some now-unneeded memsets 2022-04-01 10:19:05 -07:00
Martin Michelsen 3370b5fad3 add explicit operator!= for parray/ptext 2022-04-01 10:16:07 -07:00
Martin Michelsen c04ed9b6ce use ptext base copy constructor instead of operator= 2022-04-01 10:12:20 -07:00
Martin Michelsen 345820145e implement switch assist 2022-04-01 10:08:06 -07:00
Martin Michelsen edd9f4ea8f add proxy commands to override lobby params 2022-03-31 23:33:43 -07:00
Martin Michelsen 8a9e1a2049 use safe packed string types 2022-03-31 23:23:02 -07:00
Martin Michelsen 832135a505 always null-terminate limited-length strings 2022-03-31 10:04:37 -07:00
Martin Michelsen f39dd5a0af make size non-optional in add_color 2022-03-31 09:55:25 -07:00
Martin Michelsen 7dce8b6c2c centralize command formats; fix a few range bugs 2022-03-31 01:03:13 -07:00
Martin Michelsen db099ed2dd make proxy change ship behavior cleaner 2022-03-30 11:34:01 -07:00
Martin Michelsen 419b24e089 mention proxy ports in readme 2022-03-30 10:58:43 -07:00
Martin Michelsen a26013c571 expand readme section about proxy 2022-03-30 00:52:50 -07:00
Martin Michelsen d9a554beb3 add chat filter proxy option 2022-03-30 00:51:30 -07:00
Martin Michelsen 39f8a33588 add option to set nonzero client IDs by default 2022-03-30 00:51:08 -07:00
Martin Michelsen bcd69bab89 add option for suppressing commands 2022-03-29 23:46:17 -07:00
Martin Michelsen 5848beb6c2 add set-save-files 2022-03-29 23:37:56 -07:00
Martin Michelsen 2e839fe70a allow returning to newserv from proxy server 2022-03-29 23:25:16 -07:00
Martin Michelsen 03dcc016d8 add session timeout on proxy server 2022-03-29 23:22:45 -07:00
Martin Michelsen f3a3e18455 add some more game subcommands 2022-03-29 17:45:51 -07:00
Martin Michelsen 2e9e65f028 remove packed attr on shipgate player struct 2022-03-29 00:16:09 -07:00
Martin Michelsen 5c388c4052 make all file/network-related structs packed 2022-03-29 00:12:16 -07:00
Martin Michelsen b61a9bcdcb add warp command in proxy 2022-03-28 22:30:13 -07:00
Martin Michelsen 04aef91c16 make add_color_inplace safer 2022-03-28 22:30:04 -07:00
Martin Michelsen 97db8da273 keep track of lobby player slots on proxy server 2022-03-28 22:29:20 -07:00
Martin Michelsen 162b0327b9 add some safety for accidental newserv chat commands on proxy server 2022-03-28 22:28:05 -07:00
Martin Michelsen 5b47414a30 clean up some ip stack warnings 2022-03-28 22:27:02 -07:00
Martin Michelsen 5c6a420a61 move config to config.example.json 2022-03-28 19:56:22 -07:00
Martin Michelsen 4001968c84 use memcpy instead of strncpy where appropriate 2022-03-28 18:41:53 -07:00
Martin Michelsen 1cfd12699b also ignore -Wstringop-truncation 2022-03-28 18:38:24 -07:00
Martin Michelsen c99864fd69 fix limits and add fallthrough label in proxy server 2022-03-28 18:33:58 -07:00
Martin Michelsen 7f727d46e0 ignore -Waddress-of-packed-member 2022-03-28 18:27:15 -07:00
Martin Michelsen 50a8783e95 fix uninitialized memory in simple mail handler 2022-03-28 18:22:08 -07:00
Martin Michelsen 29c4387192 add comment about choice search implementation 2022-03-28 18:22:08 -07:00
Martin Michelsen 9699b86d1e do something for E2 commands so clients don't get stuck 2022-03-28 18:22:08 -07:00
Martin Michelsen cf44e2041e add card config backup 2022-03-28 18:22:08 -07:00
Martin Michelsen 035730c1b2 improve proxy server security data handling 2022-03-28 17:57:16 -07:00
Martin Michelsen 2597da95bc rewrite proxy server to support multiple clients and integration with game server 2022-03-28 15:55:19 -07:00
Martin Michelsen d977cf0608 supporting changes for proxy server rewrite 2022-03-28 15:55:19 -07:00
Martin Michelsen 20410e7a94 fix info board 2022-03-27 00:29:44 -07:00
Martin Michelsen 040cccf785 fix a few subcommand comments 2022-03-27 00:29:39 -07:00
Martin Michelsen e7ecec6161 fix infinite tp flag check 2022-03-27 00:29:24 -07:00
Martin Michelsen 3aeb121b00 rewrite subcommand list, add some comments 2022-03-26 23:59:58 -07:00
Martin Michelsen 0bcd76d909 add trade window to to-do list 2022-03-26 23:17:28 -07:00
clint-david 99ae834cf2 add subcommands, identified process trade command
add subcommands for drop meseta, trade proposal, place trap, trigger trap

identified command ID for process trade on gamecube (0xD0)
2022-03-26 23:11:33 -07:00
Martin Michelsen fa07ce457b make enum style consistent 2022-03-26 19:26:26 -07:00
clint-david bd8aadb09f add subcommand for level-up and revive 2022-03-25 11:13:33 -07:00
Martin Michelsen fa90f32619 fix map variations in non-BB games 2022-03-24 11:25:10 -07:00
Martin Michelsen e9ce295cda update some readme text 2022-03-23 17:38:29 -07:00
Martin Michelsen ece71153d9 add a few more subcommands 2022-03-23 17:38:29 -07:00
Martin Michelsen c0193747f4 use actual remote address when ip stack clients connect to proxy 2022-03-20 16:55:16 -07:00
Martin Michelsen 819027145c in proxy mode, log subcommands not implemented in the server 2022-03-18 18:51:00 -07:00
Martin Michelsen c40beb5227 add missing include on linux 2022-03-17 20:14:56 -07:00
865 changed files with 17003 additions and 6640 deletions
+10
View File
@@ -16,6 +16,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
with_resource_file: ['true', 'false']
steps:
- uses: actions/checkout@v2
@@ -36,6 +37,15 @@ jobs:
make
sudo make install
- name: Install resource_file
if: ${{ matrix.with_resource_file == 'true' }}
run: |
git clone https://github.com/fuzziqersoftware/resource_dasm.git
cd resource_dasm
cmake .
make
sudo make install
- name: Configure CMake
run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
+7
View File
@@ -12,3 +12,10 @@ CTestTestFile.cmake
Testing
cmake_install.cmake
install_manifest.txt
# Files modified by the user and/or server that don't have defaults
system/config.json
system/licenses.nsi
system/players/player_*
system/players/account_*
system/players/bank_*
+56 -30
View File
@@ -11,62 +11,88 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
if (MSVC)
add_compile_options(/W4 /WX)
else()
add_compile_options(-Wall -Wextra -Werror)
add_compile_options(-Wall -Wextra -Werror -Wno-address-of-packed-member)
endif()
include_directories("/usr/local/include")
link_directories("/usr/local/lib")
set(CMAKE_BUILD_TYPE Debug)
# Executable definitions
# Library search
find_path (LIBEVENT_INCLUDE_DIR NAMES event.h)
find_library (LIBEVENT_LIBRARY NAMES event)
find_library (LIBEVENT_CORE NAMES event_core)
find_library (LIBEVENT_THREAD NAMES event_pthreads)
set (LIBEVENT_INCLUDE_DIRS ${LIBEVENT_INCLUDE_DIR})
set (LIBEVENT_LIBRARIES
${LIBEVENT_LIBRARY}
${LIBEVENT_CORE}
${LIBEVENT_THREAD})
${LIBEVENT_CORE})
find_path (RESOURCE_FILE_INCLUDE_DIR NAMES resource_file/ResourceFile.hh)
find_library (RESOURCE_FILE_LIBRARY NAMES resource_file)
if(RESOURCE_FILE_INCLUDE_DIR AND RESOURCE_FILE_LIBRARY)
set(RESOURCE_FILE_FOUND 1)
else()
set(RESOURCE_FILE_FOUND 0)
endif()
# Executable definition
add_executable(newserv
src/FileContentsCache.cc
src/Menu.cc
src/PSOProtocol.cc
src/Client.cc
src/Lobby.cc
src/ServerState.cc
src/Server.cc
src/License.cc
src/PSOEncryption.cc
src/Player.cc
src/SendCommands.cc
src/Channel.cc
src/ChatCommands.cc
src/ReceiveSubcommands.cc
src/ReceiveCommands.cc
src/Version.cc
src/Items.cc
src/LevelTable.cc
src/Client.cc
src/Compression.cc
src/Quest.cc
src/RareItemSet.cc
src/Map.cc
src/NetworkAddresses.cc
src/Text.cc
src/DNSServer.cc
src/ProxyServer.cc
src/Shell.cc
src/ServerShell.cc
src/ProxyShell.cc
src/Episode3.cc
src/FileContentsCache.cc
src/FunctionCompiler.cc
src/IPFrameInfo.cc
src/IPStackSimulator.cc
src/Items.cc
src/LevelTable.cc
src/License.cc
src/Lobby.cc
src/Main.cc
src/Map.cc
src/Menu.cc
src/NetworkAddresses.cc
src/Player.cc
src/ProxyCommands.cc
src/ProxyServer.cc
src/PSOEncryption.cc
src/PSOProtocol.cc
src/Quest.cc
src/RareItemSet.cc
src/ReceiveCommands.cc
src/ReceiveSubcommands.cc
src/SendCommands.cc
src/Server.cc
src/ServerShell.cc
src/ServerState.cc
src/Shell.cc
src/StaticGameData.cc
src/Text.cc
src/Version.cc
)
target_include_directories(newserv PUBLIC ${LIBEVENT_INCLUDE_DIR})
target_link_libraries(newserv phosg ${LIBEVENT_LIBRARIES})
if(RESOURCE_FILE_FOUND)
target_compile_definitions(newserv PUBLIC HAVE_RESOURCE_FILE)
target_include_directories(newserv PUBLIC ${RESOURCE_FILE_INCLUDE_DIR})
target_link_libraries(newserv ${RESOURCE_FILE_LIBRARY})
message(STATUS "libresource_file found; enabling patch support")
else()
message(WARNING "libresource_file not available; disabling patch support")
endif()
# Installation configuration
+115 -49
View File
@@ -1,56 +1,139 @@
# newserv
newserv is a game server for Phantasy Star Online (PSO).
newserv is a game server and proxy for Phantasy Star Online (PSO).
This project includes code that was reverse-engineered by the community in ages long past, and has been included in many projects since then. It also includes some game data from Phantasy Star Online itself; this data was originally created by Sega.
This project is a rewrite of a rewrite of a game server that I wrote many years ago. So far, it works rather well with PSO GC Episodes 1 & 2, and lobbies (but not games) are implemented on Episode 3. newserv probably doesn't work at all for other versions of PSO (DC/PC/BB), since I haven't tested them yet.
This project is a rewrite of a rewrite of a game server that I wrote many years ago. So far, it works well with PSO GC Episodes 1 & 2, and lobbies (but not games) are implemented on Episode 3. Some basic functionality works on PSO PC and PSO BB, but there are probably still some cases that lead to errors (which will disconnect the client). The proxy works well with PSO GC and PSO BB.
Feel free to submit GitHub issues if you find bugs or have feature requests. I'd like to make the server as stable and complete as possible, but I can't promise that I'll respond to issues in a timely manner.
## History
In ages long past (probably 2004? I honestly can't remember), I wrote a proxy for PSO, which I named khyps. This haphazardly-glued-together mess of Windows GUI code and socket programming provided an interface to insert commands into the connection between PSO and its server, enabling some fun new features. Importantly, it also automatically blocked malformed commands which would have crashed the client, providing a safe way to navigate the wasteland that the official Sega servers had turned into after the Action Replay enable code for the game was released.
khyps soon reached "maturity" and became uninteresting, so in 2005 I began writing a PSO server. This project became known as khyller, evolving into a full-featured environment supporting all versions of the game that I had access to - PC, GC, and BB. But as this evolution occurred, the code became increasingly ugly and hard to work with, littered with debugging filth that I never cleaned up and odd coding patterns that I had picked up over the years.
Sometime in 2006 or 2007, I abandoned khyller and rebuilt the entire thing from scratch, resulting in newserv. But this newserv was not the project you're looking at now; 2007's newserv was substantially cleaner in code than khyller but was still quite ugly, and it lacked a few of the more esoteric features I had originally written (for example, the ability to convert any quest into a download quest). I felt better about working with this code, but it still had some stability problems. It turns out that 2007's newserv's concurrency implementation was simply incorrect - I had derived the concept of a mutex myself (before taking any real computer engineering classes) but implemented it incorrectly. No wonder newserv would randomly crash after running seemingly fine for a few days.
A little-known fact is that no version of khyller or newserv was ever tested with the DreamCast versions of PSO. Both projects claimed to support them, but the DC server implementations were based only on chat conversations (likely now lost to time) with other people in the community who had done research on the DC version.
Sometime in October 2018, I had some random cause to reminisce. I looked back in my old code archives and came across newserv. Somehow inspired, I spent a weekend and a couple more evenings rewriting the entire project again, cleaning up ancient patterns I had used eleven years ago, replacing entire modules with simple STL containers, and eliminating even more support files in favor of configuration autodetection. The code is now suitably modern and it no longer has insidious concurrency bugs because it's no longer concurrent - the server is now entirely event-driven.
## Future
This project is primarily for my own nostalgia; I offer no guarantees on how or when this project will advance.
Current known issues / missing features:
- Download quests are mostly implemented, but the client doesn't always accept them. It's probably a format issue in file generation.
- Test all the communication features (info board, simple mail, card search, etc.)
- PSO PC and PSOBB are essentially entirely untested. Only GC is fairly well-tested.
- Add all the chat commands that khyller used to have. (Most, but not all, currently exist in newserv.)
- The trade window isn't implemented yet.
- PSO PC and PSOBB are not well-tested and likely will disconnect when clients try to use unimplemented features. Only GC is known to be stable and mostly complete.
- Patches currently are platform-specific but not version-specific. This makes them quite a bit harder to use properly.
- Find a way to silence audio in RunDOL.s. Some old DOLs don't reset audio systems at load time and it's annoying to hear the crash buzz when the GC hasn't actually crashed.
- Implement private lobbies, and add a way to make games persistent.
## Usage
Currently this code should build on macOS and Ubuntu. It will likely work on other Linux flavors too, but probably will not work on Windows.
Currently this code should build on macOS and Ubuntu. It will likely work on other Linux flavors too. It should work on Windows as well, but I haven't tested it - the build process could be very manual.
So, you've read all of the above and you want to try it out? Here's what you do:
- Make sure you have CMake and libevent installed.
- Build and install phosg (https://github.com/fuzziqersoftware/phosg).
- Run `cmake . && make`.
- Edit system/config.json to your liking.
- Run `./newserv` in the newserv directory. This will start the game server and run the interactive shell. (You can disable the interactive shell later by editing config.json.) You may need `sudo` if newserv's built-in DNS server is enabled.
- Use the interactive shell to add a license. Run `help` in the shell to see how to do this.
There is a probably-not-too-old macOS release on the newserv GitHub repository (look in the right sidebar).
If you're running Linux or want to build newserv yourself, here's what you do:
1. Make sure you have CMake and libevent installed. (`brew install cmake libevent` on macOS, `sudo apt-get install cmake libevent-dev` on most Linuxes)
2. Build and install phosg (https://github.com/fuzziqersoftware/phosg).
3. Optionally, install resource_dasm (https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this.
4. Run `cmake . && make` on the newserv directory.
After building newserv or downloading a release, do this to set it up and use it:
1. In the system/ directory, make a copy of config.example.json named config.json, and edit it appropriately.
2. Run `./newserv` in the newserv directory. This will start the game server and run the interactive shell. You may need `sudo` if newserv's built-in DNS server is enabled.
3. Use the interactive shell to add a license. Run `help` in the shell to see how to do this.
### Installing quests
newserv automatically finds quests in the system/quests/ directory. To install your own quests, or to use quests you've saved using the proxy's set-save-files option, just put them in that directory and name them appropriately.
Standard quest file names should be like `q###-CATEGORY-VERSION.EXT`; battle quests should be named like `b###-VERSION.EXT`, and challenge quests should be named like `c###-VERSION.EXT`. The fields in each filename are:
- `###`: quest number (this doesn't really matter; it should just be unique for the version)
- `CATEGORY`: ret = Retrieval, ext = Extermination, evt = Events, shp = Shops, vr = VR, twr = Tower, gov = Government (BB only), dl = Download (these don't appear during online play), 1p = Solo (BB only)
- `VERSION`: d1 = Dreamcast v1, dc = Dreamcast v2, pc = PC, gc = GameCube Episodes 1 & 2, gc3 = Episode 3, bb = Blue Burst
- `EXT`: file extension (bin, dat, bin.gci, dat.gci, bin.dlq, dat.dlq, or qst)
There are multiple PSO quest formats out there; newserv supports most of them. Specifically, newserv can use quests in any of the following formats:
- bin/dat format: These quests consist of two files with the same base name, a .bin file and a .dat file.
- Unencrypted GCI format: These quests also consist of a .bin and .dat file, but an encoding is applied on top of them. The filenames should end in .bin.gci and .dat.gci. (Note that there also exists an encrypted GCI format, which newserv does not support.)
- Encrypted DLQ format: These quests also consist of a .bin and .dat file, but download quest encryption is applied on top of them. The filenames should end in .bin.dlq and .dat.dlq.
- QST format: These quests consist of only a .qst file, which contains both the .bin and .dat files within it.
When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats.
If you've changed the contents of the quests directory, you can re-index the quests without restarting the server by running `reload quests` in the interactive shell.
All quests, including those originally in GCI or DLQ format, are treated as online quests unless their filenames specify the dl category. newserv allows players to download all quests, even those in non-download categories.
### Patches and DOL files
Everything in this section requires resource_dasm to be installed, so newserv can use the PowerPC assembler and disassembler from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available.
You can put patches in the system/ppc directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC clients that support patching. Patches are written in PowerPC assembly and are compiled when newserv is started. See system/ppc/WriteMemory.s for a commented example of such a function.
You can also put DOL files in the system/dol directory, and they will appear in the Programs menu. Selecting a DOL file there will load the file into their GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWord.s, WriteMemory.s, and RunDOL.s must be present in the system/ppc directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
I mainly built the DOL loading functionality for documentation purposes. By now, there are many better ways to load homebrew code on an unmodified GameCube, but to my knowledge there isn't another open-source implementation of this method in existence.
### Chat commands
The server's shell supports a variety of administration commands. If the interactive shell is enabled, you can enter these commands at any time, even if the prompt isn't visible. Run `help` in the server's shell to see all of the commands and how to use them.
newserv also supports a variety of commands players can use via the chat interface. Any chat message that begins with `$` is treated as a chat command. (If you actually want to send a chat message starting with `$`, type `$$` instead.)
Some commands only work on the game server and not on the proxy server. The chat commands are:
* Information commands
* `$li`: Shows basic information about the lobby or game you're in. If you're on the proxy server, shows information about your connection (remote Guild Card number, client ID, etc.) instead.
* `$what` (game server only): Shows the type, name, and stats of the nearest item on the ground.
* Personal state commands
* `$arrow <color-id>`: Changes your lobby arrow color.
* `$secid <section-id>`: Sets your override section ID. After running this command, any games you create will use your override section ID for rare drops instead of your character's actual section ID. To revert to your actual section id, run `$secid` with no name after it.
* Blue Burst player commands (game server only)
* `$bbchar <username> <password> <1-4>`: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot.
* `$edit <stat> <value>`: Modifies your character data.
* `$item <data>`: Sets the next item to be dropped from an enemy or box.
* Game state commands (game server only)
* `$maxlevel <level>`: Sets the maximum level for players to join the current game.
* `$minlevel <level>`: Sets the minimum level for players to join the current game.
* `$password <password>`: Sets the game's join password. To unlock the game, run `$password` with nothing after it.
* Cheat mode commands
* `$cheat`: Enables or disables cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. This command does nothing on the proxy server, since cheat mode is always enabled there.
* `$infhp` / `$inftp`: Enables or disables infinite HP or TP mode. Applies to only you. In infinite HP mode, one-hit KO attacks will still kill you.
* `$warp <area-id>`: Warps yourself to the given area.
* `$next` (game server only): Warps yourself to the next area.
* `$swa`: Enables or disables switch assist. When enabled, the server will attempt to automatically unlock two-player doors in solo games if you step on both switches sequentially.
* Configuration commands
* `$event <event>`: Sets the current holiday event in the current lobby. Holiday events are documented in the "Using $event" item in the information menu. If you're on the proxy server, only you will see the new event; other players will not.
* `$allevent <event>` (game server only): Sets the current holiday event in all lobbies.
* `$song <song-id>` (game server only, Episode 3 only): Plays a specific song in the current lobby.
* Administration commands (game server only)
* `$ann <message>`: Sends an announcement message. The message text is sent to all players in all games and lobbies.
* `$ax <message>`: Sends a message to the server's terminal. This cannot be used to run server shell commands; it only prints text to stderr.
* `$silence <identifier>`: Silences a player (remove their ability to chat) or unsilences a player. The identifier may be the player's name or Guild Card number.
* `$kick <identifier>`: Disconnects a player. The identifier may be the player's name or Guild Card number.
* `$ban <identifier>`: Bans a player. The identifier may be the player's name or Guild Card number.
### Using newserv as a proxy
If you want to play online on remote servers rather than running your own server, newserv also includes a PSO proxy. Currently this works with PSO GC and may work with PC; it also works with some BB clients in specific situations.
To use the proxy, add an entry to the ProxyDestinations dictionary in config.json, then run newserv and connect to it as normal (see below). You'll see a "Proxy server" option in the main menu, and you can pick which remote server to connect to.
A few things to be aware of when using the proxy server:
- On PC and GC, using the Change Ship or Change Block actions from the lobby counter will bring you back to newserv's main menu, not the remote server's ship select. You can go back to the server you were just on by choosing it from newserv's proxy server menu again.
- The remote server will probably try to assign you a Guild Card number that doesn't match the one you have on newserv. The proxy server rewrites the commands on the fly to make it look like the remote server assigned you the same Guild Card number as you have on newserv, but if the remote server has some external integrations (e.g. forum or Discord bots), they will use the Guild Card number that the remote server believes it has assigned to you. The number assigned by the remote server is shown to you when you first connect to the remote server, and you can retrieve it in lobbies or during games with the $li command.
- There are shell commands that affect clients on the proxy (run 'help' in the shell to see what they are). All proxy commands in the shell only work when there's exactly one client connected through the proxy, since there isn't (yet) a way to say via the shell which session you want to affect.
### Connecting local clients
If you're running PSO on a real GameCube, you can make it connect to newserv by setting its default gateway and DNS server addresses to newserv's address. Note that newserv's DNS server is disabled by default; you'll have to enable it in config.json.
If you're running PSO on a real GameCube, you can make it connect to newserv by setting its default gateway and DNS server addresses to newserv's address. newserv's DNS server must be running on port 53 and accessible.
If you have PSO Plus or Episode III, it won't want to connect to a server on the same local network as the GameCube itself, as determined by the GC's IP address and subnet mask. In the old days, one way to get around this was to create a fake network adapter on the server with an IP address on a different subnet (or use an existing real one), tell the GameCube that the server is the default gateway, and have the server reply to the DNS request with its non-local IP address. To do this with newserv, just set LocalAddress in the config file to a different interface. For example, if the GameCube is on the 192.168.0.x network and your other adapter has address 10.0.1.6, set LocalAddress to 10.0.1.6. This may not work on modern systems or on non-Windows machines - I haven't tested it in many years.
If you have PSO Plus or Episode III, it won't want to connect to a server on the same local network as the GameCube itself, as determined by the GameCube's IP address and subnet mask. In the old days, one way to get around this was to create a fake network adapter on the server (or use an existing real one) that has an IP address on a different subnet, tell the GameCube that the server is the default gateway, and have the server reply to the DNS request with its non-local IP address. To do this with newserv, just set LocalAddress in the config file to a different interface. For example, if the GameCube is on the 192.168.0.x network and your other adapter has address 10.0.1.6, set newserv's LocalAddress to 10.0.1.6 and set PSO's DNS server and default gateway addresses to the server's 192.168.0.x address. This may not work on modern systems or on non-Windows machines - I haven't tested it in many years.
If you're emulating PSO using Dolphin on macOS, you can make it connect to a newserv instance running on the same machine via the tapserver interface. This works for all PSO versions, including Plus and Episode III, without the trickery described above. To do this:
- Use a build of Dolphin that has tapserver support, and set the BBA type to tapserver (Config -> GameCube -> SP1).
- Enable the IP stack simulator according to the comments in config.json, and start newserv. You do not need to install or run tapserver.
If you're emulating PSO using a version of Dolphin with tapserver support (currently only the macOS version), you can make it connect to a newserv instance running on the same machine via the tapserver interface. This works for all PSO versions, including Plus and Episode III, without the trickery described above. To do this:
- Set Dolphin's BBA type to tapserver (Config -> GameCube -> SP1).
- Enable newserv's IP stack simulator according to the comments in config.json, and start newserv. You do not need to install or run tapserver.
- In PSO, you have to configure the network settings manually (DHCP doesn't work), but the actual values don't matter as long as they're valid IP addresses. Example values:
- IP address: `10.0.1.5`
- Subnet mask: `255.255.255.0`
@@ -61,23 +144,6 @@ If you're emulating PSO using Dolphin on macOS, you can make it connect to a new
### Connecting external clients
If you want to accept connections from outside your local network, you'll need to set ExternalAddress to your public IP address in the configuration file, and you'll likely need to open some ports in your router's NAT configuration. You'll need to open the following ports depending on which client versions you want to be able to connect:
If you want to accept connections from outside your local network, you'll need to set ExternalAddress to your public IP address in the configuration file, and you'll likely need to open some ports in your router's NAT configuration - specifically, all the TCP ports listed in PortConfiguration in config.json.
PSO PC 9100, 9300, 9420, 10000
PSO GC 1.0 JP 9000, 9421
PSO GC 1.1 JP 9001, 9421
PSO GC Ep3 JP 9003, 9421
PSO GC 1.0 US 9100, 9421
PSO GC Ep3 US 9103, 9421
PSO GC 1.0 EU 9200, 9421
PSO GC 1.1 EU 9201, 9421
PSO GC Ep3 EU 9203, 9421
PSO BB 9422, 11000, 12000, 12004, 12005, 12008
For GC clients, you'll have to use newserv's built-in DNS server or set up your own DNS server as well. Remote players can connect to your server by entering your DNS server's IP address in their client's network configuration. If you use newserv's built-in DNS server, you'll also need to forward UDP port 53 to your newserv instance.
### Using newserv as a proxy
If you want to play online on remote servers rather than running your own server, newserv also includes a PSO proxy. Currently this works with PSO GC and may work with PC, but not with BB.
Run newserv like `./newserv --proxy-destination=1.1.1.1` (replace the IP address appropriately for the server you want to connect to). This works for normal clients (using the connection parameters in config.json), as well as tapserver clients.
For GC clients, you'll have to use newserv's built-in DNS server or set up your own DNS server as well. If you want external clients to be able to use your DNS server, you'll have to forward UDP port 53 to your newserv instance. Remote players can then connect to your server by entering your DNS server's IP address in their client's network configuration.
+393
View File
@@ -0,0 +1,393 @@
#include "Channel.hh"
#include <event2/buffer.h>
#include <event2/bufferevent.h>
#include <event2/event.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <phosg/Network.hh>
#include <phosg/Time.hh>
#include "Version.hh"
using namespace std;
extern bool use_terminal_colors;
static void flush_and_free_bufferevent(struct bufferevent* bev) {
bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_FINISHED);
bufferevent_free(bev);
}
Channel::Channel(
GameVersion version,
on_command_received_t on_command_received,
on_error_t on_error,
void* context_obj,
const string& name,
TerminalFormat terminal_send_color,
TerminalFormat terminal_recv_color)
: bev(nullptr, flush_and_free_bufferevent),
version(version),
name(name),
terminal_send_color(terminal_send_color),
terminal_recv_color(terminal_recv_color),
on_command_received(on_command_received),
on_error(on_error),
context_obj(context_obj) {
}
Channel::Channel(
struct bufferevent* bev,
GameVersion version,
on_command_received_t on_command_received,
on_error_t on_error,
void* context_obj,
const string& name,
TerminalFormat terminal_send_color,
TerminalFormat terminal_recv_color)
: bev(nullptr, flush_and_free_bufferevent),
version(version),
name(name),
terminal_send_color(terminal_send_color),
terminal_recv_color(terminal_recv_color),
on_command_received(on_command_received),
on_error(on_error),
context_obj(context_obj) {
this->set_bufferevent(bev);
}
void Channel::replace_with(
Channel&& other,
on_command_received_t on_command_received,
on_error_t on_error,
void* context_obj,
const std::string& name) {
this->set_bufferevent(other.bev.release());
this->local_addr = other.local_addr;
this->remote_addr = other.remote_addr;
this->is_virtual_connection = other.is_virtual_connection;
this->version = other.version;
this->crypt_in = other.crypt_in;
this->crypt_out = other.crypt_out;
this->name = name;
this->terminal_send_color = other.terminal_send_color;
this->terminal_recv_color = other.terminal_recv_color;
this->on_command_received = on_command_received;
this->on_error = on_error;
this->context_obj = context_obj;
other.disconnect(); // Clears crypts, addrs, etc.
}
void Channel::set_bufferevent(struct bufferevent* bev) {
this->bev.reset(bev);
if (this->bev.get()) {
int fd = bufferevent_getfd(this->bev.get());
if (fd < 0) {
this->is_virtual_connection = true;
memset(&this->local_addr, 0, sizeof(this->local_addr));
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
} else {
this->is_virtual_connection = false;
get_socket_addresses(fd, &this->local_addr, &this->remote_addr);
}
bufferevent_setcb(this->bev.get(),
&Channel::dispatch_on_input, nullptr,
&Channel::dispatch_on_error, this);
bufferevent_enable(this->bev.get(), EV_READ | EV_WRITE);
} else {
this->is_virtual_connection = false;
memset(&this->local_addr, 0, sizeof(this->local_addr));
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
}
}
void Channel::disconnect() {
if (this->bev.get()) {
// If the output buffer is not empty, move the bufferevent into the draining
// pool instead of disconnecting it, to make sure all the data gets sent.
struct evbuffer* out_buffer = bufferevent_get_output(this->bev.get());
if (evbuffer_get_length(out_buffer) == 0) {
this->bev.reset(); // Destructor flushes and frees the bufferevent
} else {
// The callbacks will free it when all the data is sent or the client
// disconnects
auto on_output = +[](struct bufferevent* bev, void*) -> void {
flush_and_free_bufferevent(bev);
};
auto on_error = +[](struct bufferevent* bev, short events, void*) -> void {
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
log(WARNING, "Disconnecting channel caused error %d (%s)", err,
evutil_socket_error_to_string(err));
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
bufferevent_flush(bev, EV_WRITE, BEV_FINISHED);
bufferevent_free(bev);
}
};
struct bufferevent* bev = this->bev.release();
bufferevent_setcb(bev, nullptr, on_output, on_error, bev);
bufferevent_disable(bev, EV_READ);
}
}
memset(&this->local_addr, 0, sizeof(this->local_addr));
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
this->is_virtual_connection = false;
this->crypt_in.reset();
this->crypt_out.reset();
}
Channel::Message Channel::recv(bool print_contents) {
struct evbuffer* buf = bufferevent_get_input(this->bev.get());
size_t header_size = (this->version == GameVersion::BB) ? 8 : 4;
PSOCommandHeader header;
if (evbuffer_copyout(buf, &header, header_size)
< static_cast<ssize_t>(header_size)) {
throw out_of_range("no command available");
}
if (this->crypt_in.get()) {
this->crypt_in->decrypt(&header, header_size, false);
}
size_t command_logical_size = header.size(version);
// If encryption is enabled, BB pads commands to 8-byte boundaries, and this
// is not reflected in the size field. This logic does not occur if encryption
// is not yet enabled.
size_t command_physical_size = (this->crypt_in.get() && (version == GameVersion::BB))
? ((command_logical_size + 7) & ~7) : command_logical_size;
if (evbuffer_get_length(buf) < command_physical_size) {
throw out_of_range("no command available");
}
// If we get here, then there is a full command in the buffer. Some encryption
// algorithms' advancement depends on the decrypted data, so we have to
// actually decrypt the header again (with advance=true) to keep them in a
// consistent state.
string header_data(header_size, '\0');
if (evbuffer_remove(buf, header_data.data(), header_data.size())
< static_cast<ssize_t>(header_data.size())) {
throw logic_error("enough bytes available, but could not remove them");
}
if (this->crypt_in.get()) {
this->crypt_in->decrypt(header_data.data(), header_data.size());
}
string command_data(command_physical_size - header_size, '\0');
if (evbuffer_remove(buf, command_data.data(), command_data.size())
< static_cast<ssize_t>(command_data.size())) {
throw logic_error("enough bytes available, but could not remove them");
}
if (this->crypt_in.get()) {
this->crypt_in->decrypt(command_data.data(), command_data.size());
}
command_data.resize(command_logical_size - header_size);
if (print_contents && (this->terminal_recv_color != TerminalFormat::END)) {
if (use_terminal_colors && this->terminal_recv_color != TerminalFormat::NORMAL) {
print_color_escape(stderr, this->terminal_recv_color, TerminalFormat::BOLD, TerminalFormat::END);
}
string name_token;
if (!this->name.empty()) {
name_token = " from " + this->name;
}
log(INFO, "Received%s (version=%s command=%04hX flag=%08X)",
name_token.c_str(),
name_for_version(this->version),
header.command(this->version),
header.flag(this->version));
vector<struct iovec> iovs;
iovs.emplace_back(iovec{.iov_base = header_data.data(), .iov_len = header_data.size()});
iovs.emplace_back(iovec{.iov_base = command_data.data(), .iov_len = command_data.size()});
print_data(stderr, iovs, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR);
if (use_terminal_colors && this->terminal_recv_color != TerminalFormat::NORMAL) {
print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END);
}
}
return {
.command = header.command(this->version),
.flag = header.flag(this->version),
.data = move(command_data),
};
}
void Channel::send(uint16_t cmd, uint32_t flag, const void* data, size_t size,
bool print_contents) {
if (!this->connected()) {
log(WARNING, "Attempted to send command on closed channel; dropping data");
}
string send_data;
size_t logical_size;
size_t send_data_size = 0;
switch (this->version) {
case GameVersion::GC:
case GameVersion::DC: {
PSOCommandHeaderDCGC header;
if (this->crypt_out.get()) {
send_data_size = (sizeof(header) + size + 3) & ~3;
} else {
send_data_size = (sizeof(header) + size);
}
logical_size = send_data_size;
header.command = cmd;
header.flag = flag;
header.size = send_data_size;
send_data.append(reinterpret_cast<const char*>(&header), sizeof(header));
break;
}
case GameVersion::PC:
case GameVersion::PATCH: {
PSOCommandHeaderPC header;
if (this->crypt_out.get()) {
send_data_size = (sizeof(header) + size + 3) & ~3;
} else {
send_data_size = (sizeof(header) + size);
}
logical_size = send_data_size;
header.size = send_data_size;
header.command = cmd;
header.flag = flag;
send_data.append(reinterpret_cast<const char*>(&header), sizeof(header));
break;
}
case GameVersion::BB: {
// BB has an annoying behavior here: command lengths must be multiples of
// 4, but the actual data length must be a multiple of 8. If the size
// field is not divisible by 8, 4 extra bytes are sent anyway. This
// behavior only applies when encryption is enabled - any commands sent
// before encryption is enabled have no size restrictions (except they
// must include a full header and must fit in the client's receive
// buffer), and no implicit extra bytes are sent.
PSOCommandHeaderBB header;
if (this->crypt_out.get()) {
send_data_size = (sizeof(header) + size + 7) & ~7;
} else {
send_data_size = (sizeof(header) + size);
}
logical_size = (sizeof(header) + size + 3) & ~3;
header.size = logical_size;
header.command = cmd;
header.flag = flag;
send_data.append(reinterpret_cast<const char*>(&header), sizeof(header));
break;
}
default:
throw logic_error("unimplemented game version in send_command");
}
// All versions of PSO I've seen (PC, GC, BB) have a receive buffer 0x7C00
// bytes in size
if (send_data_size > 0x7C00) {
throw runtime_error("outbound command too large");
}
if (send_data.size() < send_data_size) {
send_data.append(reinterpret_cast<const char*>(data), size);
send_data.resize(send_data_size, '\0');
}
if (print_contents && (this->terminal_send_color != TerminalFormat::END)) {
string name_token;
if (!this->name.empty()) {
name_token = " to " + this->name;
}
if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) {
print_color_escape(stderr, TerminalFormat::FG_YELLOW, TerminalFormat::BOLD, TerminalFormat::END);
}
log(INFO, "Sending%s (version=%s command=%04hX flag=%08X)",
name_token.c_str(), name_for_version(version), cmd, flag);
print_data(stderr, send_data.data(), logical_size, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR);
if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) {
print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END);
}
}
if (this->crypt_out.get()) {
this->crypt_out->encrypt(send_data.data(), send_data.size());
}
struct evbuffer* buf = bufferevent_get_output(this->bev.get());
evbuffer_add(buf, send_data.data(), send_data.size());
}
void Channel::send(uint16_t cmd, uint32_t flag, const string& data, bool print_contents) {
this->send(cmd, flag, data.data(), data.size(), print_contents);
}
void Channel::send(const void* data, size_t size, bool print_contents) {
size_t header_size = (this->version == GameVersion::BB) ? 8 : 4;
const auto* header = reinterpret_cast<const PSOCommandHeader*>(data);
this->send(
header->command(this->version),
header->flag(this->version),
reinterpret_cast<const uint8_t*>(data) + header_size,
size - header_size,
print_contents);
}
void Channel::send(const string& data, bool print_contents) {
return this->send(data.data(), data.size(), print_contents);
}
void Channel::dispatch_on_input(struct bufferevent*, void* ctx) {
Channel* ch = reinterpret_cast<Channel*>(ctx);
// The client can be disconnected during on_command_received, so we have to
// make sure ch->bev is valid every time before calling recv()
while (ch->bev.get()) {
Message msg;
try {
msg = ch->recv();
} catch (const out_of_range&) {
break;
} catch (const exception& e) {
log(WARNING, "Error receiving on channel: %s", e.what());
ch->on_error(*ch, BEV_EVENT_ERROR);
break;
}
if (ch->on_command_received) {
ch->on_command_received(*ch, msg.command, msg.flag, msg.data);
}
}
}
void Channel::dispatch_on_error(struct bufferevent*, short events, void* ctx) {
Channel* ch = reinterpret_cast<Channel*>(ctx);
if (ch->on_error) {
ch->on_error(*ch, events);
} else {
ch->disconnect();
}
}
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include <netinet/in.h>
#include <memory>
#include <string>
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "Version.hh"
struct Channel {
std::unique_ptr<struct bufferevent, void (*)(struct bufferevent*)> bev;
struct sockaddr_storage local_addr;
struct sockaddr_storage remote_addr;
bool is_virtual_connection;
GameVersion version;
std::shared_ptr<PSOEncryption> crypt_in;
std::shared_ptr<PSOEncryption> crypt_out;
std::string name;
TerminalFormat terminal_send_color;
TerminalFormat terminal_recv_color;
struct Message {
uint16_t command;
uint32_t flag;
std::string data;
};
typedef void (*on_command_received_t)(Channel&, uint16_t, uint32_t, std::string&);
typedef void (*on_error_t)(Channel&, short);
on_command_received_t on_command_received;
on_error_t on_error;
void* context_obj;
Channel(
GameVersion version,
on_command_received_t on_command_received,
on_error_t on_error,
void* context_obj,
const std::string& name = "",
TerminalFormat terminal_send_color = TerminalFormat::END,
TerminalFormat terminal_recv_color = TerminalFormat::END);
Channel(
struct bufferevent* bev,
GameVersion version,
on_command_received_t on_command_received,
on_error_t on_error,
void* context_obj,
const std::string& name = "",
TerminalFormat terminal_send_color = TerminalFormat::END,
TerminalFormat terminal_recv_color = TerminalFormat::END);
Channel(const Channel& other) = delete;
Channel(Channel&& other) = delete;
Channel& operator=(const Channel& other) = delete;
Channel& operator=(Channel&& other) = delete;
void replace_with(
Channel&& other,
on_command_received_t on_command_received,
on_error_t on_error,
void* context_obj,
const std::string& name = "");
void set_bufferevent(struct bufferevent* bev);
inline bool connected() const {
return this->bev.get() != nullptr;
}
void disconnect();
// Receives a message. Throws std::out_of_range if no messages are available.
Message recv(bool print_contents = true);
// Sends a message with an automatically-constructed header.
void send(uint16_t cmd, uint32_t flag = 0, const void* data = nullptr, size_t size = 0, bool print_contents = true);
void send(uint16_t cmd, uint32_t flag, const std::string& data, bool print_contents = true);
// Sends a message with a pre-existing header (as the first few bytes in the
// data)
void send(const void* data = nullptr, size_t size = 0, bool print_contents = true);
void send(const std::string& data, bool print_contents = true);
private:
static void dispatch_on_input(struct bufferevent*, void* ctx);
static void dispatch_on_error(struct bufferevent*, short events, void* ctx);
};
+412 -460
View File
File diff suppressed because it is too large Load Diff
+4 -26
View File
@@ -8,31 +8,9 @@
#include "ServerState.hh"
#include "Lobby.hh"
#include "Client.hh"
const std::string& name_for_section_id(uint8_t section_id);
std::u16string u16name_for_section_id(uint8_t section_id);
uint8_t section_id_for_name(const std::string& name);
uint8_t section_id_for_name(const std::u16string& name);
const std::string& name_for_event(uint8_t event);
std::u16string u16name_for_event(uint8_t event);
uint8_t event_for_name(const std::string& name);
uint8_t event_for_name(const std::u16string& name);
const std::string& name_for_lobby_type(uint8_t type);
std::u16string u16name_for_lobby_type(uint8_t type);
uint8_t lobby_type_for_name(const std::string& name);
uint8_t lobby_type_for_name(const std::u16string& name);
const std::string& name_for_technique(uint8_t tech);
std::u16string u16name_for_technique(uint8_t tech);
uint8_t technique_for_name(const std::string& name);
uint8_t technique_for_name(const std::u16string& name);
const std::string& name_for_npc(uint8_t npc);
std::u16string u16name_for_npc(uint8_t npc);
uint8_t npc_for_name(const std::string& name);
uint8_t npc_for_name(const std::u16string& name);
#include "ProxyServer.hh"
void process_chat_command(std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c, const char16_t* text);
std::shared_ptr<Client> c, const std::u16string& text);
void process_chat_command(std::shared_ptr<ServerState> s,
ProxyServer::LinkedSession& session, const std::u16string& text);
+40 -37
View File
@@ -15,7 +15,7 @@ using namespace std;
static const uint64_t CLIENT_CONFIG_MAGIC = 0x492A890E82AC9839;
const uint64_t CLIENT_CONFIG_MAGIC = 0x492A890E82AC9839;
@@ -24,60 +24,57 @@ Client::Client(
GameVersion version,
ServerBehavior server_behavior)
: version(version),
bb_game_state(0),
flags(flags_for_version(this->version, 0)),
bev(bev),
channel(bev, this->version, nullptr, nullptr, this, "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN),
server_behavior(server_behavior),
should_disconnect(false),
play_time_begin(now()),
last_recv_time(this->play_time_begin),
last_send_time(0),
should_send_to_lobby_server(false),
proxy_destination_address(0),
proxy_destination_port(0),
x(0.0f),
z(0.0f),
area(0),
lobby_id(0),
lobby_client_id(0),
lobby_arrow_color(0),
prefer_high_lobby_client_id(false),
next_exp_value(0),
override_section_id(-1),
infinite_hp(false),
infinite_tp(false),
can_chat(true) {
int fd = bufferevent_getfd(this->bev);
if (fd < 0) {
this->is_virtual_connection = true;
memset(&this->local_addr, 0, sizeof(this->local_addr));
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
} else {
this->is_virtual_connection = false;
get_socket_addresses(fd, &this->local_addr, &this->remote_addr);
}
switch_assist(false),
can_chat(true),
pending_bb_save_player_index(0),
dol_base_addr(0) {
this->last_switch_enabled_command.subcommand = 0;
memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr));
}
bool Client::send(string&& data) {
if (!this->bev) {
return false;
void Client::set_license(shared_ptr<const License> l) {
this->license = l;
this->game_data.serial_number = this->license->serial_number;
if (this->version == GameVersion::BB) {
this->game_data.bb_username = this->license->username;
}
if (this->crypt_out.get()) {
this->crypt_out->encrypt(data.data(), data.size());
}
struct evbuffer* buf = bufferevent_get_output(this->bev);
evbuffer_add(buf, data.data(), data.size());
return true;
}
ClientConfig Client::export_config() const {
ClientConfig cc;
cc.magic = CLIENT_CONFIG_MAGIC;
cc.bb_game_state = this->bb_game_state;
cc.bb_player_index = this->bb_player_index;
cc.flags = this->flags;
for (size_t x = 0; x < 5; x++) {
cc.unused[x] = 0xFFFFFFFF;
}
for (size_t x = 0; x < 2; x++) {
cc.unused_bb_only[x] = 0xFFFFFFFF;
}
cc.proxy_destination_address = this->proxy_destination_address;
cc.proxy_destination_port = this->proxy_destination_port;
cc.unused.clear(0xFF);
return cc;
}
ClientConfigBB Client::export_config_bb() const {
ClientConfigBB cc;
cc.cfg = this->export_config();
cc.bb_game_state = this->bb_game_state;
cc.bb_player_index = this->game_data.bb_player_index;
cc.unused.clear(0xFF);
return cc;
}
@@ -85,7 +82,13 @@ void Client::import_config(const ClientConfig& cc) {
if (cc.magic != CLIENT_CONFIG_MAGIC) {
throw invalid_argument("invalid client config");
}
this->bb_game_state = cc.bb_game_state;
this->bb_player_index = cc.bb_player_index;
this->flags = cc.flags;
this->proxy_destination_address = cc.proxy_destination_address;
this->proxy_destination_port = cc.proxy_destination_port;
}
void Client::import_config(const ClientConfigBB& cc) {
this->import_config(cc.cfg);
this->bb_game_state = cc.bb_game_state;
this->game_data.bb_player_index = cc.bb_player_index;
}
+65 -37
View File
@@ -4,30 +4,59 @@
#include <memory>
#include "Channel.hh"
#include "CommandFormats.hh"
#include "FunctionCompiler.hh"
#include "License.hh"
#include "Player.hh"
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "Text.hh"
enum class ServerBehavior {
SplitReconnect = 0,
LoginServer,
LobbyServer,
DataServerBB,
PatchServer,
};
extern const uint64_t CLIENT_CONFIG_MAGIC;
struct ClientConfig {
uint64_t magic;
uint8_t bb_game_state;
uint8_t bb_player_index;
uint16_t flags;
uint32_t unused[5];
uint32_t unused_bb_only[2];
} __attribute__((packed));
struct Client {
enum Flag {
// For patch server clients, client is Blue Burst rather than PC
BB_PATCH = 0x0001,
// After joining a lobby, client will no longer send D6 commands when they
// close message boxes
NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN = 0x0002,
// Client has the above flag and has already joined a lobby, or is not GC
NO_MESSAGE_BOX_CLOSE_CONFIRMATION = 0x0004,
// Client is Episode 3, should be able to see CARD lobbies, and should only
// be able to see/join games with the IS_EPISODE_3 flag
EPISODE_3 = 0x0008,
// Client is DC v1 (disables some features)
DCV1 = 0x0010,
// Client is loading into a game
LOADING = 0x0020,
// Client is loading a quest
LOADING_QUEST = 0x0040,
// Client is in the information menu (login server only)
IN_INFORMATION_MENU = 0x0080,
// Client is at the welcome message (login server only)
AT_WELCOME_MESSAGE = 0x0100,
// Client disconnect if it receives B2 (send_function_call)
DOES_NOT_SUPPORT_SEND_FUNCTION_CALL = 0x0200,
// Client has already received a 97 (enable saves) command, so don't show
// the programs menu anymore
SAVE_ENABLED = 0x0400,
// TODO: Do DCv1 and PC support send_function_call? Here we assume they don't
DEFAULT_V1 = DCV1 | NO_MESSAGE_BOX_CLOSE_CONFIRMATION | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL,
DEFAULT_V2_DC = NO_MESSAGE_BOX_CLOSE_CONFIRMATION,
DEFAULT_V2_PC = NO_MESSAGE_BOX_CLOSE_CONFIRMATION | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL,
DEFAULT_V3_GC = 0x0000,
DEFAULT_V3_GC_PLUS = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL,
DEFAULT_V3_GC_EP3 = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | EPISODE_3 | DOES_NOT_SUPPORT_SEND_FUNCTION_CALL,
DEFAULT_V4_BB = NO_MESSAGE_BOX_CLOSE_CONFIRMATION_AFTER_LOBBY_JOIN | NO_MESSAGE_BOX_CLOSE_CONFIRMATION | SAVE_ENABLED,
};
// License & account
std::shared_ptr<const License> license;
GameVersion version;
@@ -36,49 +65,48 @@ struct Client {
// config can be up to 0x20 bytes; on BB it can be 0x28 bytes. We don't use
// all of that space.
uint8_t bb_game_state;
uint8_t bb_player_index;
uint16_t flags;
// Encryption
std::unique_ptr<PSOEncryption> crypt_in;
std::unique_ptr<PSOEncryption> crypt_out;
// Network
struct sockaddr_storage local_addr;
struct sockaddr_storage remote_addr;
struct bufferevent* bev;
Channel channel;
struct sockaddr_storage next_connection_addr;
ServerBehavior server_behavior;
bool is_virtual_connection;
bool should_disconnect;
std::string recv_buffer;
bool should_send_to_lobby_server;
uint32_t proxy_destination_address;
uint16_t proxy_destination_port;
// timing & menus
uint64_t play_time_begin; // time of connection (used for incrementing play time on BB)
uint64_t last_recv_time; // time of last data received
uint64_t last_send_time; // time of last data sent
// lobby/positioning
// Lobby/positioning
float x;
float z;
uint32_t area; // which area is the client in?
uint32_t lobby_id; // which lobby is this person in?
uint8_t lobby_client_id; // which client number is this person?
uint8_t lobby_arrow_color; // lobby arrow color ID
Player player;
bool prefer_high_lobby_client_id;
ClientGameData game_data;
// miscellaneous (used by chat commands)
// Miscellaneous (used by chat commands)
uint32_t next_exp_value; // next EXP value to give
int16_t override_section_id; // valid if >= 0
bool infinite_hp; // cheats enabled
bool infinite_tp; // cheats enabled
bool switch_assist; // cheats enabled
G_SwitchStateChanged_6x05 last_switch_enabled_command;
bool can_chat;
std::string pending_bb_save_username;
uint8_t pending_bb_save_player_index;
Client(struct bufferevent* bev, GameVersion version,
ServerBehavior server_behavior);
// DOL file loading state
uint32_t dol_base_addr;
std::shared_ptr<DOLFileIndex::DOLFile> loading_dol_file;
// adds data to the client's output buffer, encrypting it first
bool send(std::string&& data);
Client(struct bufferevent* bev, GameVersion version, ServerBehavior server_behavior);
void set_license(std::shared_ptr<const License> l);
ClientConfig export_config() const;
ClientConfigBB export_config_bb() const;
void import_config(const ClientConfig& cc);
void import_config(const ClientConfigBB& cc);
};
File diff suppressed because it is too large Load Diff
+19 -16
View File
@@ -13,18 +13,15 @@ using namespace std;
struct prs_compress_ctx {
unsigned char bitpos;
uint8_t bitpos;
std::string forward_log;
std::string output;
prs_compress_ctx() : bitpos(0) { }
prs_compress_ctx() : bitpos(0), forward_log("\0", 1) { }
string finish() {
this->put_control_bit(0);
this->put_control_bit(1);
if (this->bitpos != 0) {
this->forward_log[0] = ((this->forward_log[0] << this->bitpos) >> 8);
}
this->put_static_data(0);
this->put_static_data(0);
this->output += this->forward_log;
@@ -33,8 +30,9 @@ struct prs_compress_ctx {
}
void put_control_bit_nosave(bool bit) {
this->forward_log[0] = this->forward_log[0] >> 1;
this->forward_log[0] |= ((!!bit) << 7);
if (bit) {
this->forward_log[0] |= 1 << this->bitpos;
}
this->bitpos++;
}
@@ -53,7 +51,7 @@ struct prs_compress_ctx {
}
void put_static_data(uint8_t data) {
this->forward_log += static_cast<char>(data);
this->forward_log.push_back(static_cast<char>(data));
}
void raw_byte(uint8_t value) {
@@ -98,20 +96,21 @@ struct prs_compress_ctx {
}
};
string prs_compress(const string& data) {
string prs_compress(const void* vdata, size_t size) {
const uint8_t* data = reinterpret_cast<const uint8_t*>(vdata);
prs_compress_ctx pc;
ssize_t data_ssize = static_cast<ssize_t>(data.size());
ssize_t data_ssize = static_cast<ssize_t>(size);
ssize_t read_offset = 0;
while (read_offset < data_ssize) {
// look for a chunk of data in history matching what's at the current offset
ssize_t best_offset = 0;
ssize_t best_size = 0;
for (ssize_t this_offset = -3;
(this_offset + data_ssize >= 0) &&
(this_offset > -0x1FF0) &&
(best_size < 255);
for (ssize_t this_offset = -3; // min copy size is 3 bytes
(this_offset + read_offset >= 0) && // don't go before the beginning
(this_offset > -0x1FF0) && // max offset is -0x1FF0
(best_size < 255); // max size is 0xFF bytes
this_offset--) {
// for this offset, expand the match as much as possible
@@ -119,8 +118,8 @@ string prs_compress(const string& data) {
while ((this_size < 0x100) && // max copy size is 255 bytes
((this_offset + this_size) < 0) && // don't copy past the read offset
(this_size <= data_ssize - read_offset) && // don't copy past the end
!memcmp(data.data() + read_offset + this_offset,
data.data() + read_offset, this_size)) {
!memcmp(data + read_offset + this_offset, data + read_offset,
this_size)) {
this_size++;
}
this_size--;
@@ -145,6 +144,10 @@ string prs_compress(const string& data) {
return pc.finish();
}
string prs_compress(const string& data) {
return prs_compress(data.data(), data.size());
}
static int16_t get_u8_or_eof(StringReader& r) {
+2
View File
@@ -6,6 +6,8 @@
std::string prs_compress(const void* vdata, size_t size);
std::string prs_compress(const std::string& data);
std::string prs_decompress(const std::string& data, size_t max_size = 0);
size_t prs_decompress_size(const std::string& data, size_t max_size = 0);
+1 -1
View File
@@ -101,7 +101,7 @@ void DNSServer::on_receive_message(int fd, short) {
} else if (bytes < 0x0C) {
log(WARNING, "[DNSServer] input query too small");
print_data(stderr, input);
print_data(stderr, input.data(), bytes);
} else {
input.resize(bytes);
+894
View File
@@ -0,0 +1,894 @@
#include "Episode3.hh"
#include <stdint.h>
#include <array>
#include <phosg/Filesystem.hh>
#include "Compression.hh"
#include "Text.hh"
using namespace std;
static const vector<const char*> name_for_card_type({
"HunterSC",
"ArkzSC",
"Item",
"Creature",
"Action",
"Assist",
});
static const unordered_map<uint8_t, const char*> description_for_when({
{0x01, "Set"}, // TODO: Is 01 this, or "Permanent"?
{0x02, "Attack"},
{0x03, "??? (TODO)"},
{0x04, "Before turn"},
{0x05, "Destroyed"},
{0x0A, "Permanent"}, // only used on Tollaw; could be same as 01
{0x0B, "Battle"},
{0x0C, "Opponent destroyed"}, // TODO: but this is also used for some support things like Shifta, and for Snatch, which also applies when opponents are not destroyed
{0x0D, "Attack lands"},
{0x0E, "Before attack phase"},
{0x16, "Battle end"},
{0x17, "Each defense"},
{0x20, "Each attack"},
{0x22, "Act phase"},
{0x27, "Move phase"},
{0x29, "Set and act phases"},
{0x33, "Defense phase"},
{0x3D, "Battle"}, // TODO: how is this different from 3D and 0B?
{0x3E, "Battle"}, // TODO: how is this different from 3D and 0B?
{0x3F, "Each defense"}, // TODO: how is this different from 17?
{0x46, "On specific turn"},
});
static const unordered_map<string, const char*> description_for_expr_token({
{"f", "Number of FCs controlled by current SC"},
{"d", "Die roll"},
{"ap", "Attacker AP"}, // Unused
{"tp", "Attacker TP"},
{"hp", "Attacker HP"}, // TODO: How is this different from ehp?
{"mhp", "Attacker maximum HP"},
{"dm", "Unknown: dm"}, // Unused
{"tdm", "Unknown: tdm"}, // Unused
{"tf", "Number of SC\'s destroyed FCs"},
{"ac", "Remaining ATK points"},
{"php", "Unknown: php"}, // Unused
{"dc", "Unknown: dc"}, // Unused
{"cs", "Unknown: cs"}, // Unused
{"a", "Unknown: a"}, // Unused
{"kap", "Action cards AP"},
{"ktp", "Action cards TP"},
{"dn", "Unknown: dn"}, // Unused
{"hf", "Unknown: hf"}, // Unused
{"df", "Number of destroyed ally FCs (including SC\'s own)"},
{"ff", "Number of ally FCs (including SC\'s own)"},
{"ef", "Number of enemy FCs"},
{"bi", "Number of Native FCs on either team"},
{"ab", "Number of A.Beast FCs on either team"},
{"mc", "Number of Machine FCs on either team"},
{"dk", "Number of Dark FCs on either team"},
{"sa", "Number of Sword-type items on either team"},
{"gn", "Number of Gun-type items on either team"},
{"wd", "Number of Cane-type items on either team"},
{"tt", "Unknown: tt"}, // Unused
{"lv", "Dice bonus"},
{"adm", "Attack damage"},
{"ddm", "Defending damage"},
{"sat", "Number of Sword-type items on SC\'s team"},
{"edm", "Defending damage"}, // TODO: How is this different from ddm?
{"ldm", "Unknown: ldm"}, // Unused
{"rdm", "Defending damage"}, // TODO: How is this different from ddm/edm?
{"fdm", "Final damage (after defense)"},
{"ndm", "Unknown: ndm"}, // Unused
{"ehp", "Attacker HP"},
});
// Arguments are encoded as 3-character null-terminated strings (why?!), and are
// used for adding conditions to effects (e.g. making them only trigger in
// certain situations) or otherwise customizing their results.
// Argument meanings:
// a01 = ???
// cXY/CXY = linked items (require item with cYX/CYX to be equipped as well)
// dXY = roll one die; require result between X and Y inclusive
// e00 = effect lasts while equipped? (in contrast to tXX)
// hXX = require HP >= XX
// iXX = require HP <= XX
// nXX = require condition XX (see description_for_n_condition)
// oXX = seems to be "require previous random-condition effect to have happened"
// TODO: this is used as both o01 (recovery) and o11 (reflection)
// conditions - why the difference?
// pXX = who to target (see description_for_p_target)
// rXX = randomly pass with XX% chance (if XX == 00, 100% chance?)
// sXY = require card cost between X and Y ATK points (inclusive)
// tXX = lasts XX turns, or activate after XX turns
static const vector<const char*> description_for_n_condition({
/* n00 */ "Always true",
/* n01 */ "??? (TODO)",
/* n02 */ "Destroyed with a single attack?",
/* n03 */ "Unknown", // Unused
/* n04 */ "Attack has Pierce",
/* n05 */ "Attack has Rampage",
/* n06 */ "Native attribute",
/* n07 */ "A.Beast attribute",
/* n08 */ "Machine attribute",
/* n09 */ "Dark attribute",
/* n10 */ "Sword-type item",
/* n11 */ "Gun-type item",
/* n12 */ "Cane-type item",
/* n13 */ "Guard item",
/* n14 */ "Story Character",
/* n15 */ "Attacker does not use action cards",
/* n16 */ "Aerial attribute",
/* n17 */ "Same AP as opponent",
/* n18 */ "Opponent is SC",
/* n19 */ "Has Paralyzed condition",
/* n20 */ "Has Frozen condition",
});
static const vector<const char*> description_for_p_target({
/* p00 */ "Unknown: p00", // Unused; probably invalid
/* p01 */ "SC / FC who set the card",
/* p02 */ "Attacking SC / FC",
/* p03 */ "Unknown: p03", // Unused
/* p04 */ "Unknown: p04", // Unused
/* p05 */ "Unknown: p05", // Unused
/* p06 */ "??? (TODO)",
/* p07 */ "??? (TODO; Weakness)",
/* p08 */ "FC's owner SC",
/* p09 */ "Unknown: p09", // Unused
/* p10 */ "All ally FCs",
/* p11 */ "All ally FCs", // TODO: how is this different from p10?
/* p12 */ "All non-aerial FCs on both teams",
/* p13 */ "All FCs on both teams that are Frozen",
/* p14 */ "All FCs on both teams that have <= 3 HP",
/* p15 */ "All FCs except SCs", // TODO: used during attacks only?
/* p16 */ "All FCs except SCs", // TODO: used during attacks only? how is this different from p15?
/* p17 */ "This card",
/* p18 */ "SC who equipped this card",
/* p19 */ "Unknown: p19", // Unused
/* p20 */ "Unknown: p20", // Unused
/* p21 */ "Unknown: p21", // Unused
/* p22 */ "All characters (SCs & FCs) including this card", // TODO: But why does Shifta apply only to allies then?
/* p23 */ "All characters (SCs & FCs) except this card",
/* p24 */ "All FCs on both teams that have Paralysis",
/* p25 */ "Unknown: p25", // Unused
/* p26 */ "Unknown: p26", // Unused
/* p27 */ "Unknown: p27", // Unused
/* p28 */ "Unknown: p28", // Unused
/* p29 */ "Unknown: p29", // Unused
/* p30 */ "Unknown: p30", // Unused
/* p31 */ "Unknown: p31", // Unused
/* p32 */ "Unknown: p32", // Unused
/* p33 */ "Unknown: p33", // Unused
/* p34 */ "Unknown: p34", // Unused
/* p35 */ "All characters (SCs & FCs) within range", // Used for Explosion effect
/* p36 */ "All ally SCs within range, but not the caster", // Resta
/* p37 */ "All FCs or all opponent FCs (TODO)", // TODO: when to use which selector? is a3 involved here somehow?
/* p38 */ "All allies except items within range (and not this card)",
/* p39 */ "All FCs that cost 4 or more points",
/* p40 */ "All FCs that cost 3 or fewer points",
/* p41 */ "Unknown: p41", // Unused
/* p42 */ "Attacker during defense phase", // Only used by TP Defense
/* p43 */ "Owner SC of defending FC during attack",
/* p44 */ "SC\'s own creature FCs within range",
/* p45 */ "Both attacker and defender", // Used for Snatch, which moves EXP from one to the other
/* p46 */ "All SCs & FCs one space left or right of this card",
/* p47 */ "FC\'s owner Boss SC", // Only used for Gibbles+ which explicitly mentions Boss SC, so it looks like this is p08 but for bosses
/* p48 */ "Everything within range, including this card\'s user", // Madness
/* p49 */ "All ally FCs within range except this card",
});
struct Ep3AbilityDescription {
uint8_t command;
bool has_expr;
const char* name;
const char* description;
};
static const std::vector<Ep3AbilityDescription> name_for_effect_command({
{0x00, false, nullptr, nullptr},
{0x01, true, "AP Boost", "Temporarily increase AP by N"},
{0x02, false, "Rampage", "Rampage"},
{0x03, true, "Multi Strike", "Duplicate attack N times"},
{0x04, true, "Damage Modifier 1", "Set attack damage / AP to N after action cards applied (step 1)"},
{0x05, false, "Immobile", "Give Immobile condition"},
{0x06, false, "Hold", "Give Hold condition"},
{0x07, false, nullptr, nullptr},
{0x08, true, "TP Boost", "Add N TP temporarily during attack"},
{0x09, true, "Give Damage", "Cause direct N HP loss"},
{0x0A, false, "Guom", "Give Guom condition"},
{0x0B, false, "Paralyze", "Give Paralysis condition"},
{0x0C, false, nullptr, nullptr},
{0x0D, false, "A/H Swap", "Swap AP and HP temporarily"},
{0x0E, false, "Pierce", "Attack SC directly even if they have items equipped"},
{0x0F, false, nullptr, nullptr},
{0x10, true, "Heal", "Increase HP by N"},
{0x11, false, "Return to Hand", "Return card to hand"},
{0x12, false, nullptr, nullptr},
{0x13, false, nullptr, nullptr},
{0x14, false, "Acid", "Give Acid condition"},
{0x15, false, nullptr, nullptr},
{0x16, true, "Mighty Knuckle", "Temporarily increase AP by N, and set ATK dice to zero"},
{0x17, true, "Unit Blow", "Temporarily increase AP by N * number of this card set within phase"},
{0x18, false, "Curse", "Give Curse condition"},
{0x19, false, "Combo (AP)", "Temporarily increase AP by number of this card set within phase"},
{0x1A, false, "Pierce/Rampage Block", "Block attack if Pierce/Rampage (?)"},
{0x1B, false, "Ability Trap", "Temporarily disable opponent abilities"},
{0x1C, false, "Freeze", "Give Freeze condition"},
{0x1D, false, "Anti-Abnormality", "Cure all conditions"},
{0x1E, false, nullptr, nullptr},
{0x1F, false, "Explosion", "Damage all SCs and FCs by number of this same card set * 2"},
{0x20, false, nullptr, nullptr},
{0x21, false, nullptr, nullptr},
{0x22, false, nullptr, nullptr},
{0x23, false, "Return to Deck", "Cancel discard and move to bottom of deck instead"},
{0x24, false, "Aerial", "Give Aerial status"},
{0x25, true, "AP Loss", "Make attacker temporarily lose N AP during defense"},
{0x26, true, "Bonus From Leader", "Gain AP equal to the number of cards of type N on the field"},
{0x27, false, "Free Maneuver", "Enable movement over occupied tiles"},
{0x28, false, "Haste", "Make move actions free"},
{0x29, true, "Clone", "Make setting this card free if at least one card of type N is already on the field"},
{0x2A, true, "DEF Disable by Cost", "Disable use of any defense cards costing between (N / 10) and (N % 10) points, inclusive"},
{0x2B, true, "Filial", "Increase controlling SC\'s HP by N when this card is destroyed"},
{0x2C, true, "Snatch", "Steal N EXP during attack"},
{0x2D, true, "Hand Disrupter", "DIscard N cards from hand immediately"},
{0x2E, false, "Drop", "Give Drop condition"},
{0x2F, false, "Action Disrupter", "Destroy all action cards used by attacker"},
{0x30, true, "Set HP", "Set HP to N (?) (TODO)"},
{0x31, false, "Native Shield", "Block attacks from Native creatures"},
{0x32, false, "A.Beast Shield", "Block attacks from A.Beast creatures"},
{0x33, false, "Machine Shield", "Block attacks from Machine creatures"},
{0x34, false, "Dark Shield", "Block attacks from Dark creatures"},
{0x35, false, "Sword Shield", "Block attacks from Sword items"},
{0x36, false, "Gun Shield", "Block attacks from Gun items"},
{0x37, false, "Cane Shield", "Block attacks from Cane items"},
{0x38, false, nullptr, nullptr},
{0x39, false, nullptr, nullptr},
{0x3A, false, "Defender", "Make attacks go to setter of this card instead of original target"},
{0x3B, false, "Survival Decoys", "Redirect damage for multi-sided attack"},
{0x3C, true, "Give/Take EXP", "Give N EXP, or take if N is negative"},
{0x3D, false, nullptr, nullptr},
{0x3E, false, "Death Companion", "If this card has 1 or 2 HP, set its HP to N"},
{0x3F, true, "EXP Decoy", "If defender has EXP, lose EXP instead of getting damage when attacked"},
{0x40, true, "Set MV", "Set MV to N"},
{0x41, true, "Group", "Temporarily increase AP by N * number of this card on field, excluding itself"},
{0x42, false, "Berserk", "User of this card receives the same damage as target, and isn't helped by target's defense cards"},
{0x43, false, "Guard Creature", "Attacks on controlling SC damage this card instead"},
{0x44, false, "Tech", "Technique cards cost 1 fewer ATK point"},
{0x45, false, "Big Swing", "Increase all attacking ATK costs by 1"},
{0x46, false, nullptr, nullptr},
{0x47, false, "Shield Weapon", "Limit attacker\'s choice of target to guard items"},
{0x48, false, "ATK Dice Boost", "Increase ATK dice roll by 1"},
{0x49, false, nullptr, nullptr},
{0x4A, false, "Major Pierce", "If SC has over half of max HP, attacks target SC instead of equipped items"},
{0x4B, false, "Heavy Pierce", "If SC has 3 or more items equipped, attacks target SC instead of equipped items"},
{0x4C, false, "Major Rampage", "If SC has over half of max HP, attacks target SC and all equipped items"},
{0x4D, false, "Heavy Rampage", "If SC has 3 or more items equipped, attacks target SC and all equipped items"},
{0x4E, true, "AP Growth", "Permanently increase AP by N"},
{0x4F, true, "TP Growth", "Permanently increase TP by N"},
{0x50, true, "Reborn", "If any card of type N is on the field, this card goes to the hand when destroyed instead of being discarded"},
{0x51, true, "Copy", "Temporarily set AP/TP to N percent (or 100% if N is 0) of opponent\'s values"},
{0x52, false, nullptr, nullptr},
{0x53, true, "Misc. Guards", "Add N to card's defense value"},
{0x54, true, "AP Override", "Set AP to N temporarily"},
{0x55, true, "TP Override", "Set TP to N temporarily"},
{0x56, false, "Return", "Return card to hand on destruction instead of discarding"},
{0x57, false, "A/T Swap Perm", "Permanently swap AP and TP"},
{0x58, false, "A/H Swap Perm", "Permanently swap AP and HP"},
{0x59, true, "Slayers/Assassins", "Temporarily increase AP during attack"},
{0x5A, false, "Anti-Abnormality", "Remove all conditions"},
{0x5B, false, "Fixed Range", "Use SC\'s range instead of weapon or attack card ranges"},
{0x5C, false, "Elude", "SC does not lose HP when equipped items are destroyed"},
{0x5D, false, "Parry", "Forward attack to a random FC within one tile of original target, excluding attacker and original target"},
{0x5E, false, "Block Attack", "Completely block attack"},
{0x5F, false, nullptr, nullptr},
{0x60, false, nullptr, nullptr},
{0x61, true, "Combo (TP)", "Gain TP equal to the number of cards of type N on the field"},
{0x62, true, "Misc. AP Bonuses", "Temporarily increase AP by N"},
{0x63, true, "Misc. TP Bonuses", "Temporarily increase TP by N"},
{0x64, false, nullptr, nullptr},
{0x65, true, "Misc. Defense Bonuses", "Decrease damage by N"},
{0x66, true, "Mostly Halfguards", "Reduce damage from incoming attack by N"},
{0x67, false, "Periodic Field", "Swap immunity to tech or physical attacks"},
{0x68, false, "Unlimited Summoning", "Allow unlimited summoning"},
{0x69, false, nullptr, nullptr},
{0x6A, true, "MV Bonus", "Increase MV by N"},
{0x6B, true, "Forward Damage", "Give N damage back to attacker during defense (?) (TODO)"},
{0x6C, true, "Weak Spot / Influence", "Temporarily decrease AP by N"},
{0x6D, true, "Damage Modifier 2", "Set attack damage / AP after action cards applied (step 2)"},
{0x6E, true, "Weak Hit Block", "Block all attacks of N damage or less"},
{0x6F, true, "AP Silence", "Temporarily decrease AP of opponent by N"},
{0x70, true, "TP Silence", "Temporarily decrease TP of opponent by N"},
{0x71, false, "A/T Swap", "Temporarily swap AP and TP"},
{0x72, true, "Halfguard", "Halve damage from attacks that would inflict N or more damage"},
{0x73, false, nullptr, nullptr},
{0x74, true, "Rampage AP Loss", "Temporarily reduce AP by N"},
{0x75, false, nullptr, nullptr},
{0x76, false, "Reflect", "Generate reverse attack"},
});
void Ep3CardStats::Stat::decode_code() {
this->type = static_cast<Type>(this->code / 1000);
int16_t value = this->code - (this->type * 1000);
if (value != 999) {
switch (this->type) {
case Type::BLANK:
this->stat = 0;
break;
case Type::STAT:
case Type::PLUS_STAT:
case Type::EQUALS_STAT:
this->stat = value;
break;
case Type::MINUS_STAT:
this->stat = -value;
break;
default:
throw runtime_error("invalid card stat type");
}
} else {
this->stat = 0;
this->type = static_cast<Type>(this->type + 4);
}
}
string Ep3CardStats::Stat::str() const {
switch (this->type) {
case Type::BLANK:
return "";
case Type::STAT:
return string_printf("%hhd", this->stat);
case Type::PLUS_STAT:
return string_printf("+%hhd", this->stat);
case Type::MINUS_STAT:
return string_printf("-%d", -this->stat);
case Type::EQUALS_STAT:
return string_printf("=%hhd", this->stat);
case Type::UNKNOWN:
return "?";
case Type::PLUS_UNKNOWN:
return "+?";
case Type::MINUS_UNKNOWN:
return "-?";
case Type::EQUALS_UNKNOWN:
return "=?";
default:
return string_printf("[%02hhX %02hhX]", this->type, this->stat);
}
}
bool Ep3CardStats::Effect::is_empty() const {
return (this->command == 0 &&
this->expr.is_filled_with(0) &&
this->when == 0 &&
this->arg1.is_filled_with(0) &&
this->arg2.is_filled_with(0) &&
this->arg3.is_filled_with(0) &&
this->unknown_a3.is_filled_with(0));
}
string Ep3CardStats::Effect::str_for_arg(const std::string& arg) {
if (arg.empty()) {
return arg;
}
if (arg.size() != 3) {
return arg + "/(invalid)";
}
size_t value;
try {
value = stoul(arg.c_str() + 1, nullptr, 10);
} catch (const invalid_argument&) {
return arg + "/(invalid)";
}
switch (arg[0]) {
case 'a':
return arg + "/(unknown)";
case 'C':
case 'c':
return string_printf("%s/Req. linked item (%zu=>%zu)", arg.c_str(), value / 10, value % 10);
case 'd':
return string_printf("%s/Req. die roll in [%zu, %zu]", arg.c_str(), value / 10, value % 10);
case 'e':
return arg + "/While equipped";
case 'h':
return string_printf("%s/Req. HP >= %zu", arg.c_str(), value);
case 'i':
return string_printf("%s/Req. HP <= %zu", arg.c_str(), value);
case 'n':
try {
return string_printf("%s/Req. condition: %s", arg.c_str(), description_for_n_condition.at(value));
} catch (const out_of_range&) {
return arg + "/(unknown)";
}
case 'o':
return arg + "/Req. prev effect conditions passed";
case 'p':
try {
return string_printf("%s/Target: %s", arg.c_str(), description_for_p_target.at(value));
} catch (const out_of_range&) {
return arg + "/(unknown)";
}
case 'r':
return string_printf("%s/Req. random with %zu%% chance", arg.c_str(), value == 0 ? 100 : value);
case 's':
return string_printf("%s/Req. cost in [%zu, %zu]", arg.c_str(), value / 10, value % 10);
case 't':
return string_printf("%s/Turns: %zu", arg.c_str(), value);
default:
return arg + "/(unknown)";
}
}
string Ep3CardStats::Effect::str() const {
string cmd_str = string_printf("%02hhX", this->command);
try {
const char* name = name_for_effect_command.at(this->command).name;
if (name) {
cmd_str += ':';
cmd_str += name;
}
} catch (const out_of_range&) { }
string when_str = string_printf("%02hhX", this->when);
try {
const char* name = description_for_when.at(this->when);
if (name) {
when_str += ':';
when_str += name;
}
} catch (const out_of_range&) { }
string expr_str = this->expr;
if (!expr_str.empty()) {
expr_str = ", expr=" + expr_str;
}
string arg1str = this->str_for_arg(this->arg1);
string arg2str = this->str_for_arg(this->arg2);
string arg3str = this->str_for_arg(this->arg3);
string a3str = format_data_string(this->unknown_a3.data(), sizeof(this->unknown_a3));
return string_printf("(cmd=%s%s, when=%s, arg1=%s, arg2=%s, arg3=%s, a3=%s)",
cmd_str.c_str(), expr_str.c_str(), when_str.c_str(), arg1str.data(),
arg2str.data(), arg3str.data(), a3str.c_str());
}
void Ep3CardStats::decode_range() {
// If the cell representing the FC is nonzero, the card has a range from a
// list of constants. Otherwise, its range is already defined in the range
// array and should be left alone.
uint8_t index = (this->range[4] >> 8) & 0xF;
if (index != 0) {
this->range.clear(0);
switch (index) {
case 1: // Single cell in front of FC
this->range[3] = 0x00000100;
break;
case 2: // Cell in front of FC and the front-left and front-right (Slash)
this->range[3] = 0x00001110;
break;
case 3: // 3 cells in a line in front of FC
this->range[1] = 0x00000100;
this->range[2] = 0x00000100;
this->range[3] = 0x00000100;
break;
case 4: // All 8 cells around FC
this->range[3] = 0x00001110;
this->range[4] = 0x00001010;
this->range[5] = 0x00001110;
break;
case 5: // 2 cells in a line in front of FC
this->range[2] = 0x00000100;
this->range[3] = 0x00000100;
break;
case 6: // Entire field (renders as "A")
for (size_t x = 0; x < 6; x++) {
this->range[x] = 0x000FFFFF;
}
break;
case 7: // Superposition of 4 and 5 (unused)
this->range[2] = 0x00000100;
this->range[3] = 0x00001110;
this->range[4] = 0x00001010;
this->range[5] = 0x00001110;
break;
case 8: // All 8 cells around FC and FC's cell
this->range[3] = 0x00001110;
this->range[4] = 0x00001110;
this->range[5] = 0x00001110;
break;
case 9: // No cells
break;
// The table in the DOL file only appears to contain 9 entries; there are
// some pointers immediately after. So probably if a card specified A-F,
// its range would be filled in with garbage in the original game.
default:
throw runtime_error("invalid fixed range index");
}
}
}
string name_for_rarity(uint8_t rarity) {
static const vector<const char*> names({
"N1",
"R1",
"S",
"E",
"N2",
"N3",
"N4",
"R2",
"R3",
"R4",
"SS",
"D1",
"D2",
"INVIS",
});
try {
return names.at(rarity - 1);
} catch (const out_of_range&) {
return string_printf("(%02hhX)", rarity);
}
}
string name_for_target_mode(uint8_t target_mode) {
static const vector<const char*> names({
"NONE",
"SINGLE",
"MULTI",
"SELF",
"TEAM",
"ALL",
"MULTI-ALLY",
"ALL-ALLY",
"ALL-ATTACK",
"OWN-FCS",
});
try {
return names.at(target_mode);
} catch (const out_of_range&) {
return string_printf("(%02hhX)", target_mode);
}
}
string string_for_colors(const parray<uint8_t, 8>& colors) {
string ret;
for (size_t x = 0; x < 8; x++) {
if (colors[x]) {
ret += '0' + colors[x];
}
}
if (ret.empty()) {
return "none";
}
return ret;
}
string string_for_assist_turns(uint8_t turns) {
if (turns == 90) {
return "ONCE";
} else if (turns == 99) {
return "FOREVER";
} else {
return string_printf("%hhu", turns);
}
}
string string_for_range(const parray<be_uint32_t, 6>& range) {
string ret;
for (size_t x = 0; x < 6; x++) {
ret += string_printf("%05" PRIX32 "/", range[x].load());
}
while (starts_with(ret, "00000/")) {
ret = ret.substr(6);
}
if (!ret.empty()) {
ret.resize(ret.size() - 1);
}
return ret;
}
string Ep3CardStats::str() const {
string type_str;
try {
type_str = name_for_card_type.at(this->type);
} catch (const out_of_range&) {
type_str = string_printf("%02hhX", this->type);
}
string rarity_str = name_for_rarity(this->rarity);
string target_mode_str = name_for_target_mode(this->target_mode);
string range_str = string_for_range(this->range);
string assist_turns_str = string_for_assist_turns(this->assist_turns);
string hp_str = this->hp.str();
string ap_str = this->ap.str();
string tp_str = this->tp.str();
string mv_str = this->mv.str();
string left_str = string_for_colors(this->left_colors);
string right_str = string_for_colors(this->right_colors);
string top_str = string_for_colors(this->top_colors);
string effects_str;
for (size_t x = 0; x < 3; x++) {
if (this->effects[x].is_empty()) {
continue;
}
if (!effects_str.empty()) {
effects_str += ", ";
}
effects_str += this->effects[x].str();
}
return string_printf(
"[Card: %04" PRIX32 " name=%s type=%s-%02hhX rare=%s cost=%hhX+%hhX "
"target=%s range=%s assist_turns=%s cannot_move=%s cannot_attack=%s "
"hidden=%s hp=%s ap=%s tp=%s mv=%s left=%s right=%s top=%s a2=%08" PRIX32 " "
"assist_effect=[%hu, %hu] a3=[%hu, %hu] has_effects=%s effects=[%s]]",
this->card_id.load(),
this->name.data(),
type_str.c_str(),
this->subtype,
rarity_str.c_str(),
this->self_cost,
this->ally_cost,
target_mode_str.c_str(),
range_str.c_str(),
assist_turns_str.c_str(),
this->cannot_move ? "true" : "false",
this->cannot_attack ? "true" : "false",
this->hide_in_deck_edit ? "true" : "false",
hp_str.c_str(),
ap_str.c_str(),
tp_str.c_str(),
mv_str.c_str(),
left_str.c_str(),
right_str.c_str(),
top_str.c_str(),
this->unknown_a2.load(),
this->assist_effect[0].load(),
this->assist_effect[1].load(),
this->unknown_a3[0].load(),
this->unknown_a3[1].load(),
this->has_effects ? "true" : "false",
effects_str.c_str());
}
Ep3DataIndex::Ep3DataIndex(const string& directory) {
static constexpr bool debug_enabled = false;
unordered_map<uint32_t, vector<string>> card_tags;
if (debug_enabled) {
unordered_map<uint32_t, string> card_text;
try {
string data = prs_decompress(load_file(directory + "/cardtext.mnr"));
StringReader r(data);
while (!r.eof()) {
uint32_t card_id = stoul(r.get_cstr());
// Most cards have multiple pages, but we only care about the first page
// (for now)
string text = r.get_cstr();
// Preprocess text: first, delete all color markers
size_t offset = text.find("\tC");
while (offset != string::npos) {
text = text.substr(0, offset) + text.substr(offset + 3);
offset = text.find("\tC");
}
// Preprocess text: delete all initial lines that don't start with \t
offset = text.find('\t');
if (offset == string::npos) {
text.clear();
} else {
text = text.substr(offset);
}
// Preprocess text: merge lines that don't begin with \t
for (offset = 0; offset < text.size(); offset++) {
if (text[offset] == '\n' && text[offset + 1] != '\t') {
text = text.substr(0, offset) + text.substr(offset + 1);
offset--;
}
}
// Split text into tags
vector<string> tags;
auto lines = split(text, '\n');
for (const auto& line : lines) {
if (line[0] == '\t' && line[1] == 'D') {
tags.emplace_back("D: " + line.substr(2));
} else if (line[0] == '\t' && line[1] == 'S') {
tags.emplace_back("S: " + line.substr(2));
}
}
if (!card_text.emplace(card_id, move(text)).second) {
throw runtime_error("duplicate card text id");
}
if (!card_tags.emplace(card_id, move(tags)).second) {
throw logic_error("duplicate card tags id");
}
r.go((r.where() + 0x3FF) & (~0x3FF));
}
} catch (const exception& e) {
log(WARNING, "Failed to load card text: %s", e.what());
}
}
try {
this->compressed_card_definitions = load_file(directory + "/cardupdate.mnr");
string data = prs_decompress(this->compressed_card_definitions);
// There's a footer after the card definitions, but we ignore it
if (data.size() % sizeof(Ep3CardStats) != sizeof(Ep3CardStatsFooter)) {
throw runtime_error(string_printf(
"decompressed card update file size %zX is not aligned with card definition size %zX (%zX extra bytes)",
data.size(), sizeof(Ep3CardStats), data.size() % sizeof(Ep3CardStats)));
}
const auto* stats = reinterpret_cast<const Ep3CardStats*>(data.data());
size_t max_cards = data.size() / sizeof(Ep3CardStats);
for (size_t x = 0; x < max_cards; x++) {
// The last card entry has the build date and some other metadata (and
// isn't a real card, obviously), so skip it. Seems like the card ID is
// always a large number that won't fit in a uint16_t, so we use that to
// determine if the entry is a real card or not.
if (stats[x].card_id & 0xFFFF0000) {
continue;
}
shared_ptr<CardEntry> entry(new CardEntry({stats[x], {}}));
if (!this->card_definitions.emplace(entry->stats.card_id, entry).second) {
throw runtime_error(string_printf(
"duplicate card id: %08" PRIX32, entry->stats.card_id.load()));
}
entry->stats.hp.decode_code();
entry->stats.ap.decode_code();
entry->stats.tp.decode_code();
entry->stats.mv.decode_code();
entry->stats.decode_range();
if (debug_enabled) {
string card_str = entry->stats.str();
try {
string tags_str = join(card_tags.at(stats[x].card_id), ", ");
fprintf(stderr, "%s tags: [%s]\n", card_str.c_str(), tags_str.c_str());
} catch (const out_of_range&) {
fprintf(stderr, "%s\n", card_str.c_str());
}
}
}
log(INFO, "Indexed %zu Episode 3 card definitions", this->card_definitions.size());
} catch (const exception& e) {
log(WARNING, "Failed to load Episode 3 card update: %s", e.what());
}
for (const auto& filename : list_directory(directory)) {
try {
shared_ptr<MapEntry> entry;
if (ends_with(filename, ".mnmd")) {
entry.reset(new MapEntry(load_object_file<Ep3Map>(directory + "/" + filename)));
} else if (ends_with(filename, ".mnm")) {
entry.reset(new MapEntry(load_file(directory + "/" + filename)));
}
if (entry.get()) {
if (!this->maps.emplace(entry->map.map_number, entry).second) {
throw runtime_error("duplicate map number");
}
string name = entry->map.name;
log(INFO, "Indexed Episode 3 map %s (%08" PRIX32 "; %s)",
filename.c_str(), entry->map.map_number.load(), name.c_str());
}
} catch (const exception& e) {
log(WARNING, "Failed to index Episode 3 map %s: %s",
filename.c_str(), e.what());
}
}
}
Ep3DataIndex::MapEntry::MapEntry(const Ep3Map& map) : map(map) { }
Ep3DataIndex::MapEntry::MapEntry(const string& compressed)
: compressed_data(compressed) {
string decompressed = prs_decompress(this->compressed_data);
if (decompressed.size() != sizeof(Ep3Map)) {
throw runtime_error(string_printf(
"decompressed data size is incorrect (expected %zu bytes, read %zu bytes)",
sizeof(Ep3Map), decompressed.size()));
}
this->map = *reinterpret_cast<const Ep3Map*>(decompressed.data());
}
string Ep3DataIndex::MapEntry::compressed() const {
if (this->compressed_data.empty()) {
this->compressed_data = prs_compress(&this->map, sizeof(this->map));
}
return this->compressed_data;
}
const string& Ep3DataIndex::get_compressed_card_definitions() const {
if (this->compressed_card_definitions.empty()) {
throw runtime_error("card definitions are not available");
}
return this->compressed_card_definitions;
}
shared_ptr<const Ep3DataIndex::CardEntry> Ep3DataIndex::get_card_definition(
uint32_t id) const {
return this->card_definitions.at(id);
}
const string& Ep3DataIndex::get_compressed_map_list() const {
if (this->compressed_map_list.empty()) {
// TODO: Write a version of prs_compress that takes iovecs (or something
// similar) so we can eliminate all this string copying here.
StringWriter entries_w;
StringWriter strings_w;
for (const auto& map_it : this->maps) {
Ep3MapList::Entry e;
const auto& map = map_it.second->map;
e.map_x = map.map_x;
e.map_y = map.map_y;
e.scene_data2 = map.scene_data2;
e.map_number = map.map_number.load();
e.width = map.width;
e.height = map.height;
e.map_tiles = map.map_tiles;
e.modification_tiles = map.modification_tiles;
e.name_offset = strings_w.size();
strings_w.write(map.name.data(), map.name.len());
strings_w.put_u8(0);
e.location_name_offset = strings_w.size();
strings_w.write(map.location_name.data(), map.location_name.len());
strings_w.put_u8(0);
e.quest_name_offset = strings_w.size();
strings_w.write(map.quest_name.data(), map.quest_name.len());
strings_w.put_u8(0);
e.description_offset = strings_w.size();
strings_w.write(map.description.data(), map.description.len());
strings_w.put_u8(0);
e.unknown_a2 = 0xFF000000;
entries_w.put(e);
}
Ep3MapList header;
header.num_maps = this->maps.size();
header.unknown_a1 = 0;
header.strings_offset = entries_w.size();
header.total_size = sizeof(Ep3MapList) + entries_w.size() + strings_w.size();
StringWriter w;
w.put(header);
w.write(entries_w.str());
w.write(strings_w.str());
StringWriter compressed_w;
compressed_w.put_u32b(w.str().size());
compressed_w.write(prs_compress(w.str()));
this->compressed_map_list = move(compressed_w.str());
log(INFO, "Generated Episode 3 compressed map list (%zu -> %zu bytes)",
w.size(), this->compressed_map_list.size());
}
return this->compressed_map_list;
}
shared_ptr<const Ep3DataIndex::MapEntry> Ep3DataIndex::get_map(uint32_t id) const {
return this->maps.at(id);
}
+351
View File
@@ -0,0 +1,351 @@
#pragma once
#include <stdint.h>
#include <string>
#include <map>
#include <memory>
#include <unordered_map>
#include <phosg/Encoding.hh>
#include "Text.hh"
// Note: Much of the structures and enums here are based on the card list file,
// and comparing the card text with the data in the file. Some inferences may be
// incorrect here, since Episode 3's card text is wrong in various places.
struct Ep3CardStats {
enum Rarity : uint8_t {
N1 = 0x01,
R1 = 0x02,
S = 0x03,
E = 0x04,
N2 = 0x05,
N3 = 0x06,
N4 = 0x07,
R2 = 0x08,
R3 = 0x09,
R4 = 0x0A,
SS = 0x0B,
D1 = 0x0C,
D2 = 0x0D,
INVIS = 0x0E,
};
enum Type : uint8_t {
SC_HUNTERS = 0x00, // No subtypes
SC_ARKZ = 0x01, // No subtypes
ITEM = 0x02, // Subtype 01 = sword, 02 = gun, 03 = cane. TODO: there are many more subtypes than those 3
CREATURE = 0x03, // No subtypes (TODO: Where are attributes stored then?)
ACTION = 0x04, // TODO: What do the subtypes mean? Are they actually flags instead?
ASSIST = 0x05, // No subtypes
};
struct Stat {
enum Type : uint8_t {
BLANK = 0,
STAT = 1,
PLUS_STAT = 2,
MINUS_STAT = 3,
EQUALS_STAT = 4,
UNKNOWN = 5,
PLUS_UNKNOWN = 6,
MINUS_UNKNOWN = 7,
EQUALS_UNKNOWN = 8,
};
be_uint16_t code;
Type type;
int8_t stat;
void decode_code();
std::string str() const;
} __attribute__((packed));
struct Effect {
uint8_t command;
ptext<char, 0x0F> expr; // May be blank if the command doesn't use it
uint8_t when;
ptext<char, 4> arg1;
ptext<char, 4> arg2;
ptext<char, 4> arg3;
parray<uint8_t, 3> unknown_a3;
bool is_empty() const;
static std::string str_for_arg(const std::string& arg);
std::string str() const;
} __attribute__((packed));
be_uint32_t card_id;
parray<uint8_t, 0x40> jp_name;
int8_t type; // Type enum. If <0, then this is the end of the card list
uint8_t self_cost; // ATK dice points required
uint8_t ally_cost; // ATK points from allies required; PBs use this
uint8_t unused_a0; // Always 0
Stat hp;
Stat ap;
Stat tp;
Stat mv;
parray<uint8_t, 8> left_colors;
parray<uint8_t, 8> right_colors;
parray<uint8_t, 8> top_colors;
parray<be_uint32_t, 6> range;
be_uint32_t unused_a1; // Always 0
// Target modes:
// 00 = no targeting (used for defense cards, mags, shields, etc.)
// 01 = single enemy
// 02 = multiple enemies (with range)
// 03 = self (assist)
// 04 = team (assist)
// 05 = everyone (assist)
// 06 = multiple allies (with range); only used by Shifta
// 07 = all allies including yourself; see Anti, Resta, Leilla
// 08 = all (attack); see e.g. Last Judgment, Earthquake
// 09 = your own FCs but not SCs; see Traitor
uint8_t target_mode;
uint8_t assist_turns; // 90 (dec) = once, 99 (dec) = forever
uint8_t cannot_move; // 0 for SC and creature cards; 1 for everything else
uint8_t cannot_attack; // 1 for shields, mags, defense actions, and assist cards
uint8_t unused_a2; // Always 0
uint8_t hide_in_deck_edit; // 0 = player can use this card (appears in deck edit)
uint8_t subtype; // e.g. gun, sword, etc. (used for checking if SCs can use it)
uint8_t rarity; // Rarity enum
be_uint32_t unknown_a2;
// These two fields seem to always contain the same value, and are always 0
// for non-assist cards and nonzero for assists. Each assist card has a unique
// value here and no effects, which makes it look like this is how assist
// effects are implemented. There seems to be some 1k-modulation going on here
// too; most cards are in the range 101-174 but a few have e.g. 1150, 2141. A
// few pairs of cards have the same effect, which makes it look like some
// other fields are also involved in determining their effects (see e.g. Skip
// Draw / Skip Move, Dice Fever / Dice Fever +, Reverse Card / Rich +).
parray<be_uint16_t, 2> assist_effect;
parray<be_uint16_t, 2> unknown_a3;
ptext<char, 0x14> name;
ptext<char, 0x0B> jp_short_name;
ptext<char, 0x07> short_name;
be_uint16_t has_effects; // 1 if any of the following structs are not blank
Effect effects[3];
void decode_range();
std::string str() const;
} __attribute__((packed)); // 0x128 bytes in total
struct Ep3CardStatsFooter {
be_uint32_t num_cards1;
be_uint32_t unknown_a1;
be_uint32_t num_cards2;
be_uint32_t unknown_a2[11];
be_uint32_t unknown_offset_a3;
be_uint32_t unknown_a4[3];
be_uint32_t footer_offset;
be_uint32_t unknown_a5[3];
} __attribute__((packed));
struct Ep3Deck {
ptext<char, 0x10> name;
be_uint32_t client_id; // 0-3
// List of card IDs. The card count is the number of nonzero entries here
// before a zero entry (or 50 if no entries are nonzero). The first card ID is
// the SC card, which the game implicitly subtracts from the limit - so a
// valid deck should actually have 31 cards in it.
parray<le_uint16_t, 50> card_ids;
be_uint32_t unknown_a1;
// Last modification time
le_uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
uint8_t unknown_a2;
} __attribute__((packed)); // 0x84 bytes in total
struct Ep3Config {
// Offsets in comments in this struct are relative to start of 61/98 command
/* 0728 */ parray<uint8_t, 0x1434> unknown_a1;
/* 1B5C */ parray<Ep3Deck, 25> decks;
/* 2840 */ uint64_t unknown_a2;
/* 2848 */ be_uint32_t offline_clv_exp; // CLvOff = this / 100
/* 284C */ be_uint32_t online_clv_exp; // CLvOn = this / 100
/* 2850 */ parray<uint8_t, 0x14C> unknown_a3;
/* 299C */ ptext<char, 0x10> name;
// Other records are probably somewhere in here - e.g. win/loss, play time, etc.
/* 29AC */ parray<uint8_t, 0xCC> unknown_a4;
} __attribute__((packed));
struct Ep3BattleRules {
// When this structure is used in a map/quest definition, FF in any of these
// fields means the user is allowed to override it. Any non-FF fields are
// fixed for the map/quest and cannot be overridden.
uint8_t overall_time_limit; // In increments of 5 minutes; 0 = unlimited
uint8_t phase_time_limit; // In seconds; 0 = unlimited
uint8_t allowed_cards; // 0 = any, 1 = N-rank only, 2 = N and R, 3 = N, R, and S
uint8_t min_dice; // 0 = default (1)
// 4
uint8_t max_dice; // 0 = default (6)
uint8_t disable_deck_shuffle; // 0 = shuffle on, 1 = off
uint8_t disable_deck_loop; // 0 = loop on, 1 = off
uint8_t char_hp;
// 8
uint8_t hp_type; // 0 = defeat player, 1 = defeat team, 2 = common hp
uint8_t no_assist_cards; // 1 = assist cards disallowed
uint8_t disable_dialogue; // 0 = dialogue on, 1 = dialogue off
uint8_t dice_exchange_mode; // 0 = high attack, 1 = high defense, 2 = none
// C
uint8_t disable_dice_boost; // 0 = dice boost on, 1 = off
parray<uint8_t, 3> unused;
} __attribute__((packed));
struct Ep3MapList {
be_uint32_t num_maps;
be_uint32_t unknown_a1; // Always 0?
be_uint32_t strings_offset; // From after total_size field (add 0x10 to this value)
be_uint32_t total_size; // Including header, entries, and strings
struct Entry { // Should be 0x220 bytes in total
// These 3 fields probably include the location ID (scenery to load) and the
// music ID
be_uint16_t map_x;
be_uint16_t map_y;
be_uint16_t scene_data2;
be_uint16_t map_number;
// Text offsets are from the beginning of the strings block after all map
// entries (that is, add strings_offset to them to get the string offset)
be_uint32_t name_offset;
be_uint32_t location_name_offset;
be_uint32_t quest_name_offset;
be_uint32_t description_offset;
be_uint16_t width;
be_uint16_t height;
parray<uint8_t, 0x100> map_tiles;
parray<uint8_t, 0x100> modification_tiles;
be_uint32_t unknown_a2; // Seems to always be 0xFF000000
} __attribute__((packed));
// Variable-length fields:
// Entry entries[num_maps];
// char strings[...EOF]; // Null-terminated strings, pointed to by offsets in Entry structs
} __attribute__((packed));
struct Ep3CompressedMapHeader { // .mnm file format
le_uint32_t map_number;
le_uint32_t compressed_data_size;
// Compressed data immediately follows (which decompresses to an Ep3Map)
} __attribute__((packed));
struct Ep3Map { // .mnmd format
/* 0000 */ be_uint32_t unknown_a1;
/* 0004 */ be_uint32_t map_number;
/* 0008 */ uint8_t width;
/* 0009 */ uint8_t height;
/* 000A */ uint8_t scene_data2; // TODO: What is this?
// All alt_maps fields (including the floats) past num_alt_maps are filled in
// with FF. For example, if num_alt_maps == 8, the last two fields in each
// alt_maps array are filled with FF.
/* 000B */ uint8_t num_alt_maps; // TODO: What are the alt maps for?
// In the map_tiles array, the values are:
// 00 = not a valid tile
// 01 = valid tile unless punched out (later)
// 02 = team A start (1v1)
// 03, 04 = team A start (2v2)
// 05 = ???
// 06, 07 = team B start (2v2)
// 08 = team B start (1v1)
// Note that the game displays the map reversed vertically in the preview
// window. For example, player 1 is on team A, which usually starts at the top
// of the map as defined in this struct, or at the bottom as shown in the
// preview window.
/* 000C */ parray<uint8_t, 0x100> map_tiles;
/* 010C */ parray<uint8_t, 0x0C> unknown_a2;
/* 0118 */ parray<uint8_t, 0x100> alt_maps1[0x0A];
/* 0B18 */ parray<uint8_t, 0x100> alt_maps2[0x0A];
/* 1518 */ parray<be_float, 0x12> alt_maps_unknown_a3[0x0A];
/* 17E8 */ parray<be_float, 0x12> alt_maps_unknown_a4[0x0A];
/* 1AB8 */ parray<be_float, 0x6C> unknown_a5;
// In the modification_tiles array, the values are:
// 10 = blocked (as if the corresponding map_tiles value was 00)
// 20 = blocked (maybe one of 10 or 20 are passable by Aerial characters though)
// 30, 31 = teleporters (green, red)
// 40-44 = ???? (used in 244, 2E4, 2F9)
// 50 = appears as improperly-z-buffered teal cube in preview
// TODO: There may be more values that are valid here.
/* 1C68 */ parray<uint8_t, 0x100> modification_tiles;
/* 1D68 */ parray<uint8_t, 0x74> unknown_a6;
/* 1DDC */ Ep3BattleRules default_rules;
/* 1DEC */ parray<uint8_t, 4> unknown_a7;
/* 1DF0 */ ptext<char, 0x14> name;
/* 1E04 */ ptext<char, 0x14> location_name;
/* 1E18 */ ptext<char, 0x3C> quest_name; // == location_name if not a quest
/* 1E54 */ ptext<char, 0x190> description;
/* 1FE4 */ be_uint16_t map_x;
/* 1FE6 */ be_uint16_t map_y;
struct NPCDeck {
ptext<char, 0x18> name;
parray<be_uint16_t, 0x20> card_ids; // Last one appears to always be FFFF
} __attribute__((packed));
/* 1FE8 */ NPCDeck npc_decks[3]; // Unused if name[0] == 0
struct NPCCharacter {
parray<be_uint16_t, 2> unknown_a1;
parray<uint8_t, 4> unknown_a2;
ptext<char, 0x10> name;
parray<be_uint16_t, 0x7E> unknown_a3;
} __attribute__((packed));
/* 20F0 */ NPCCharacter npc_chars[3]; // Unused if name[0] == 0
/* 242C */ parray<uint8_t, 0x14> unknown_a8; // Always FF?
/* 2440 */ ptext<char, 0x190> before_message;
/* 25D0 */ ptext<char, 0x190> after_message;
/* 2760 */ ptext<char, 0x190> dispatch_message; // Usually "You can only dispatch <character>" or blank
struct DialogueSet {
be_uint16_t unknown_a1;
be_uint16_t unknown_a2; // Always 0x0064 if valid, 0xFFFF if unused?
ptext<char, 0x40> strings[4];
} __attribute__((packed)); // Total size: 0x104 bytes
/* 28F0 */ DialogueSet dialogue_sets[3][0x10]; // Up to 0x10 per valid NPC
/* 59B0 */ be_uint16_t reward_card_id; // TODO: This could be an array. The only examples I've seen have only one here
/* 59B2 */ parray<be_uint16_t, 0x33> unknown_a9;
/* 5A18 */
} __attribute__((packed));
class Ep3DataIndex {
public:
explicit Ep3DataIndex(const std::string& directory);
struct CardEntry {
Ep3CardStats stats;
std::vector<std::string> text;
};
class MapEntry {
public:
Ep3Map map;
MapEntry(const Ep3Map& map);
MapEntry(const std::string& compressed_data);
std::string compressed() const;
private:
mutable std::string compressed_data;
};
const std::string& get_compressed_card_definitions() const;
std::shared_ptr<const CardEntry> get_card_definition(uint32_t id) const;
const std::string& get_compressed_map_list() const;
std::shared_ptr<const MapEntry> get_map(uint32_t id) const;
private:
std::string compressed_card_definitions;
std::unordered_map<uint32_t, std::shared_ptr<CardEntry>> card_definitions;
// The compressed map list is generated on demand from the maps map below.
// It's marked mutable because the logical consistency of the Ep3DataIndex
// object is not violated from the caller's perspective even if we don't
// generate the compressed map list at load time.
mutable std::string compressed_map_list;
std::map<uint32_t, std::shared_ptr<MapEntry>> maps;
};
+12 -3
View File
@@ -12,7 +12,15 @@ FileContentsCache::File::File(const string& name, shared_ptr<const string> conte
uint64_t load_time) : name(name), contents(contents), load_time(load_time) { }
shared_ptr<const string> FileContentsCache::get(const std::string& name) {
return this->get(name, [name]() -> string { return load_file(name); });
}
shared_ptr<const string> FileContentsCache::get(const char* name) {
return this->get(string(name));
}
shared_ptr<const string> FileContentsCache::get(const std::string& name,
std::function<std::string()> generate) {
uint64_t t = now();
try {
auto& entry = this->name_to_file.at(name);
@@ -21,7 +29,7 @@ shared_ptr<const string> FileContentsCache::get(const std::string& name) {
}
} catch (const out_of_range& e) { }
shared_ptr<const string> contents(new string(load_file(name)));
shared_ptr<const string> contents(new string(generate()));
this->name_to_file.erase(name);
this->name_to_file.emplace(piecewise_construct, forward_as_tuple(name),
forward_as_tuple(name, contents, t));
@@ -29,6 +37,7 @@ shared_ptr<const string> FileContentsCache::get(const std::string& name) {
return contents;
}
shared_ptr<const string> FileContentsCache::get(const char* name) {
return this->get(string(name));
shared_ptr<const string> FileContentsCache::get(const char* name,
std::function<std::string()> generate) {
return this->get(string(name), generate);
}
+6
View File
@@ -3,6 +3,7 @@
#include <memory>
#include <string>
#include <unordered_map>
#include <functional>
using namespace std;
@@ -35,6 +36,11 @@ public:
std::shared_ptr<const std::string> get(const std::string& name);
std::shared_ptr<const std::string> get(const char* name);
std::shared_ptr<const std::string> get(
const std::string& name, std::function<std::string()> generate);
std::shared_ptr<const std::string> get(
const char* name, std::function<std::string()> generate);
private:
std::unordered_map<std::string, File> name_to_file;
};
+231
View File
@@ -0,0 +1,231 @@
#include "FunctionCompiler.hh"
#include <stdio.h>
#include <string.h>
#include <stdexcept>
#include <phosg/Filesystem.hh>
#ifdef HAVE_RESOURCE_FILE
#include <resource_file/Emulators/PPC32Emulator.hh>
#endif
#include "CommandFormats.hh"
using namespace std;
bool function_compiler_available() {
#ifndef HAVE_RESOURCE_FILE
return false;
#else
return true;
#endif
}
string CompiledFunctionCode::generate_client_command(
const unordered_map<string, uint32_t>& label_writes,
const string& suffix) const {
S_ExecuteCode_Footer_GC_B2 footer;
footer.num_relocations = this->relocation_deltas.size();
footer.unused1.clear();
footer.entrypoint_addr_offset = this->entrypoint_offset_offset;
footer.unused2.clear();
StringWriter w;
if (!label_writes.empty()) {
string modified_code = this->code;
for (const auto& it : label_writes) {
size_t offset = this->label_offsets.at(it.first);
if (offset > modified_code.size() - 4) {
throw runtime_error("label out of range");
}
*reinterpret_cast<be_uint32_t*>(modified_code.data() + offset) = it.second;
}
w.write(modified_code);
} else {
w.write(this->code);
}
w.write(suffix);
while (w.size() & 3) {
w.put_u8(0);
}
footer.relocations_offset = w.size();
for (uint16_t delta : this->relocation_deltas) {
w.put_u16b(delta);
}
if (this->relocation_deltas.size() & 1) {
w.put_u16(0);
}
w.put(footer);
return move(w.str());
}
shared_ptr<CompiledFunctionCode> compile_function_code(
const string& directory, const string& name, const string& text) {
#ifndef HAVE_RESOURCE_FILE
(void)directory;
(void)name;
(void)text;
throw runtime_error("PowerPC assembler is not available");
#else
std::unordered_set<string> get_include_stack; // For mutual recursion detection
function<string(const string&)> get_include = [&](const string& name) -> string {
if (!get_include_stack.emplace(name).second) {
throw runtime_error("mutual recursion between includes");
}
string filename = directory + "/" + name + ".inc.s";
if (isfile(filename)) {
return PPC32Emulator::assemble(load_file(filename), get_include).code;
}
filename = directory + "/" + name + ".inc.bin";
if (isfile(filename)) {
return load_file(filename);
}
throw runtime_error("data not found for include " + name);
};
shared_ptr<CompiledFunctionCode> ret(new CompiledFunctionCode());
ret->name = name;
ret->index = 0;
auto assembled = PPC32Emulator::assemble(text, get_include);
ret->code = move(assembled.code);
ret->label_offsets = move(assembled.label_offsets);
set<uint32_t> reloc_indexes;
for (const auto& it : ret->label_offsets) {
if (starts_with(it.first, "reloc")) {
reloc_indexes.emplace(it.second / 4);
} else if (starts_with(it.first, "newserv_index_")) {
ret->index = stoul(it.first.substr(14), nullptr, 16);
}
}
try {
ret->entrypoint_offset_offset = ret->label_offsets.at("entry_ptr");
} catch (const out_of_range&) {
throw runtime_error("code does not contain entry_ptr label");
}
uint32_t prev_index = 0;
for (const auto& it : reloc_indexes) {
uint32_t delta = it - prev_index;
if (delta > 0xFFFF) {
throw runtime_error("relocation delta too far away");
}
ret->relocation_deltas.emplace_back(delta);
prev_index = it;
}
return ret;
#endif
}
FunctionCodeIndex::FunctionCodeIndex(const string& directory) {
if (!function_compiler_available()) {
log(INFO, "Function compiler is not available");
return;
}
uint32_t next_menu_item_id = 0;
for (const auto& filename : list_directory(directory)) {
if (!ends_with(filename, ".s") || ends_with(filename, ".inc.s")) {
continue;
}
bool is_patch = ends_with(filename, ".patch.s");
string name = filename.substr(0, filename.size() - (is_patch ? 8 : 2));
try {
string path = directory + "/" + filename;
string text = load_file(path);
auto code = compile_function_code(directory, name, text);
if (code->index != 0) {
if (!this->index_to_function.emplace(code->index, code).second) {
throw runtime_error(string_printf(
"duplicate function index: %08" PRIX32, code->index));
}
}
this->name_to_function.emplace(name, code);
if (is_patch) {
this->menu_item_id_to_patch_function.emplace(next_menu_item_id++, code);
this->name_to_patch_function.emplace(name, code);
}
if (code->index) {
log(INFO, "Compiled function %02X => %s", code->index, name.c_str());
} else {
log(INFO, "Compiled function %s", name.c_str());
}
} catch (const exception& e) {
log(WARNING, "Failed to compile function %s: %s", name.c_str(), e.what());
}
}
}
vector<MenuItem> FunctionCodeIndex::patch_menu() const {
vector<MenuItem> ret;
ret.emplace_back(PatchesMenuItemID::GO_BACK, u"Go back", u"", 0);
for (const auto& it : this->name_to_patch_function) {
const auto& fn = it.second;
ret.emplace_back(fn->menu_item_id, decode_sjis(fn->name), u"",
MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL);
}
return ret;
}
DOLFileIndex::DOLFileIndex(const string& directory) {
if (!function_compiler_available()) {
log(INFO, "Function compiler is not available");
return;
}
if (!isdir(directory)) {
log(INFO, "DOL file directory is missing");
return;
}
uint32_t next_menu_item_id = 0;
for (const auto& filename : list_directory(directory)) {
if (!ends_with(filename, ".dol")) {
continue;
}
string name = filename.substr(0, filename.size() - 4);
try {
shared_ptr<DOLFile> dol(new DOLFile());
dol->menu_item_id = next_menu_item_id++;
dol->name = name;
string path = directory + "/" + filename;
dol->data = load_file(path);
this->name_to_file.emplace(dol->name, dol);
this->item_id_to_file.emplace_back(dol);
log(INFO, "Loaded DOL file %s", filename.c_str());
} catch (const exception& e) {
log(WARNING, "Failed to load DOL file %s: %s", filename.c_str(), e.what());
}
}
}
vector<MenuItem> DOLFileIndex::menu() const {
vector<MenuItem> ret;
ret.emplace_back(ProgramsMenuItemID::GO_BACK, u"Go back", u"", 0);
for (const auto& dol : this->item_id_to_file) {
ret.emplace_back(dol->menu_item_id, decode_sjis(dol->name), u"",
MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL);
}
return ret;
}
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include <inttypes.h>
#include <string>
#include <unordered_map>
#include <map>
#include <vector>
#include <memory>
#include "Menu.hh"
bool function_compiler_available();
// TODO: Support x86 function calls in the future. Currently we only support
// PPC32 because I haven't written an appropriate x86 assembler yet.
struct CompiledFunctionCode {
std::string code;
std::vector<uint16_t> relocation_deltas;
std::unordered_map<std::string, uint32_t> label_offsets;
uint32_t entrypoint_offset_offset;
std::string name;
uint32_t index; // 0 = unused (not registered in index_to_function)
uint32_t menu_item_id;
std::string generate_client_command(
const std::unordered_map<std::string, uint32_t>& label_writes = {},
const std::string& suffix = "") const;
};
std::shared_ptr<CompiledFunctionCode> compile_function_code(
const std::string& directory,
const std::string& name,
const std::string& text);
struct FunctionCodeIndex {
FunctionCodeIndex(const std::string& directory);
std::unordered_map<std::string, std::shared_ptr<CompiledFunctionCode>> name_to_function;
std::unordered_map<uint32_t, std::shared_ptr<CompiledFunctionCode>> index_to_function;
std::unordered_map<uint32_t, std::shared_ptr<CompiledFunctionCode>> menu_item_id_to_patch_function;
std::map<std::string, std::shared_ptr<CompiledFunctionCode>> name_to_patch_function;
std::vector<MenuItem> patch_menu() const;
inline bool patch_menu_empty() const {
return this->name_to_patch_function.empty();
}
};
struct DOLFileIndex {
struct DOLFile {
uint32_t menu_item_id;
std::string name;
std::string data;
};
std::vector<std::shared_ptr<DOLFile>> item_id_to_file;
std::map<std::string, std::shared_ptr<DOLFile>> name_to_file;
DOLFileIndex(const std::string& directory);
std::vector<MenuItem> menu() const;
inline bool empty() const {
return this->name_to_file.empty() && this->item_id_to_file.empty();
}
};
+72 -54
View File
@@ -27,6 +27,7 @@ using namespace std;
static const size_t DEFAULT_RESEND_PUSH_USECS = 200000; // 200ms
PrefixedLogger IPStackSimulator::log("[IPStackSimulator] ");
@@ -76,12 +77,8 @@ string IPStackSimulator::str_for_tcp_connection(shared_ptr<const IPClient> c,
IPStackSimulator::IPStackSimulator(
std::shared_ptr<struct event_base> base,
std::shared_ptr<Server> game_server,
std::shared_ptr<ProxyServer> proxy_server,
std::shared_ptr<ServerState> state)
: base(base),
game_server(game_server),
proxy_server(proxy_server),
state(state),
pcap_text_log_file(state->ip_stack_debug ? fopen("IPStackSimulator-Log.txt", "wt") : nullptr) {
memset(this->host_mac_address_bytes, 0x90, 6);
@@ -123,7 +120,7 @@ void IPStackSimulator::add_socket(int fd) {
uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_addr) {
// Use and address not on the same subnet as the client, so that PSO Plus and
// Use an address not on the same subnet as the client, so that PSO Plus and
// Episode III will think they're talking to a remote network and won't reject
// the connection.
if ((remote_addr & 0xFF000000) != 0x23000000) {
@@ -164,7 +161,7 @@ void IPStackSimulator::dispatch_on_listen_accept(
void IPStackSimulator::on_listen_accept(struct evconnlistener* listener,
evutil_socket_t fd, struct sockaddr*, int) {
int listen_fd = evconnlistener_get_fd(listener);
log(INFO, "[IPStackSimulator] Client fd %d connected via fd %d",
this->log(INFO, "Client fd %d connected via fd %d",
fd, listen_fd);
struct bufferevent *bev = bufferevent_socket_new(this->base.get(), fd,
@@ -185,7 +182,7 @@ void IPStackSimulator::dispatch_on_listen_error(
void IPStackSimulator::on_listen_error(struct evconnlistener* listener) {
int err = EVUTIL_SOCKET_ERROR();
log(ERROR, "[IPStackSimulator] Failure on listening socket %d: %d (%s)",
this->log(ERROR, "Failure on listening socket %d: %d (%s)",
evconnlistener_get_fd(listener), err, evutil_socket_error_to_string(err));
event_base_loopexit(this->base.get(), nullptr);
}
@@ -205,7 +202,7 @@ void IPStackSimulator::on_client_input(struct bufferevent* bev) {
c = this->bev_to_client.at(bev);
} catch (const out_of_range&) {
size_t bytes = evbuffer_get_length(buf);
log(ERROR, "[IPStackSimulator] Ignoring data received from unregistered client (0x%zX bytes)",
this->log(ERROR, "Ignoring data received from unregistered client (0x%zX bytes)",
bytes);
evbuffer_drain(buf, bytes);
return;
@@ -225,8 +222,10 @@ void IPStackSimulator::on_client_input(struct bufferevent* bev) {
try {
this->on_client_frame(c, frame);
} catch (const exception& e) {
log(WARNING, "[IPStackSimulator] Failed to process client frame: %s", e.what());
print_data(stderr, frame);
if (this->state->ip_stack_debug) {
this->log(WARNING, "Failed to process client frame: %s", e.what());
print_data(stderr, frame);
}
}
}
}
@@ -239,11 +238,11 @@ void IPStackSimulator::on_client_error(struct bufferevent* bev,
short events) {
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
log(WARNING, "[IPStackSimulator] Client caused error %d (%s)", err,
this->log(WARNING, "Client caused error %d (%s)", err,
evutil_socket_error_to_string(err));
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
log(INFO, "[IPStackSimulator] Client fd %d disconnected",
this->log(INFO, "Client fd %d disconnected",
bufferevent_getfd(bev));
this->bev_to_client.erase(bev);
@@ -256,7 +255,7 @@ void IPStackSimulator::on_client_frame(
shared_ptr<IPClient> c, const string& frame) {
if (this->state->ip_stack_debug) {
fputc('\n', stderr);
log(INFO, "[IPStackSimulator] Client sent frame");
this->log(INFO, "Client sent frame");
print_data(stderr, frame);
}
this->log_frame(frame);
@@ -264,7 +263,7 @@ void IPStackSimulator::on_client_frame(
FrameInfo fi(frame);
if (this->state->ip_stack_debug) {
string fi_header = fi.header_str();
log(INFO, "[IPStackSimulator] Frame header: %s", fi_header.c_str());
this->log(INFO, "Frame header: %s", fi_header.c_str());
}
if (fi.arp) {
@@ -373,7 +372,7 @@ void IPStackSimulator::on_client_arp_frame(
evbuffer_add(out_buf, r_payload, sizeof(r_payload));
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Sending ARP response");
this->log(INFO, "Sending ARP response");
}
if (this->pcap_text_log_file) {
@@ -435,7 +434,7 @@ void IPStackSimulator::on_client_udp_frame(
if (this->state->ip_stack_debug) {
string remote_str = this->str_for_ipv4_netloc(fi.ipv4->src_addr, fi.udp->src_port);
log(INFO, "[IPStackSimulator] Sending DNS response to %s", remote_str.c_str());
this->log(INFO, "Sending DNS response to %s", remote_str.c_str());
}
uint16_t frame_size = sizeof(r_ether) + sizeof(r_ipv4) + sizeof(r_udp) + r_data.size();
@@ -482,7 +481,7 @@ uint64_t IPStackSimulator::tcp_conn_key_for_client_frame(const FrameInfo& fi) {
void IPStackSimulator::on_client_tcp_frame(
shared_ptr<IPClient> c, const FrameInfo& fi) {
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Client sent TCP frame (seq=%08" PRIX32 ", ack=%08" PRIX32 ")",
this->log(INFO, "Client sent TCP frame (seq=%08" PRIX32 ", ack=%08" PRIX32 ")",
fi.tcp->seq_num.load(), fi.tcp->ack_num.load());
}
@@ -544,7 +543,7 @@ void IPStackSimulator::on_client_tcp_frame(
uint64_t key = this->tcp_conn_key_for_client_frame(fi);
auto emplace_ret = c->tcp_connections.emplace(key, IPClient::TCPConnection());
auto& conn = emplace_ret.first->second;
string conn_str = this->state->ip_stack_debug ? this->str_for_tcp_connection(c, conn) : "";
string conn_str;
if (emplace_ret.second) {
// Connection is new; initialize it
@@ -559,9 +558,14 @@ void IPStackSimulator::on_client_tcp_frame(
conn.resend_push_usecs = DEFAULT_RESEND_PUSH_USECS;
conn.awaiting_first_ack = true;
conn.max_frame_size = max_frame_size;
conn_str = this->str_for_tcp_connection(c, conn);
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Client opened TCP connection %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")",
this->log(INFO, "Client opened TCP connection %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")",
conn_str.c_str(), conn.acked_server_seq, conn.next_client_seq);
} else {
this->log(INFO, "Client opened TCP connection %s",
conn_str.c_str());
}
} else {
@@ -571,8 +575,9 @@ void IPStackSimulator::on_client_tcp_frame(
}
// TODO: We should check the syn/ack numbers here instead of just assuming
// they're correct
conn_str = this->str_for_tcp_connection(c, conn);
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Client resent SYN for TCP connection %s",
this->log(INFO, "Client resent SYN for TCP connection %s",
conn_str.c_str());
}
}
@@ -580,7 +585,7 @@ void IPStackSimulator::on_client_tcp_frame(
// Send a SYN+ACK (send_tcp_frame always adds the ACK flag)
this->send_tcp_frame(c, conn, TCPHeader::Flag::SYN);
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Sent SYN+ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")",
this->log(INFO, "Sent SYN+ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")",
conn_str.c_str(), conn.acked_server_seq, conn.next_client_seq);
}
@@ -597,7 +602,7 @@ void IPStackSimulator::on_client_tcp_frame(
if (fi.tcp->flags & TCPHeader::Flag::ACK) {
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Client sent ACK %08" PRIX32, fi.tcp->ack_num.load());
this->log(INFO, "Client sent ACK %08" PRIX32, fi.tcp->ack_num.load());
}
if (conn->awaiting_first_ack) {
if (fi.tcp->ack_num != conn->acked_server_seq + 1) {
@@ -609,7 +614,7 @@ void IPStackSimulator::on_client_tcp_frame(
} else {
if (seq_num_greater(fi.tcp->ack_num, conn->acked_server_seq)) {
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Advancing acked_server_seq from %08" PRIX32, conn->acked_server_seq);
this->log(INFO, "Advancing acked_server_seq from %08" PRIX32, conn->acked_server_seq);
}
uint32_t ack_delta = fi.tcp->ack_num - conn->acked_server_seq;
size_t pending_bytes = evbuffer_get_length(conn->pending_data.get());
@@ -622,7 +627,7 @@ void IPStackSimulator::on_client_tcp_frame(
conn->resend_push_usecs = DEFAULT_RESEND_PUSH_USECS;
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Removed %08" PRIX32 " bytes from pending buffer and advanced acked_server_seq to %08" PRIX32,
this->log(INFO, "Removed %08" PRIX32 " bytes from pending buffer and advanced acked_server_seq to %08" PRIX32,
ack_delta, conn->acked_server_seq);
}
@@ -642,10 +647,8 @@ void IPStackSimulator::on_client_tcp_frame(
throw runtime_error("client sent TCP FIN+RST");
}
if (this->state->ip_stack_debug) {
string conn_str = this->str_for_tcp_connection(c, *conn);
log(INFO, "[IPStackSimulator] Client closed TCP connection %s", conn_str.c_str());
}
string conn_str = this->str_for_tcp_connection(c, *conn);
this->log(INFO, "Client closed TCP connection %s", conn_str.c_str());
// TODO: Are we supposed to send a response to an RST? Here we do, and the
// client probably just ignores it anyway
@@ -686,8 +689,8 @@ void IPStackSimulator::on_client_tcp_frame(
// ignore it (but warn) and send an ACK later, and the client should
// retransmit the lost data
if (this->state->ip_stack_debug) {
log(WARNING,
"[IPStackSimulator] Client sent out-of-order sequence number (expected %08" PRIX32 ", received %08" PRIX32 ", 0x%zX data bytes)",
this->log(WARNING,
"Client sent out-of-order sequence number (expected %08" PRIX32 ", received %08" PRIX32 ", 0x%zX data bytes)",
conn->next_client_seq, fi.tcp->seq_num.load(), fi.payload_size);
}
payload_skip_bytes = fi.payload_size;
@@ -703,10 +706,10 @@ void IPStackSimulator::on_client_tcp_frame(
if (this->state->ip_stack_debug) {
if (payload_skip_bytes) {
log(INFO, "[IPStackSimulator] Client sent data on TCP connection %s, overlapping existing ack'ed data (0x%zX bytes ignored)",
this->log(INFO, "Client sent data on TCP connection %s, overlapping existing ack'ed data (0x%zX bytes ignored)",
conn_str.c_str(), payload_skip_bytes);
} else {
log(INFO, "[IPStackSimulator] Client sent data on TCP connection %s",
this->log(INFO, "Client sent data on TCP connection %s",
conn_str.c_str());
}
print_data(stderr, payload, payload_size);
@@ -725,7 +728,7 @@ void IPStackSimulator::on_client_tcp_frame(
// Send an ACK
this->send_tcp_frame(c, *conn);
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Sent PSH ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ", bytes_received=0x%zX)",
this->log(INFO, "Sent PSH ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ", bytes_received=0x%zX)",
conn_str.c_str(), conn->acked_server_seq, conn->next_client_seq, conn->bytes_received);
}
}
@@ -743,13 +746,6 @@ void IPStackSimulator::open_server_connection(
throw logic_error("server connection is already open");
}
const PortConfiguration* port_config;
try {
port_config = &this->state->numbered_port_configuration.at(conn.server_port);
} catch (const out_of_range&) {
throw logic_error("client connected to port missing from configuration");
}
struct bufferevent* bevs[2];
bufferevent_pair_new(this->base.get(), 0, bevs);
@@ -762,16 +758,35 @@ void IPStackSimulator::open_server_connection(
// Link the client to the server - the server sees this as a normal TCP
// connection and treats it as if the client connected to one of its listening
// sockets
if (this->game_server.get()) {
this->game_server->connect_client(bevs[1], c->ipv4_addr, conn.client_port,
port_config->version, port_config->behavior);
} else if (this->proxy_server.get()) {
this->proxy_server->connect_client(bevs[1]);
shared_ptr<const PortConfiguration> port_config;
try {
port_config = this->state->number_to_port_config.at(conn.server_port);
} catch (const out_of_range&) {
bufferevent_free(bevs[1]);
throw logic_error("client connected to port missing from configuration");
}
string conn_str = this->str_for_tcp_connection(c, conn);
log(INFO, "[IPStackSimulator] Connected TCP connection %s to game server",
conn_str.c_str());
if (port_config->behavior == ServerBehavior::PROXY_SERVER) {
if (!this->state->proxy_server.get()) {
this->log(ERROR, "TCP connection %s is to non-running proxy server",
conn_str.c_str());
flush_and_free_bufferevent(bevs[1]);
} else {
this->state->proxy_server->connect_client(bevs[1], conn.server_port);
this->log(INFO, "Connected TCP connection %s to proxy server",
conn_str.c_str());
}
} else if (this->state->game_server.get()) {
this->state->game_server->connect_client(bevs[1], c->ipv4_addr,
conn.client_port, port_config->version, port_config->behavior);
this->log(INFO, "Connected TCP connection %s to game server",
conn_str.c_str());
} else {
this->log(ERROR, "No server available for TCP connection %s",
conn_str.c_str());
flush_and_free_bufferevent(bevs[1]);
}
}
void IPStackSimulator::send_pending_push_frame(
@@ -784,7 +799,7 @@ void IPStackSimulator::send_pending_push_frame(
size_t bytes_to_send = min<size_t>(pending_bytes, conn.max_frame_size);
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Sending PSH frame with seq_num %08" PRIX32 ", 0x%zX/0x%zX data bytes",
this->log(INFO, "Sending PSH frame with seq_num %08" PRIX32 ", 0x%zX/0x%zX data bytes",
conn.acked_server_seq, bytes_to_send, pending_bytes);
}
@@ -872,7 +887,7 @@ void IPStackSimulator::dispatch_on_resend_push(evutil_socket_t, short, void* ctx
auto* conn = reinterpret_cast<IPClient::TCPConnection*>(ctx);
auto c = conn->client.lock();
if (!c.get()) {
log(WARNING, "[IPStackSimulator] Resend push event triggered for deleted client; ignoring");
IPStackSimulator::log(WARNING, "Resend push event triggered for deleted client; ignoring");
} else {
c->sim->on_resend_push(c, *conn);
}
@@ -886,7 +901,7 @@ void IPStackSimulator::dispatch_on_server_input(struct bufferevent*, void* ctx)
auto* conn = reinterpret_cast<IPClient::TCPConnection*>(ctx);
auto c = conn->client.lock();
if (!c.get()) {
log(WARNING, "[IPStackSimulator] Server input event triggered for deleted client; ignoring");
IPStackSimulator::log(WARNING, "Server input event triggered for deleted client; ignoring");
} else {
c->sim->on_server_input(c, *conn);
}
@@ -895,7 +910,7 @@ void IPStackSimulator::dispatch_on_server_input(struct bufferevent*, void* ctx)
void IPStackSimulator::on_server_input(shared_ptr<IPClient> c, IPClient::TCPConnection& conn) {
struct evbuffer* buf = bufferevent_get_input(conn.server_bev.get());
if (this->state->ip_stack_debug) {
log(INFO, "[IPStackSimulator] Server input event: 0x%zX bytes to read",
this->log(INFO, "Server input event: 0x%zX bytes to read",
evbuffer_get_length(buf));
}
@@ -908,7 +923,7 @@ void IPStackSimulator::dispatch_on_server_error(
auto* conn = reinterpret_cast<IPClient::TCPConnection*>(ctx);
auto c = conn->client.lock();
if (!c.get()) {
log(WARNING, "[IPStackSimulator] Server error event triggered for deleted client; ignoring");
IPStackSimulator::log(WARNING, "Server error event triggered for deleted client; ignoring");
} else {
c->sim->on_server_error(c, *conn, events);
}
@@ -918,7 +933,7 @@ void IPStackSimulator::on_server_error(
shared_ptr<IPClient> c, IPClient::TCPConnection& conn, short events) {
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
log(WARNING, "[IPStackSimulator] Received error %d from virtual connection (%s)", err,
this->log(WARNING, "Received error %d from virtual connection (%s)", err,
evutil_socket_error_to_string(err));
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
@@ -929,6 +944,9 @@ void IPStackSimulator::on_server_error(
// Delete the connection object (this also flushes and frees the server
// virtual connection bufferevent)
string conn_str = this->str_for_tcp_connection(c, conn);
this->log(INFO, "Server closed TCP connection %s",
conn_str.c_str());
c->tcp_connections.erase(this->tcp_conn_key_for_connection(conn));
}
}
@@ -938,7 +956,7 @@ void IPStackSimulator::on_server_error(
void IPStackSimulator::log_frame(const string& data) const {
if (this->pcap_text_log_file) {
print_data(this->pcap_text_log_file, data, 0, nullptr,
PrintDataFlags::SkipSeparator);
PrintDataFlags::SKIP_SEPARATOR);
fputc('\n', this->pcap_text_log_file);
fflush(this->pcap_text_log_file);
}
+1 -4
View File
@@ -17,8 +17,6 @@ class IPStackSimulator {
public:
IPStackSimulator(
std::shared_ptr<struct event_base> base,
std::shared_ptr<Server> game_server,
std::shared_ptr<ProxyServer> proxy_server,
std::shared_ptr<ServerState> state);
~IPStackSimulator();
@@ -30,9 +28,8 @@ public:
static uint32_t connect_address_for_remote_address(uint32_t remote_addr);
private:
static PrefixedLogger log;
std::shared_ptr<struct event_base> base;
std::shared_ptr<Server> game_server;
std::shared_ptr<ProxyServer> proxy_server;
std::shared_ptr<ServerState> state;
using unique_listener = std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)>;
+128 -129
View File
@@ -142,23 +142,24 @@ using namespace std;
////////////////////////////////////////////////////////////////////////////////
void player_use_item(shared_ptr<Client> c, size_t item_index) {
auto player = c->game_data.player();
ssize_t equipped_weapon = -1;
// ssize_t equipped_armor = -1;
// ssize_t equipped_shield = -1;
// ssize_t equipped_mag = -1;
for (size_t y = 0; y < c->player.inventory.num_items; y++) {
if (c->player.inventory.items[y].equip_flags & 0x0008) {
if (c->player.inventory.items[y].data.item_data1[0] == 0) {
for (size_t y = 0; y < c->game_data.player()->inventory.num_items; y++) {
if (c->game_data.player()->inventory.items[y].equip_flags & 0x0008) {
if (c->game_data.player()->inventory.items[y].data.data1[0] == 0) {
equipped_weapon = y;
}
// else if ((c->player.inventory.items[y].data.item_data1[0] == 1) &&
// (c->player.inventory.items[y].data.item_data1[1] == 1)) {
// else if ((c->game_data.player()->inventory.items[y].data.data1[0] == 1) &&
// (c->game_data.player()->inventory.items[y].data.data1[1] == 1)) {
// equipped_armor = y;
// } else if ((c->player.inventory.items[y].data.item_data1[0] == 1) &&
// (c->player.inventory.items[y].data.item_data1[1] == 2)) {
// } else if ((c->game_data.player()->inventory.items[y].data.data1[0] == 1) &&
// (c->game_data.player()->inventory.items[y].data.data1[1] == 2)) {
// equipped_shield = y;
// } else if (c->player.inventory.items[y].data.item_data1[0] == 2) {
// } else if (c->game_data.player()->inventory.items[y].data.data1[0] == 2) {
// equipped_mag = y;
// }
}
@@ -166,42 +167,42 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
bool should_delete_item = true;
auto& item = c->player.inventory.items[item_index];
if (item.data.item_data1w[0] == 0x0203) { // technique disk
c->player.disp.technique_levels[item.data.item_data1[4]] = item.data.item_data1[2];
auto& item = c->game_data.player()->inventory.items[item_index];
if (item.data.data1w[0] == 0x0203) { // technique disk
c->game_data.player()->disp.technique_levels.data()[item.data.data1[4]] = item.data.data1[2];
} else if (item.data.item_data1w[0] == 0x0A03) { // grinder
} else if (item.data.data1w[0] == 0x0A03) { // grinder
if (equipped_weapon < 0) {
throw invalid_argument("grinder used with no weapon equipped");
}
if (item.data.item_data1[2] > 2) {
if (item.data.data1[2] > 2) {
throw invalid_argument("incorrect grinder value");
}
c->player.inventory.items[equipped_weapon].data.item_data1[3] += (item.data.item_data1[2] + 1);
c->game_data.player()->inventory.items[equipped_weapon].data.data1[3] += (item.data.data1[2] + 1);
// TODO: we should check for max grind here
} else if (item.data.item_data1w[0] == 0x0B03) { // material
switch (item.data.item_data1[2]) {
} else if (item.data.data1w[0] == 0x0B03) { // material
switch (item.data.data1[2]) {
case 0: // Power Material
c->player.disp.stats.atp += 2;
c->game_data.player()->disp.stats.atp += 2;
break;
case 1: // Mind Material
c->player.disp.stats.mst += 2;
c->game_data.player()->disp.stats.mst += 2;
break;
case 2: // Evade Material
c->player.disp.stats.evp += 2;
c->game_data.player()->disp.stats.evp += 2;
break;
case 3: // HP Material
c->player.inventory.hp_materials_used += 2;
c->game_data.player()->inventory.hp_materials_used += 2;
break;
case 4: // TP Material
c->player.inventory.tp_materials_used += 2;
c->game_data.player()->inventory.tp_materials_used += 2;
break;
case 5: // Def Material
c->player.disp.stats.dfp += 2;
c->game_data.player()->disp.stats.dfp += 2;
break;
case 6: // Luck Material
c->player.disp.stats.lck += 2;
c->game_data.player()->disp.stats.lck += 2;
break;
default:
throw invalid_argument("unknown material used");
@@ -209,17 +210,17 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
} else {
// default item action is to unwrap the item if it's a present
if ((item.data.item_data1[0] == 2) && (item.data.item_data2[2] & 0x40)) {
item.data.item_data2[2] &= 0xBF;
if ((item.data.data1[0] == 2) && (item.data.data2[2] & 0x40)) {
item.data.data2[2] &= 0xBF;
should_delete_item = false;
} else if ((item.data.item_data1[0] != 2) && (item.data.item_data1[4] & 0x40)) {
item.data.item_data1[4] &= 0xBF;
} else if ((item.data.data1[0] != 2) && (item.data.data1[4] & 0x40)) {
item.data.data1[4] &= 0xBF;
should_delete_item = false;
}
}
if (should_delete_item) {
c->player.remove_item(item.data.item_id, 1, nullptr);
c->game_data.player()->remove_item(item.data.id, 1);
}
}
@@ -315,7 +316,6 @@ ItemData CommonItemCreator::create_drop_item(bool is_box, uint8_t episode,
}
ItemData item;
memset(&item, 0, sizeof(item));
// picks a random non-rare item type, then gives it appropriate random stats
// modify some of the constants in this section to change the system's
@@ -323,56 +323,56 @@ ItemData CommonItemCreator::create_drop_item(bool is_box, uint8_t episode,
int32_t type = this->decide_item_type(is_box);
switch (type) {
case 0x00: // material
item.item_data1[0] = 0x03;
item.item_data1[1] = 0x0B;
item.item_data1[2] = random_int(0, 6);
item.data1[0] = 0x03;
item.data1[1] = 0x0B;
item.data1[2] = random_int(0, 6);
break;
case 0x01: // equipment
switch (random_int(0, 3)) {
case 0x00: // weapon
item.item_data1[1] = random_int(1, 12); // random normal class
item.item_data1[2] = difficulty + random_int(0, 2); // special type
if ((item.item_data1[1] > 0x09) && (item.item_data1[2] > 0x04)) {
item.item_data1[2] = 0x04; // no special classes above 4
item.data1[1] = random_int(1, 12); // random normal class
item.data1[2] = difficulty + random_int(0, 2); // special type
if ((item.data1[1] > 0x09) && (item.data1[2] > 0x04)) {
item.data1[2] = 0x04; // no special classes above 4
}
item.item_data1[4] = 0x80; // untekked
if (item.item_data1[2] < 0x04) {
item.item_data1[4] |= random_int(0, 40); // give a special
item.data1[4] = 0x80; // untekked
if (item.data1[2] < 0x04) {
item.data1[4] |= random_int(0, 40); // give a special
}
for (size_t x = 0, y = 0; (x < 5) && (y < 3); x++) { // percentages
if (random_int(0, 10) == 1) { // 1/11 chance of getting each type of percentage
item.item_data1[6 + (y * 2)] = x + 1;
item.item_data1[7 + (y * 2)] = random_int(0, 10) * 5;
item.data1[6 + (y * 2)] = x + 1;
item.data1[7 + (y * 2)] = random_int(0, 10) * 5;
y++;
}
}
break;
case 0x01: // armor
item.item_data1[0] = 0x01;
item.item_data1[1] = 0x01;
item.item_data1[2] = (6 * difficulty) + random_int(0, ((area / 2) + 2) - 1); // standard type based on difficulty and area
if (item.item_data1[2] > 0x17) {
item.item_data1[2] = 0x17; // no standard types above 0x17
item.data1[0] = 0x01;
item.data1[1] = 0x01;
item.data1[2] = (6 * difficulty) + random_int(0, ((area / 2) + 2) - 1); // standard type based on difficulty and area
if (item.data1[2] > 0x17) {
item.data1[2] = 0x17; // no standard types above 0x17
}
if (random_int(0, 10) == 0) { // +/-
item.item_data1[4] = random_int(0, 5);
item.item_data1[6] = random_int(0, 2);
item.data1[4] = random_int(0, 5);
item.data1[6] = random_int(0, 2);
}
item.item_data1[5] = random_int(0, 4); // slots
item.data1[5] = random_int(0, 4); // slots
break;
case 0x02: // shield
item.item_data1[0] = 0x01;
item.item_data1[1] = 0x02;
item.item_data1[2] = (5 * difficulty) + random_int(0, ((area / 2) + 2) - 1); // standard type based on difficulty and area
if (item.item_data1[2] > 0x14) {
item.item_data1[2] = 0x14; // no standard types above 0x14
item.data1[0] = 0x01;
item.data1[1] = 0x02;
item.data1[2] = (5 * difficulty) + random_int(0, ((area / 2) + 2) - 1); // standard type based on difficulty and area
if (item.data1[2] > 0x14) {
item.data1[2] = 0x14; // no standard types above 0x14
}
if (random_int(0, 10) == 0) { // +/-
item.item_data1[4] = random_int(0, 5);
item.item_data1[6] = random_int(0, 5);
item.data1[4] = random_int(0, 5);
item.data1[6] = random_int(0, 5);
}
break;
@@ -382,81 +382,81 @@ ItemData CommonItemCreator::create_drop_item(bool is_box, uint8_t episode,
if (type == 0xFF) {
throw out_of_range("no item dropped"); // 0xFF -> no item drops
}
item.item_data1[0] = 0x01;
item.item_data1[1] = 0x03;
item.item_data1[2] = type;
item.data1[0] = 0x01;
item.data1[1] = 0x03;
item.data1[2] = type;
break;
}
}
break;
case 0x02: // technique
item.item_data1[0] = 0x03;
item.item_data1[1] = 0x02;
item.item_data1[4] = random_int(0, 18); // tech type
if ((item.item_data1[4] != 14) && (item.item_data1[4] != 17)) { // if not ryuker or reverser, give it a level
if (item.item_data1[4] == 16) { // if not anti, give it a level between 1 and 30
item.data1[0] = 0x03;
item.data1[1] = 0x02;
item.data1[4] = random_int(0, 18); // tech type
if ((item.data1[4] != 14) && (item.data1[4] != 17)) { // if not ryuker or reverser, give it a level
if (item.data1[4] == 16) { // if not anti, give it a level between 1 and 30
if (area > 3) {
item.item_data1[2] = difficulty + random_int(0, ((area - 1) / 2) - 1);
item.data1[2] = difficulty + random_int(0, ((area - 1) / 2) - 1);
} else {
item.item_data1[2] = difficulty;
item.data1[2] = difficulty;
}
if (item.item_data1[2] > 6) {
item.item_data1[2] = 6;
if (item.data1[2] > 6) {
item.data1[2] = 6;
}
} else {
item.item_data1[2] = (5 * difficulty) + random_int(0, ((area * 3) / 2) - 1); // else between 1 and 7
item.data1[2] = (5 * difficulty) + random_int(0, ((area * 3) / 2) - 1); // else between 1 and 7
}
}
break;
case 0x03: // scape doll
item.item_data1[0] = 0x03;
item.item_data1[1] = 0x09;
item.item_data1[2] = 0x00;
item.data1[0] = 0x03;
item.data1[1] = 0x09;
item.data1[2] = 0x00;
break;
case 0x04: // grinder
item.item_data1[0] = 0x03;
item.item_data1[1] = 0x0A;
item.item_data1[2] = random_int(0, 2); // mono, di, tri
item.data1[0] = 0x03;
item.data1[1] = 0x0A;
item.data1[2] = random_int(0, 2); // mono, di, tri
break;
case 0x05: // consumable
item.item_data1[0] = 0x03;
item.item_data1[5] = 0x01;
item.data1[0] = 0x03;
item.data1[5] = 0x01;
switch (random_int(0, 2)) {
case 0: // antidote / antiparalysis
item.item_data1[1] = 6;
item.item_data1[2] = random_int(0, 1);
item.data1[1] = 6;
item.data1[2] = random_int(0, 1);
break;
case 1: // telepipe / trap vision
item.item_data1[1] = 7 + random_int(0, 1);
item.data1[1] = 7 + random_int(0, 1);
break;
case 2: // sol / moon / star atomizer
item.item_data1[1] = 3 + random_int(0, 2);
item.data1[1] = 3 + random_int(0, 2);
break;
}
break;
case 0x06: // consumable
item.item_data1[0] = 0x03;
item.item_data1[5] = 0x01;
item.item_data1[1] = random_int(0, 1); // mate or fluid
item.data1[0] = 0x03;
item.data1[5] = 0x01;
item.data1[1] = random_int(0, 1); // mate or fluid
if (difficulty == 0) {
item.item_data1[2] = random_int(0, 1); // only mono and di on normal
item.data1[2] = random_int(0, 1); // only mono and di on normal
} else if (difficulty == 3) {
item.item_data1[2] = random_int(1, 2); // only di and tri on ultimate
item.data1[2] = random_int(1, 2); // only di and tri on ultimate
} else {
item.item_data1[2] = random_int(0, 2); // else, any of the three
item.data1[2] = random_int(0, 2); // else, any of the three
}
break;
case 0x07: // meseta
item.item_data1[0] = 0x04;
item.item_data2d = (90 * difficulty) + (random_int(0, 20) * (area * 2)); // meseta amount
item.data1[0] = 0x04;
item.data2d = (90 * difficulty) + (random_int(1, 20) * (area * 2)); // meseta amount
break;
default:
@@ -475,29 +475,28 @@ ItemData CommonItemCreator::create_shop_item(uint8_t difficulty,
static const uint8_t max_anti_level[4] = { 2, 4, 6, 7};
ItemData item;
memset(&item, 0, sizeof(item));
item.item_data1[0] = item_type;
while (item.item_data1[0] == 2) {
item.item_data1[0] = rand() % 3;
item.data1[0] = item_type;
while (item.data1[0] == 2) {
item.data1[0] = rand() % 3;
}
switch (item.item_data1[0]) {
switch (item.data1[0]) {
case 0: { // weapon
item.item_data1[1] = (rand() % 12) + 1;
if (item.item_data1[1] > 9) {
item.item_data1[2] = difficulty;
item.data1[1] = (rand() % 12) + 1;
if (item.data1[1] > 9) {
item.data1[2] = difficulty;
} else {
item.item_data1[2] = (rand() & 1) + difficulty;
item.data1[2] = (rand() & 1) + difficulty;
}
item.item_data1[3] = rand() % 11;
item.item_data1[4] = rand() % 11;
item.data1[3] = rand() % 11;
item.data1[4] = rand() % 11;
size_t num_percentages = 0;
for (size_t x = 0; (x < 5) && (num_percentages < 3); x++) {
if ((rand() % 4) == 1) {
item.item_data1[(num_percentages * 2) + 6] = x;
item.item_data1[(num_percentages * 2) + 7] = rand() % (max_percentages[difficulty] + 1);
item.data1[(num_percentages * 2) + 6] = x;
item.data1[(num_percentages * 2) + 7] = rand() % (max_percentages[difficulty] + 1);
num_percentages++;
}
}
@@ -505,69 +504,69 @@ ItemData CommonItemCreator::create_shop_item(uint8_t difficulty,
}
case 1: // armor
item.item_data1[1] = 0;
while (item.item_data1[1] == 0) {
item.item_data1[1] = rand() & 3;
item.data1[1] = 0;
while (item.data1[1] == 0) {
item.data1[1] = rand() & 3;
}
switch (item.item_data1[1]) {
switch (item.data1[1]) {
case 1:
item.item_data1[2] = (rand() % 6) + (difficulty * 6);
item.item_data1[5] = rand() % 5;
item.data1[2] = (rand() % 6) + (difficulty * 6);
item.data1[5] = rand() % 5;
break;
case 2:
item.item_data2[2] = (rand() % 6) + (difficulty * 5);
*reinterpret_cast<short*>(&item.item_data1[6]) = (rand() % 9) - 4;
*reinterpret_cast<short*>(&item.item_data1[9]) = (rand() % 9) - 4;
item.data2[2] = (rand() % 6) + (difficulty * 5);
*reinterpret_cast<short*>(&item.data1[6]) = (rand() % 9) - 4;
*reinterpret_cast<short*>(&item.data1[9]) = (rand() % 9) - 4;
break;
case 3:
item.item_data2[2] = rand() % 0x3B;
*reinterpret_cast<short*>(&item.item_data1[7]) = (rand() % 5) - 4;
item.data2[2] = rand() % 0x3B;
*reinterpret_cast<short*>(&item.data1[7]) = (rand() % 5) - 4;
break;
}
break;
case 3: // tool
item.item_data1[1] = rand() % 12;
switch (item.item_data1[1]) {
item.data1[1] = rand() % 12;
switch (item.data1[1]) {
case 0:
case 1:
if (difficulty == 0) {
item.item_data1[2] = 0;
item.data1[2] = 0;
} else if (difficulty == 1) {
item.item_data1[2] = rand() % 2;
item.data1[2] = rand() % 2;
} else if (difficulty == 2) {
item.item_data1[2] = (rand() % 2) + 1;
item.data1[2] = (rand() % 2) + 1;
} else if (difficulty == 3) {
item.item_data1[2] = 2;
item.data1[2] = 2;
}
break;
case 6:
item.item_data1[2] = rand() % 2;
item.data1[2] = rand() % 2;
break;
case 10:
item.item_data1[2] = rand() % 3;
item.data1[2] = rand() % 3;
break;
case 11:
item.item_data1[2] = rand() % 7;
item.data1[2] = rand() % 7;
break;
}
switch (item.item_data1[1]) {
switch (item.data1[1]) {
case 2:
item.item_data1[4] = rand() % 19;
switch (item.item_data1[4]) {
item.data1[4] = rand() % 19;
switch (item.data1[4]) {
case 14:
case 17:
item.item_data1[2] = 0; // reverser & ryuker always level 1
item.data1[2] = 0; // reverser & ryuker always level 1
break;
case 16:
item.item_data1[2] = rand() % max_anti_level[difficulty];
item.data1[2] = rand() % max_anti_level[difficulty];
break;
default:
item.item_data1[2] = rand() % max_tech_level[difficulty];
item.data1[2] = rand() % max_tech_level[difficulty];
}
break;
case 0:
@@ -579,7 +578,7 @@ ItemData CommonItemCreator::create_shop_item(uint8_t difficulty,
case 7:
case 8:
case 16:
item.item_data1[5] = rand() % (max_quantity[difficulty] + 1);
item.data1[5] = rand() % (max_quantity[difficulty] + 1);
break;
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ using namespace std;
LevelTable::LevelTable(const char* filename, bool compressed) {
LevelTable::LevelTable(const string& filename, bool compressed) {
string data = load_file(filename);
if (compressed) {
+21 -6
View File
@@ -2,7 +2,22 @@
#include <stdint.h>
#include "Player.hh"
#include <string>
#include <phosg/Encoding.hh>
struct PlayerStats {
le_uint16_t atp;
le_uint16_t mst;
le_uint16_t evp;
le_uint16_t hp;
le_uint16_t dfp;
le_uint16_t ata;
le_uint16_t lck;
PlayerStats() noexcept;
} __attribute__((packed));
// information on a single level for a single class
struct LevelStats {
@@ -13,19 +28,19 @@ struct LevelStats {
uint8_t dfp; // dfp to add on level up
uint8_t ata; // ata to add on level up
uint8_t unknown[2];
uint32_t experience; // EXP value of this level
le_uint32_t experience; // EXP value of this level
void apply(PlayerStats& ps) const;
};
} __attribute__((packed));
// level table format (PlyLevelTbl.prs)
struct LevelTable {
PlayerStats base_stats[12];
uint32_t unknown[12];
le_uint32_t unknown[12];
LevelStats levels[12][200];
LevelTable(const char* filename, bool compressed);
LevelTable(const std::string& filename, bool compressed);
const PlayerStats& base_stats_for_class(uint8_t char_class) const;
const LevelStats& stats_for_level(uint8_t char_class, uint8_t level) const;
};
} __attribute__((packed));
+59 -45
View File
@@ -11,31 +11,23 @@ using namespace std;
License::License() {
memset(this->username, 0, 20);
memset(this->bb_password, 0, 20);
this->serial_number = 0;
memset(this->access_key, 0, 16);
memset(this->gc_password, 0, 12);
this->privileges = 0;
this->ban_end_time = 0;
}
License::License() : serial_number(0), privileges(0), ban_end_time(0) { }
string License::str() const {
string ret = string_printf("License(serial_number=%" PRIu32, this->serial_number);
if (this->username[0]) {
if (!this->username.empty()) {
ret += ", username=";
ret += this->username;
}
if (this->bb_password[0]) {
if (!this->bb_password.empty()) {
ret += ", bb-password=";
ret += this->bb_password;
}
if (this->access_key[0]) {
if (!this->access_key.empty()) {
ret += ", access-key=";
ret += this->access_key;
}
if (this->gc_password[0]) {
if (!this->gc_password.empty()) {
ret += ", gc-password=";
ret += this->gc_password;
}
@@ -53,6 +45,12 @@ LicenseManager::LicenseManager(const string& filename) : filename(filename) {
auto licenses = load_vector_file<License>(this->filename);
for (const auto& read_license : licenses) {
shared_ptr<License> license(new License(read_license));
// Before the temporary flag existed, licenses with root privileges would
// have the temporary flag set. To migrate these, explicitly unset the
// flag for all licenses loaded from the license file.
license->privileges &= ~Privilege::TEMPORARY;
uint32_t serial_number = license->serial_number;
this->bb_username_to_license.emplace(license->username, license);
this->serial_number_to_license.emplace(serial_number, license);
@@ -67,19 +65,19 @@ LicenseManager::LicenseManager(const string& filename) : filename(filename) {
void LicenseManager::save() const {
auto f = fopen_unique(this->filename, "wb");
for (const auto& it : this->serial_number_to_license) {
if (it.second->privileges & Privilege::TEMPORARY) {
continue;
}
fwritex(f.get(), it.second.get(), sizeof(License));
}
}
shared_ptr<const License> LicenseManager::verify_pc(uint32_t serial_number,
const char* access_key, const char* password) const {
const string& access_key) const {
auto& license = this->serial_number_to_license.at(serial_number);
if (strncmp(license->access_key, access_key, 8)) {
if (!license->access_key.eq_n(access_key, 8)) {
throw invalid_argument("incorrect access key");
}
if (password && (strcmp(license->gc_password, password))) {
throw invalid_argument("incorrect password");
}
if (license->ban_end_time && (license->ban_end_time >= now())) {
throw invalid_argument("user is banned");
@@ -88,25 +86,36 @@ shared_ptr<const License> LicenseManager::verify_pc(uint32_t serial_number,
}
shared_ptr<const License> LicenseManager::verify_gc(uint32_t serial_number,
const char* access_key, const char* password) const {
const string& access_key) const {
auto& license = this->serial_number_to_license.at(serial_number);
if (strncmp(license->access_key, access_key, 12)) {
if (!license->access_key.eq_n(access_key, 12)) {
throw invalid_argument("incorrect access key");
}
if (password && (strcmp(license->gc_password, password))) {
throw invalid_argument("incorrect password");
}
if (license->ban_end_time && (license->ban_end_time >= now())) {
throw invalid_argument("user is banned");
}
return license;
}
shared_ptr<const License> LicenseManager::verify_bb(const char* username,
const char* password) const {
shared_ptr<const License> LicenseManager::verify_gc(uint32_t serial_number,
const string& access_key, const string& password) const {
auto& license = this->serial_number_to_license.at(serial_number);
if (!license->access_key.eq_n(access_key, 12)) {
throw invalid_argument("incorrect access key");
}
if (license->gc_password != password) {
throw invalid_argument("incorrect password");
}
if (license->ban_end_time && (license->ban_end_time >= now())) {
throw invalid_argument("user is banned");
}
return license;
}
shared_ptr<const License> LicenseManager::verify_bb(const string& username,
const string& password) const {
auto& license = this->bb_username_to_license.at(username);
if (password && strcmp(license->bb_password, password)) {
if (license->bb_password != password) {
throw invalid_argument("incorrect password");
}
@@ -128,20 +137,18 @@ void LicenseManager::ban_until(uint32_t serial_number, uint64_t end_time) {
void LicenseManager::add(shared_ptr<License> l) {
uint32_t serial_number = l->serial_number;
this->serial_number_to_license.emplace(serial_number, l);
if (l->username[0]) {
if (!l->username.empty()) {
this->bb_username_to_license.emplace(l->username, l);
}
this->save();
}
void LicenseManager::remove(uint32_t serial_number) {
auto l = this->serial_number_to_license.at(serial_number);
this->serial_number_to_license.erase(l->serial_number);
if (l->username[0]) {
if (!l->username.empty()) {
this->bb_username_to_license.erase(l->username);
}
this->save();
}
@@ -154,33 +161,40 @@ vector<License> LicenseManager::snapshot() const {
}
shared_ptr<const License> LicenseManager::create_license_pc(
uint32_t serial_number,const char* access_key, const char* password) {
shared_ptr<License> LicenseManager::create_license_pc(
uint32_t serial_number, const string& access_key, bool temporary) {
shared_ptr<License> l(new License());
l->serial_number = serial_number;
strncpy(l->access_key, access_key, 8);
if (password) {
strncpy(l->gc_password, password, 8);
l->access_key = access_key;
if (temporary) {
l->privileges |= Privilege::TEMPORARY;
}
return l;
}
shared_ptr<const License> LicenseManager::create_license_gc(
uint32_t serial_number, const char* access_key, const char* password) {
shared_ptr<License> LicenseManager::create_license_gc(
uint32_t serial_number, const string& access_key, const string& password,
bool temporary) {
shared_ptr<License> l(new License());
l->serial_number = serial_number;
strncpy(l->access_key, access_key, 12);
if (password) {
strncpy(l->gc_password, password, 8);
l->access_key = access_key;
l->gc_password = password;
if (temporary) {
l->privileges |= Privilege::TEMPORARY;
}
return l;
}
shared_ptr<const License> LicenseManager::create_license_bb(
uint32_t serial_number, const char* username, const char* password) {
shared_ptr<License> LicenseManager::create_license_bb(
uint32_t serial_number, const string& username, const string& password,
bool temporary) {
shared_ptr<License> l(new License());
l->serial_number = serial_number;
strncpy(l->username, username, 19);
strncpy(l->bb_password, password, 19);
l->username = username;
l->bb_password = password;
if (temporary) {
l->privileges |= Privilege::TEMPORARY;
}
return l;
}
+35 -27
View File
@@ -5,34 +5,38 @@
#include <vector>
#include <memory>
enum Privilege {
KickUser = 0x00000001,
BanUser = 0x00000002,
SilenceUser = 0x00000004,
ChangeLobbyInfo = 0x00000008,
ChangeEvent = 0x00000010,
Announce = 0x00000020,
FreeJoinGames = 0x00000040,
UnlockGames = 0x00000080,
#include "Text.hh"
Moderator = 0x00000007,
Administrator = 0x0000003F,
Root = 0xFFFFFFFF,
enum Privilege {
KICK_USER = 0x00000001,
BAN_USER = 0x00000002,
SILENCE_USER = 0x00000004,
CHANGE_LOBBY_INFO = 0x00000008,
CHANGE_EVENT = 0x00000010,
ANNOUNCE = 0x00000020,
FREE_JOIN_GAMES = 0x00000040,
UNLOCK_GAMES = 0x00000080,
MODERATOR = 0x00000007,
ADMINISTRATOR = 0x0000003F,
ROOT = 0x7FFFFFFF,
TEMPORARY = 0x80000000,
};
enum LicenseVerifyAction {
BB = 0x00,
GC = 0x01,
PC = 0x02,
SerialNumber = 0x03,
SERIAL_NUMBER = 0x03,
};
struct License {
char username[20]; // BB username (max. 16 chars; should technically be Unicode)
char bb_password[20]; // BB password (max. 16 chars)
ptext<char, 0x14> username; // BB username (max. 16 chars; should technically be Unicode)
ptext<char, 0x14> bb_password; // BB password (max. 16 chars)
uint32_t serial_number; // PC/GC serial number. MUST BE PRESENT FOR BB LICENSES TOO; this is also the player's guild card number.
char access_key[16]; // PC/GC access key. (to log in using PC on a GC license, just enter the first 8 characters of the GC access key)
char gc_password[12]; // GC password
ptext<char, 0x10> access_key; // PC/GC access key. (to log in using PC on a GC license, just enter the first 8 characters of the GC access key)
ptext<char, 0x0C> gc_password; // GC password
uint32_t privileges; // privilege level
uint64_t ban_end_time; // end time of ban (zero = not banned)
@@ -46,11 +50,13 @@ public:
~LicenseManager() = default;
std::shared_ptr<const License> verify_pc(uint32_t serial_number,
const char* access_key, const char* password) const;
const std::string& access_key) const;
std::shared_ptr<const License> verify_gc(uint32_t serial_number,
const char* access_key, const char* password) const;
std::shared_ptr<const License> verify_bb(const char* username,
const char* password) const;
const std::string& access_key) const;
std::shared_ptr<const License> verify_gc(uint32_t serial_number,
const std::string& access_key, const std::string& password) const;
std::shared_ptr<const License> verify_bb(const std::string& username,
const std::string& password) const;
void ban_until(uint32_t serial_number, uint64_t seconds);
size_t count() const;
@@ -59,12 +65,14 @@ public:
void remove(uint32_t serial_number);
std::vector<License> snapshot() const;
static std::shared_ptr<const License> create_license_pc(
uint32_t serial_number, const char* access_key, const char* password);
static std::shared_ptr<const License> create_license_gc(
uint32_t serial_number, const char* access_key, const char* password);
static std::shared_ptr<const License> create_license_bb(
uint32_t serial_number, const char* username, const char* password);
static std::shared_ptr<License> create_license_pc(
uint32_t serial_number, const std::string& access_key, bool temporary);
static std::shared_ptr<License> create_license_gc(
uint32_t serial_number, const std::string& access_key,
const std::string& password, bool temporary);
static std::shared_ptr<License> create_license_bb(
uint32_t serial_number, const std::string& username,
const std::string& password, bool temporary);
protected:
void save() const;
+53 -38
View File
@@ -12,22 +12,14 @@ using namespace std;
Lobby::Lobby() : lobby_id(0), min_level(0), max_level(0xFFFFFFFF),
next_game_item_id(0), version(GameVersion::GC), section_id(0), episode(1),
difficulty(0), mode(0), rare_seed(random_object<uint32_t>()), event(0),
block(0), type(0), leader_id(0), max_clients(12), flags(0),
loading_quest_id(0) {
next_game_item_id(0x00810000), version(GameVersion::GC), section_id(0),
episode(1), difficulty(0), mode(0), rare_seed(random_object<uint32_t>()),
event(0), block(0), type(0), leader_id(0), max_clients(12), flags(0) {
for (size_t x = 0; x < 12; x++) {
this->next_item_id[x] = 0;
this->next_item_id[x] = 0x00010000 + 0x00200000 * x;
}
memset(&this->next_drop_item, 0, sizeof(this->next_drop_item));
memset(this->variations, 0, 0x20 * sizeof(this->variations[0]));
memset(this->password, 0, 36 * sizeof(this->password[0]));
memset(this->name, 0, 36 * sizeof(this->name[0]));
}
bool Lobby::is_game() const {
return this->flags & LobbyFlag::IsGame;
this->next_drop_item = PlayerInventoryItem();
}
void Lobby::reassign_leader_on_client_departure(size_t leaving_client_index) {
@@ -48,7 +40,7 @@ bool Lobby::any_client_loading() const {
if (!this->clients[x].get()) {
continue;
}
if (this->clients[x]->flags & ClientFlag::Loading) {
if (this->clients[x]->flags & (Client::Flag::LOADING | Client::Flag::LOADING_QUEST)) {
return true;
}
}
@@ -67,22 +59,35 @@ size_t Lobby::count_clients() const {
void Lobby::add_client(shared_ptr<Client> c) {
ssize_t index;
for (index = 0; index < max_clients; index++) {
if (!this->clients[index].get()) {
this->clients[index] = c;
break;
if (c->prefer_high_lobby_client_id) {
for (index = max_clients - 1; index >= 0; index--) {
if (!this->clients[index].get()) {
this->clients[index] = c;
break;
}
}
if (index < 0) {
throw out_of_range("no space left in lobby");
}
} else {
for (index = 0; index < max_clients; index++) {
if (!this->clients[index].get()) {
this->clients[index] = c;
break;
}
}
if (index >= max_clients) {
throw out_of_range("no space left in lobby");
}
}
if (index >= max_clients) {
throw out_of_range("no space left in lobby");
}
c->lobby_client_id = index;
c->lobby_id = this->lobby_id;
// if there's no one else in the lobby, set the leader id as well
if (index == 0) {
for (index = 1; index < max_clients; index++) {
if (this->clients[index].get()) {
// If there's no one else in the lobby, set the leader id as well
if (index == (max_clients - 1) * c->prefer_high_lobby_client_id) {
for (index = 0; index < max_clients; index++) {
if (this->clients[index].get() && this->clients[index] != c) {
break;
}
}
@@ -90,6 +95,17 @@ void Lobby::add_client(shared_ptr<Client> c) {
this->leader_id = c->lobby_client_id;
}
}
// If the lobby is a game and item tracking is enabled, assign the inventory's
// item IDs
if (this->is_game() && (this->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) {
auto& inv = c->game_data.player()->inventory;
size_t count = min<uint8_t>(inv.num_items, 30);
for (size_t x = 0; x < count; x++) {
inv.items[x].data.id = this->generate_item_id(c->lobby_client_id);
}
c->game_data.player()->print_inventory(stderr);
}
}
void Lobby::remove_client(shared_ptr<Client> c) {
@@ -103,7 +119,7 @@ void Lobby::remove_client(shared_ptr<Client> c) {
this->clients[c->lobby_client_id] = nullptr;
// unassign the client's lobby if it matches the current lobby's id (it may
// Unassign the client's lobby if it matches the current lobby's id (it may
// not match if the client was already added to another lobby - this can
// happen during the lobby change procedure)
if (c->lobby_id == this->lobby_id) {
@@ -129,7 +145,7 @@ void Lobby::move_client_to_lobby(shared_ptr<Lobby> dest_lobby,
shared_ptr<Client> Lobby::find_client(const char16_t* identifier,
shared_ptr<Client> Lobby::find_client(const u16string* identifier,
uint64_t serial_number) {
for (size_t x = 0; x < this->max_clients; x++) {
if (!this->clients[x]) {
@@ -139,7 +155,7 @@ shared_ptr<Client> Lobby::find_client(const char16_t* identifier,
(this->clients[x]->license->serial_number == serial_number)) {
return this->clients[x];
}
if (identifier && !char16cmp(this->clients[x]->player.disp.name, identifier, 0x10)) {
if (identifier && (this->clients[x]->game_data.player()->disp.name == *identifier)) {
return this->clients[x];
}
}
@@ -164,17 +180,22 @@ uint8_t Lobby::game_event_for_lobby_event(uint8_t lobby_event) {
void Lobby::add_item(const PlayerInventoryItem& item) {
this->item_id_to_floor_item.emplace(item.data.item_id, item);
void Lobby::add_item(const PlayerInventoryItem& item, uint8_t area, float x, float z) {
auto& fi = this->item_id_to_floor_item[item.data.id];
fi.inv_item = item;
fi.area = area;
fi.x = x;
fi.z = z;
}
void Lobby::remove_item(uint32_t item_id, PlayerInventoryItem* item) {
PlayerInventoryItem Lobby::remove_item(uint32_t item_id) {
auto item_it = this->item_id_to_floor_item.find(item_id);
if (item_it == this->item_id_to_floor_item.end()) {
throw out_of_range("item not present");
}
*item = move(item_it->second);
PlayerInventoryItem ret = move(item_it->second.inv_item);
this->item_id_to_floor_item.erase(item_it);
return ret;
}
uint32_t Lobby::generate_item_id(uint8_t client_id) {
@@ -183,9 +204,3 @@ uint32_t Lobby::generate_item_id(uint8_t client_id) {
}
return this->next_game_item_id++;
}
void Lobby::assign_item_ids_for_player(uint32_t client_id, PlayerInventory& inv) {
for (size_t x = 0; x < inv.num_items; x++) {
inv.items[x].data.item_id = this->generate_item_id(client_id);
}
}
+44 -25
View File
@@ -2,47 +2,65 @@
#include <inttypes.h>
#include <array>
#include <vector>
#include <string>
#include <memory>
#include <unordered_map>
#include <phosg/Encoding.hh>
#include "Client.hh"
#include "Player.hh"
#include "Map.hh"
#include "RareItemSet.hh"
enum LobbyFlag {
IsGame = 0x01,
CheatsEnabled = 0x02, // game only
Public = 0x04, // lobby only
Episode3 = 0x08, // lobby & game
QuestInProgress = 0x10, // game only
JoinableQuestInProgress = 0x20, // game only
Default = 0x40, // lobby only; not set for games and private lobbies
Persistent = 0x80, // if not set, lobby is deleted when empty
};
#include "Text.hh"
#include "Quest.hh"
struct Lobby {
enum Flag {
GAME = 0x00000001,
EPISODE_3_ONLY = 0x00000002,
// Flags used only for games
CHEATS_ENABLED = 0x00000100,
QUEST_IN_PROGRESS = 0x00000200,
JOINABLE_QUEST_IN_PROGRESS = 0x00000400,
ITEM_TRACKING_ENABLED = 0x00000800,
// Flags used only for lobbies
PUBLIC = 0x00010000,
DEFAULT = 0x00020000,
PERSISTENT = 0x00040000,
};
uint32_t lobby_id;
uint32_t min_level;
uint32_t max_level;
// item info
struct FloorItem {
PlayerInventoryItem inv_item;
float x;
float z;
uint8_t area;
};
std::vector<PSOEnemy> enemies;
std::shared_ptr<const RareItemSet> rare_item_set;
uint32_t next_item_id[12];
std::array<uint32_t, 12> next_item_id;
uint32_t next_game_item_id;
PlayerInventoryItem next_drop_item;
std::unordered_map<uint32_t, PlayerInventoryItem> item_id_to_floor_item;
uint32_t variations[0x20];
std::unordered_map<uint32_t, FloorItem> item_id_to_floor_item;
parray<le_uint32_t, 0x20> variations;
// game config
GameVersion version;
uint8_t section_id;
uint8_t episode;
uint8_t episode; // 1 = Ep1, 2 = Ep2, 3 = Ep4, 0xFF = Ep3
uint8_t difficulty;
uint8_t mode;
char16_t password[0x24];
char16_t name[0x24];
std::u16string password;
std::u16string name;
uint32_t rare_seed;
//EP3_GAME_CONFIG* ep3; // only present if this is an Episode 3 game
@@ -54,12 +72,14 @@ struct Lobby {
uint8_t leader_id;
uint8_t max_clients;
uint32_t flags;
uint32_t loading_quest_id; // for use with joinable quests
std::shared_ptr<Client> clients[12];
std::shared_ptr<const Quest> loading_quest;
std::array<std::shared_ptr<Client>, 12> clients;
Lobby();
bool is_game() const;
inline bool is_game() const {
return this->flags & Flag::GAME;
}
void reassign_leader_on_client_departure(size_t leaving_client_id);
size_t count_clients() const;
@@ -71,15 +91,14 @@ struct Lobby {
void move_client_to_lobby(std::shared_ptr<Lobby> dest_lobby,
std::shared_ptr<Client> c);
std::shared_ptr<Client> find_client(const char16_t* identifier = nullptr,
std::shared_ptr<Client> find_client(
const std::u16string* identifier = nullptr,
uint64_t serial_number = 0);
void add_item(const PlayerInventoryItem& item);
void remove_item(uint32_t item_id, PlayerInventoryItem* item);
void add_item(const PlayerInventoryItem& item, uint8_t area, float x, float z);
PlayerInventoryItem remove_item(uint32_t item_id);
size_t find_item(uint32_t item_id);
uint32_t generate_item_id(uint8_t client_id);
void assign_item_ids_for_player(uint32_t client_id, PlayerInventory& inv);
static uint8_t game_event_for_lobby_event(uint8_t lobby_event);
};
+221 -113
View File
@@ -19,7 +19,6 @@
#include "FileContentsCache.hh"
#include "Text.hh"
#include "ServerShell.hh"
#include "ProxyShell.hh"
#include "IPStackSimulator.hh"
using namespace std;
@@ -27,33 +26,23 @@ using namespace std;
FileContentsCache file_cache;
bool use_terminal_colors = false;
static const unordered_map<string, PortConfiguration> default_port_to_behavior({
{"gc-jp10", {9000, GameVersion::GC, ServerBehavior::LoginServer}},
{"gc-jp11", {9001, GameVersion::GC, ServerBehavior::LoginServer}},
{"gc-jp3", {9003, GameVersion::GC, ServerBehavior::LoginServer}},
{"gc-us10", {9100, GameVersion::PC, ServerBehavior::SplitReconnect}},
{"gc-us3", {9103, GameVersion::GC, ServerBehavior::LoginServer}},
{"gc-eu10", {9200, GameVersion::GC, ServerBehavior::LoginServer}},
{"gc-eu11", {9201, GameVersion::GC, ServerBehavior::LoginServer}},
{"gc-eu3", {9203, GameVersion::GC, ServerBehavior::LoginServer}},
{"pc-login", {9300, GameVersion::PC, ServerBehavior::LoginServer}},
{"pc-patch", {10000, GameVersion::Patch, ServerBehavior::PatchServer}},
{"bb-patch", {11000, GameVersion::Patch, ServerBehavior::PatchServer}},
{"bb-data", {12000, GameVersion::BB, ServerBehavior::DataServerBB}},
// these aren't hardcoded in any games; user can override them
{"bb-data1", {12004, GameVersion::BB, ServerBehavior::DataServerBB}},
{"bb-data2", {12005, GameVersion::BB, ServerBehavior::DataServerBB}},
{"bb-login", {12008, GameVersion::BB, ServerBehavior::LoginServer}},
{"pc-lobby", {9420, GameVersion::PC, ServerBehavior::LobbyServer}},
{"gc-lobby", {9421, GameVersion::GC, ServerBehavior::LobbyServer}},
{"bb-lobby", {9422, GameVersion::BB, ServerBehavior::LobbyServer}},
});
static vector<PortConfiguration> parse_port_configuration(
shared_ptr<const JSONObject> json) {
vector<PortConfiguration> ret;
for (const auto& item_json_it : json->as_dict()) {
auto item_list = item_json_it.second->as_list();
PortConfiguration& pc = ret.emplace_back();
pc.name = item_json_it.first;
pc.port = item_list[0]->as_int();
pc.version = version_for_name(item_list[1]->as_string().c_str());
pc.behavior = server_behavior_for_name(item_list[2]->as_string().c_str());
}
return ret;
}
template <typename T>
vector<T> parse_int_vector(shared_ptr<const JSONObject> o) {
@@ -83,8 +72,7 @@ void populate_state_from_config(shared_ptr<ServerState> s,
}
} catch (const out_of_range&) { }
// TODO: make this configurable
s->set_port_configuration(default_port_to_behavior);
s->set_port_configuration(parse_port_configuration(d.at("PortConfiguration")));
auto enemy_categories = parse_int_vector<uint32_t>(d.at("CommonItemDropRates-Enemy"));
auto box_categories = parse_int_vector<uint32_t>(d.at("CommonItemDropRates-Box"));
@@ -95,27 +83,6 @@ void populate_state_from_config(shared_ptr<ServerState> s,
s->common_item_creator.reset(new CommonItemCreator(enemy_categories,
box_categories, unit_types));
shared_ptr<vector<MenuItem>> information_menu(new vector<MenuItem>());
shared_ptr<vector<u16string>> information_contents(new vector<u16string>());
information_menu->emplace_back(INFORMATION_MENU_GO_BACK, u"Go back",
u"Return to the\nmain menu", 0);
uint32_t item_id = 0;
for (const auto& item : d.at("InformationMenuContents")->as_list()) {
auto& v = item->as_list();
information_menu->emplace_back(item_id, decode_sjis(v.at(0)->as_string()),
decode_sjis(v.at(1)->as_string()), MenuItemFlag::RequiresMessageBoxes);
information_contents->emplace_back(decode_sjis(v.at(2)->as_string()));
item_id++;
}
s->information_menu = information_menu;
s->information_contents = information_contents;
try {
s->welcome_message = decode_sjis(d.at("WelcomeMessage")->as_string());
} catch (const out_of_range&) { }
auto local_address_str = d.at("LocalAddress")->as_string();
try {
s->local_address = s->all_addresses.at(local_address_str);
@@ -161,24 +128,27 @@ void populate_state_from_config(shared_ptr<ServerState> s,
s->allow_unregistered_users = true;
}
{
string key_file_name = d.at("BlueBurstKeyFile")->as_string();
string key_file_contents = load_file("system/blueburst/keys/" + key_file_name + ".nsk");
if (key_file_contents.size() != sizeof(PSOBBEncryption::KeyFile)) {
log(WARNING, "Blue Burst key file is the wrong size (%zu bytes; should be %zu bytes)",
key_file_contents.size(), sizeof(PSOBBEncryption::KeyFile));
log(WARNING, "Ignoring key file; Blue Burst clients will not be able to connect");
} else {
memcpy(&s->default_key_file, key_file_contents.data(), sizeof(PSOBBEncryption::KeyFile));
log(INFO, "Loaded Blue Burst key file: %s", key_file_name.c_str());
}
try {
s->item_tracking_enabled = d.at("EnableItemTracking")->as_bool();
} catch (const out_of_range&) {
s->item_tracking_enabled = true;
}
for (const string& filename : list_directory("system/blueburst/keys")) {
if (!ends_with(filename, ".nsk")) {
continue;
}
s->bb_private_keys.emplace_back(new PSOBBEncryption::KeyFile(
load_object_file<PSOBBEncryption::KeyFile>("system/blueburst/keys/" + filename)));
log(INFO, "Loaded Blue Burst key file: %s", filename.c_str());
}
log(INFO, "%zu Blue Burst key file(s) loaded", s->bb_private_keys.size());
try {
bool run_shell = d.at("RunInteractiveShell")->as_bool();
s->run_shell_behavior = run_shell ?
ServerState::RunShellBehavior::Always :
ServerState::RunShellBehavior::Never;
ServerState::RunShellBehavior::ALWAYS :
ServerState::RunShellBehavior::NEVER;
} catch (const out_of_range&) { }
}
@@ -214,26 +184,138 @@ void drop_privileges(const string& username) {
int main(int argc, char* argv[]) {
string proxy_hostname;
int proxy_port = 0;
GameVersion proxy_version = GameVersion::GC;
enum class Behavior {
RUN_SERVER = 0,
DECRYPT_DATA,
ENCRYPT_DATA,
DECODE_QUEST_FILE,
DECODE_SJIS,
};
enum class EncryptionType {
PC = 0,
GC,
BB,
};
enum class QuestFileFormat {
GCI = 0,
DLQ,
QST,
};
int main(int argc, char** argv) {
Behavior behavior = Behavior::RUN_SERVER;
EncryptionType crypt_type = EncryptionType::PC;
QuestFileFormat quest_file_type = QuestFileFormat::GCI;
string quest_filename;
string seed;
string key_file_name;
bool parse_data = false;
for (int x = 1; x < argc; x++) {
if (!strncmp(argv[x], "--proxy-destination=", 20)) {
auto netloc = parse_netloc(&argv[x][20], 9100);
proxy_hostname = netloc.first;
proxy_port = netloc.second;
} else if (!strncmp(argv[x], "--proxy-version=", 16)) {
proxy_version = version_for_name(&argv[x][16]);
if (!strcmp(argv[x], "--decrypt-data")) {
behavior = Behavior::DECRYPT_DATA;
} else if (!strcmp(argv[x], "--encrypt-data")) {
behavior = Behavior::ENCRYPT_DATA;
} else if (!strcmp(argv[x], "--decode-sjis")) {
behavior = Behavior::DECODE_SJIS;
} else if (!strncmp(argv[x], "--decode-gci=", 13)) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::GCI;
quest_filename = &argv[x][13];
} else if (!strncmp(argv[x], "--decode-dlq=", 13)) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::DLQ;
quest_filename = &argv[x][13];
} else if (!strncmp(argv[x], "--decode-qst=", 13)) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::QST;
quest_filename = &argv[x][13];
} else if (!strcmp(argv[x], "--pc")) {
crypt_type = EncryptionType::PC;
} else if (!strcmp(argv[x], "--gc")) {
crypt_type = EncryptionType::GC;
} else if (!strcmp(argv[x], "--bb")) {
crypt_type = EncryptionType::BB;
} else if (!strncmp(argv[x], "--seed=", 7)) {
seed = &argv[x][7];
} else if (!strncmp(argv[x], "--key=", 6)) {
key_file_name = &argv[x][6];
} else if (!strcmp(argv[x], "--parse-data")) {
parse_data = true;
} else {
throw invalid_argument(string_printf("unknown option: %s", argv[x]));
}
}
if (behavior == Behavior::DECRYPT_DATA || behavior == Behavior::ENCRYPT_DATA) {
shared_ptr<PSOEncryption> crypt;
if (crypt_type == EncryptionType::PC) {
crypt.reset(new PSOPCEncryption(stoul(seed, nullptr, 16)));
} else if (crypt_type == EncryptionType::GC) {
crypt.reset(new PSOGCEncryption(stoul(seed, nullptr, 16)));
} else if (crypt_type == EncryptionType::BB) {
seed = parse_data_string(seed);
auto key = load_object_file<PSOBBEncryption::KeyFile>(
"system/blueburst/keys/" + key_file_name + ".nsk");
crypt.reset(new PSOBBEncryption(key, seed.data(), seed.size()));
} else {
throw logic_error("invalid encryption type");
}
string data = read_all(stdin);
if (parse_data) {
data = parse_data_string(data);
}
if (behavior == Behavior::DECRYPT_DATA) {
crypt->decrypt(data.data(), data.size());
} else if (behavior == Behavior::ENCRYPT_DATA) {
crypt->encrypt(data.data(), data.size());
} else {
throw logic_error("invalid behavior");
}
if (isatty(fileno(stdout))) {
print_data(stdout, data);
} else {
fwritex(stdout, data);
}
fflush(stdout);
return 0;
} else if (behavior == Behavior::DECODE_QUEST_FILE) {
if (quest_file_type == QuestFileFormat::GCI) {
save_file(quest_filename + ".dec", Quest::decode_gci(quest_filename));
} else if (quest_file_type == QuestFileFormat::DLQ) {
save_file(quest_filename + ".dec", Quest::decode_dlq(quest_filename));
} else if (quest_file_type == QuestFileFormat::QST) {
auto data = Quest::decode_qst(quest_filename);
save_file(quest_filename + ".bin", data.first);
save_file(quest_filename + ".dat", data.second);
} else {
throw logic_error("invalid quest file format");
}
return 0;
} else if (behavior == Behavior::DECODE_SJIS) {
string data = read_all(stdin);
if (parse_data) {
data = parse_data_string(data);
}
auto decoded = decode_sjis(data);
print_data(stderr, decoded.data(), decoded.size() * sizeof(decoded[0]));
return 0;
}
signal(SIGPIPE, SIG_IGN);
if (isatty(fileno(stderr))) {
use_terminal_colors = true;
}
shared_ptr<ServerState> state(new ServerState());
shared_ptr<struct event_base> base(event_base_new(), event_base_free);
@@ -249,6 +331,30 @@ int main(int argc, char* argv[]) {
auto config_json = JSONObject::parse(load_file("system/config.json"));
populate_state_from_config(state, config_json);
log(INFO, "Loading license list");
state->license_manager.reset(new LicenseManager("system/licenses.nsi"));
log(INFO, "Loading battle parameters");
state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry"));
log(INFO, "Loading level table");
state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
log(INFO, "Collecting Episode 3 data");
state->ep3_data_index.reset(new Ep3DataIndex("system/ep3"));
log(INFO, "Collecting quest metadata");
state->quest_index.reset(new QuestIndex("system/quests"));
log(INFO, "Compiling client functions");
state->function_code_index.reset(new FunctionCodeIndex("system/ppc"));
log(INFO, "Loading DOL files");
state->dol_file_index.reset(new DOLFileIndex("system/dol"));
log(INFO, "Creating menus");
state->create_menus(config_json);
shared_ptr<DNSServer> dns_server;
if (state->dns_server_port) {
log(INFO, "Starting DNS server");
@@ -259,44 +365,50 @@ int main(int argc, char* argv[]) {
log(INFO, "DNS server is disabled");
}
shared_ptr<ProxyServer> proxy_server;
shared_ptr<Server> game_server;
if (proxy_port) {
log(INFO, "Starting proxy server");
sockaddr_storage proxy_destination_ss = make_sockaddr_storage(
proxy_hostname, proxy_port).first;
proxy_server.reset(new ProxyServer(base, proxy_destination_ss,
proxy_version));
proxy_server->listen(proxy_port);
if (proxy_version == GameVersion::BB) {
proxy_server->listen(proxy_port + 1);
log(INFO, "Opening sockets");
for (const auto& it : state->name_to_port_config) {
const auto& pc = it.second;
if (pc->behavior == ServerBehavior::PROXY_SERVER) {
if (!state->proxy_server.get()) {
log(INFO, "Starting proxy server");
state->proxy_server.reset(new ProxyServer(base, state));
}
if (state->proxy_server.get()) {
// For PC and GC, proxy sessions are dynamically created when a client
// picks a destination from the menu. For patch and BB clients, there's
// no way to ask the client which destination they want, so only one
// destination is supported, and we have to manually specify the
// destination netloc here.
if (pc->version == GameVersion::PATCH) {
struct sockaddr_storage ss = make_sockaddr_storage(
state->proxy_destination_patch.first,
state->proxy_destination_patch.second).first;
state->proxy_server->listen(pc->port, pc->version, &ss);
} else if (pc->version == GameVersion::BB) {
struct sockaddr_storage ss = make_sockaddr_storage(
state->proxy_destination_bb.first,
state->proxy_destination_bb.second).first;
state->proxy_server->listen(pc->port, pc->version, &ss);
} else {
state->proxy_server->listen(pc->port, pc->version);
}
}
} else {
if (!state->game_server.get()) {
log(INFO, "Starting game server");
state->game_server.reset(new Server(base, state));
}
string name = string_printf("%s (%s, %s) on port %hu",
pc->name.c_str(), name_for_version(pc->version),
name_for_server_behavior(pc->behavior), pc->port);
state->game_server->listen(name, "", pc->port, pc->version, pc->behavior);
}
} else {
log(INFO, "Starting game server");
game_server.reset(new Server(base, state));
for (const auto& it : state->named_port_configuration) {
game_server->listen("", it.second.port, it.second.version, it.second.behavior);
}
log(INFO, "Loading license list");
state->license_manager.reset(new LicenseManager("system/licenses.nsi"));
log(INFO, "Loading battle parameters");
state->battle_params.reset(new BattleParamTable("system/blueburst/BattleParamEntry"));
log(INFO, "Loading level table");
state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true));
log(INFO, "Collecting quest metadata");
state->quest_index.reset(new QuestIndex("system/quests"));
}
shared_ptr<IPStackSimulator> ip_stack_simulator;
if (!state->ip_stack_addresses.empty()) {
log(INFO, "Starting IP stack simulator");
ip_stack_simulator.reset(new IPStackSimulator(
base, game_server, proxy_server, state));
ip_stack_simulator.reset(new IPStackSimulator(base, state));
for (const auto& it : state->ip_stack_addresses) {
auto netloc = parse_netloc(it);
ip_stack_simulator->listen(netloc.first, netloc.second);
@@ -308,24 +420,20 @@ int main(int argc, char* argv[]) {
drop_privileges(state->username);
}
bool should_run_shell = (state->run_shell_behavior == ServerState::RunShellBehavior::Always);
if (state->run_shell_behavior == ServerState::RunShellBehavior::Default) {
bool should_run_shell = (state->run_shell_behavior == ServerState::RunShellBehavior::ALWAYS);
if (state->run_shell_behavior == ServerState::RunShellBehavior::DEFAULT) {
should_run_shell = isatty(fileno(stdin));
}
shared_ptr<Shell> shell;
if (should_run_shell) {
log(INFO, "Starting interactive shell");
if (proxy_port) {
shell.reset(new ProxyShell(base, state, proxy_server));
} else {
shell.reset(new ServerShell(base, state));
}
shell.reset(new ServerShell(base, state));
}
log(INFO, "Ready");
event_base_dispatch(base.get());
log(INFO, "Normal shutdown");
state->proxy_server.reset(); // Break reference cycle
return 0;
}
+2 -2
View File
@@ -74,7 +74,7 @@ struct EnemyEntry {
uint32_t reserved14;
uint32_t skin;
uint32_t reserved15;
};
} __attribute__((packed));
static vector<PSOEnemy> parse_map(uint8_t episode, uint8_t difficulty,
const BattleParams* battle_params, const EnemyEntry* map,
@@ -433,7 +433,7 @@ static vector<PSOEnemy> parse_map(uint8_t episode, uint8_t difficulty,
return enemies;
}
vector<PSOEnemy> load_map(const char* filename, uint8_t episode,
vector<PSOEnemy> load_map(const std::string& filename, uint8_t episode,
uint8_t difficulty, const BattleParams* battle_params, bool alt_enemies) {
shared_ptr<const string> data = file_cache.get(filename);
const EnemyEntry* entries = reinterpret_cast<const EnemyEntry*>(data->data());
+8 -7
View File
@@ -3,6 +3,7 @@
#include <inttypes.h>
#include <vector>
#include <string>
@@ -10,14 +11,14 @@ struct BattleParams {
uint16_t atp; // attack power
uint16_t psv; // perseverance (intelligence?)
uint16_t evp; // evasion
uint16_t hp; // hit points
uint16_t hp; // hit points
uint16_t dfp; // defense
uint16_t ata; // accuracy
uint16_t lck; // luck
uint8_t unknown[14];
uint8_t unknown_a1[0x0E];
uint32_t experience;
uint32_t difficulty;
};
} __attribute__((packed));
struct BattleParamTable {
BattleParams entries[2][3][4][0x60]; // online/offline, episode, difficulty, monster type
@@ -28,13 +29,13 @@ struct BattleParamTable {
uint8_t monster_type) const;
const BattleParams* get_subtable(bool solo, uint8_t episode,
uint8_t difficulty) const;
};
} __attribute__((packed));
struct BattleParamIndex {
BattleParamTable table_for_episode[3];
};
} __attribute__((packed));
// an enemy entry as loaded by the game
struct PSOEnemy {
@@ -46,7 +47,7 @@ struct PSOEnemy {
PSOEnemy();
PSOEnemy(uint32_t experience, uint32_t rt_index);
};
} __attribute__((packed));
std::vector<PSOEnemy> load_map(const char* filename, uint8_t episode,
std::vector<PSOEnemy> load_map(const std::string& filename, uint8_t episode,
uint8_t difficulty, const BattleParams* bp, bool alt_enemies);
+59 -7
View File
@@ -6,16 +6,68 @@
enum MenuItemFlag {
InvisibleOnDC = 0x01,
InvisibleOnPC = 0x02,
InvisibleOnGC = 0x04,
InvisibleOnGCEpisode3 = 0x08,
InvisibleOnBB = 0x10,
RequiresMessageBoxes = 0x20,
// Note: These aren't enums because neither enum nor enum class does what we
// want. Specifically, we need GO_BACK to be valid in multiple enums (and enums
// aren't namespaced unless they're enum classes), so we can't use enums. But we
// also want to be able to use non-enum values in switch statements without
// casting values all over the place, so we can't use enum classes either.
namespace MenuID {
constexpr uint32_t MAIN = 0x11000011;
constexpr uint32_t INFORMATION = 0x22000022;
constexpr uint32_t LOBBY = 0x33000033;
constexpr uint32_t GAME = 0x44000044;
constexpr uint32_t QUEST = 0x55000055;
constexpr uint32_t QUEST_FILTER = 0x66000066;
constexpr uint32_t PROXY_DESTINATIONS = 0x77000077;
constexpr uint32_t PROGRAMS = 0x88000088;
constexpr uint32_t PATCHES = 0x99000099;
}
namespace MainMenuItemID {
constexpr uint32_t GO_TO_LOBBY = 0x11222211;
constexpr uint32_t INFORMATION = 0x11333311;
constexpr uint32_t DOWNLOAD_QUESTS = 0x11444411;
constexpr uint32_t PROXY_DESTINATIONS = 0x11555511;
constexpr uint32_t PATCHES = 0x11666611;
constexpr uint32_t PROGRAMS = 0x11777711;
constexpr uint32_t DISCONNECT = 0x11888811;
constexpr uint32_t CLEAR_LICENSE = 0x11999911;
}
namespace InformationMenuItemID {
constexpr uint32_t GO_BACK = 0x22FFFF22;
};
namespace ProxyDestinationsMenuItemID {
constexpr uint32_t GO_BACK = 0x77FFFF77;
};
namespace ProgramsMenuItemID {
constexpr uint32_t GO_BACK = 0x88FFFF88;
};
namespace PatchesMenuItemID {
constexpr uint32_t GO_BACK = 0x99FFFF99;
};
struct MenuItem {
enum Flag {
INVISIBLE_ON_DC = 0x01,
INVISIBLE_ON_PC = 0x02,
INVISIBLE_ON_GC = 0x04,
INVISIBLE_ON_BB = 0x08,
DC_ONLY = INVISIBLE_ON_PC | INVISIBLE_ON_GC | INVISIBLE_ON_BB,
PC_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_GC | INVISIBLE_ON_BB,
GC_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_BB,
BB_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_GC,
REQUIRES_MESSAGE_BOXES = 0x10,
REQUIRES_SEND_FUNCTION_CALL = 0x20,
REQUIRES_SAVE_DISABLED = 0x40,
};
uint32_t item_id;
std::u16string name;
std::u16string description;
+545 -259
View File
@@ -16,9 +16,9 @@ using namespace std;
// most ciphers used by pso are symmetric; alias decrypt to encrypt by default
void PSOEncryption::decrypt(void* data, size_t size) {
this->encrypt(data, size);
// Most ciphers used by PSO are symmetric; alias decrypt to encrypt by default
void PSOEncryption::decrypt(void* data, size_t size, bool advance) {
this->encrypt(data, size, advance);
}
@@ -30,8 +30,7 @@ void PSOPCEncryption::update_stream() {
eax = edi;
while (edx > 0) {
esi = this->stream[eax + 0x1F];
ebp = this->stream[eax];
ebp = ebp - esi;
ebp = this->stream[eax] - esi;
this->stream[eax] = ebp;
eax++;
edx--;
@@ -41,8 +40,7 @@ void PSOPCEncryption::update_stream() {
eax = edi;
while (edx > 0) {
esi = this->stream[eax - 0x18];
ebp = this->stream[eax];
ebp = ebp - esi;
ebp = this->stream[eax] - esi;
this->stream[eax] = ebp;
eax++;
edx--;
@@ -71,23 +69,30 @@ PSOPCEncryption::PSOPCEncryption(uint32_t seed) : offset(1) {
}
}
uint32_t PSOPCEncryption::next() {
uint32_t PSOPCEncryption::next(bool advance) {
if (this->offset == PC_STREAM_LENGTH) {
this->update_stream();
this->offset = 1;
}
return this->stream[this->offset++];
uint32_t ret = this->stream[this->offset];
if (advance) {
this->offset++;
}
return ret;
}
void PSOPCEncryption::encrypt(void* vdata, size_t size) {
void PSOPCEncryption::encrypt(void* vdata, size_t size, bool advance) {
if (size & 3) {
throw invalid_argument("size must be a multiple of 4");
}
if (!advance && (size != 4)) {
throw logic_error("cannot peek-encrypt/decrypt with size > 4");
}
size >>= 2;
uint32_t* data = reinterpret_cast<uint32_t*>(vdata);
le_uint32_t* data = reinterpret_cast<le_uint32_t*>(vdata);
for (size_t x = 0; x < size; x++) {
data[x] ^= this->next();
data[x] ^= this->next(advance);
}
}
@@ -110,12 +115,15 @@ void PSOGCEncryption::update_stream() {
this->offset = 0;
}
uint32_t PSOGCEncryption::next() {
this->offset++;
uint32_t PSOGCEncryption::next(bool advance) {
if (this->offset == GC_STREAM_LENGTH) {
this->update_stream();
}
return this->stream[this->offset];
uint32_t ret = this->stream[this->offset];
if (advance) {
this->offset++;
}
return ret;
}
PSOGCEncryption::PSOGCEncryption(uint32_t seed) : offset(0) {
@@ -145,102 +153,248 @@ PSOGCEncryption::PSOGCEncryption(uint32_t seed) : offset(0) {
this->stream[this->offset++] = (this->stream[source3++] ^ (((this->stream[source1++] << 23) & 0xFF800000) ^ ((this->stream[source2++] >> 9) & 0x007FFFFF)));
}
for (size_t x = 0; x < 3; x++) {
for (size_t x = 0; x < 4; x++) {
this->update_stream();
}
this->offset = GC_STREAM_LENGTH - 1;
}
void PSOGCEncryption::encrypt(void* vdata, size_t size) {
void PSOGCEncryption::encrypt(void* vdata, size_t size, bool advance) {
if (size & 3) {
throw invalid_argument("size must be a multiple of 4");
}
if (!advance && (size != 4)) {
throw logic_error("cannot peek-encrypt/decrypt with size > 4");
}
size >>= 2;
uint32_t* data = reinterpret_cast<uint32_t*>(vdata);
le_uint32_t* data = reinterpret_cast<le_uint32_t*>(vdata);
for (size_t x = 0; x < size; x++) {
data[x] ^= this->next();
data[x] ^= this->next(advance);
}
}
void PSOBBEncryption::decrypt(void* vdata, size_t size) {
if (size & 7) {
throw invalid_argument("size must be a multiple of 8");
}
size >>= 3;
void PSOBBEncryption::decrypt(void* vdata, size_t size, bool advance) {
if (this->state.subtype == Subtype::TFS1) {
if (size & 7) {
throw invalid_argument("size must be a multiple of 8");
}
uint32_t* data = reinterpret_cast<uint32_t*>(vdata);
uint32_t edx, ebx, ebp, esi, edi;
le_uint32_t* dwords = reinterpret_cast<le_uint32_t*>(vdata);
for (size_t x = 0; x < (size >> 2); x += 2) {
for (size_t y = 4; y > 0; y -= 2) {
dwords[x] = dwords[x] ^ this->state.initial_keys.as32[y + 1];
dwords[x + 1] ^= ((this->state.private_keys.as32[dwords[x] >> 24] +
this->state.private_keys.as32[((dwords[x] >> 16) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((dwords[x] >> 8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(dwords[x] & 0xFF) + 0x300];
dwords[x + 1] ^= this->state.initial_keys.as32[y];
dwords[x] ^= ((this->state.private_keys.as32[dwords[x + 1] >> 24] +
this->state.private_keys.as32[((dwords[x + 1] >> 16) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((dwords[x + 1] >> 8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(dwords[x + 1] & 0xFF) + 0x300];
}
dwords[x] ^= this->state.initial_keys.as32[1];
dwords[x + 1] ^= this->state.initial_keys.as32[0];
edx = 0;
while (edx < size) {
ebx = data[edx];
ebx = ebx ^ this->stream[5];
ebp = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112])
^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312];
ebp = ebp ^ this->stream[4];
ebp ^= data[edx + 1];
edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112])
^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312];
edi = edi ^ this->stream[3];
ebx = ebx ^ edi;
esi = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112])
^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312];
ebp = ebp ^ esi ^ this->stream[2];
edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112])
^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312];
edi = edi ^ this->stream[1];
ebp = ebp ^ this->stream[0];
ebx = ebx ^ edi;
data[edx] = ebp;
data[edx + 1] = ebx;
edx += 2;
uint32_t a = dwords[x];
dwords[x] = dwords[x + 1];
dwords[x + 1] = a;
}
} else if (this->state.subtype == Subtype::JSD1) {
if (size & 1) {
throw invalid_argument("size must be a multiple of 2");
}
if (!advance && (size > 0x100)) {
throw logic_error("JSD1 can only peek-decrypt up to 0x100 bytes");
}
uint8_t* bytes = reinterpret_cast<uint8_t*>(vdata);
for (size_t z = 0; z < size; z += 2) {
uint8_t a = bytes[z];
uint8_t b = bytes[z + 1];
bytes[z] = (a & 0x55) | (b & 0xAA);
bytes[z + 1] = (a & 0xAA) | (b & 0x55);
}
for (size_t z = 0; z < size; z++) {
bytes[z] ^= this->state.private_keys.as8[this->state.initial_keys.jsd1_stream_offset];
if (advance) {
this->state.private_keys.as8[this->state.initial_keys.jsd1_stream_offset] -= bytes[z];
}
this->state.initial_keys.jsd1_stream_offset++;
}
if (!advance) {
this->state.initial_keys.jsd1_stream_offset -= size;
}
} else { // STANDARD or MOCB1
if (size & 7) {
throw invalid_argument("size must be a multiple of 8");
}
size_t num_dwords = size >> 2;
le_uint32_t* dwords = reinterpret_cast<le_uint32_t*>(vdata);
uint32_t edx, ebx, ebp, esi, edi;
edx = 0;
while (edx < num_dwords) {
ebx = dwords[edx];
ebx = ebx ^ this->state.initial_keys.as32[5];
ebp = ((this->state.private_keys.as32[(ebx >> 0x18)] +
this->state.private_keys.as32[((ebx >> 0x10) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((ebx >> 0x8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(ebx & 0xFF) + 0x300];
ebp = ebp ^ this->state.initial_keys.as32[4];
ebp ^= dwords[edx + 1];
edi = ((this->state.private_keys.as32[(ebp >> 0x18)] +
this->state.private_keys.as32[((ebp >> 0x10) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((ebp >> 0x8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(ebp & 0xFF) + 0x300];
edi = edi ^ this->state.initial_keys.as32[3];
ebx = ebx ^ edi;
esi = ((this->state.private_keys.as32[(ebx >> 0x18)] +
this->state.private_keys.as32[((ebx >> 0x10) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((ebx >> 0x8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(ebx & 0xFF) + 0x300];
ebp = ebp ^ esi ^ this->state.initial_keys.as32[2];
edi = ((this->state.private_keys.as32[(ebp >> 0x18)] +
this->state.private_keys.as32[((ebp >> 0x10) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((ebp >> 0x8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(ebp & 0xFF) + 0x300];
edi = edi ^ this->state.initial_keys.as32[1];
ebp = ebp ^ this->state.initial_keys.as32[0];
ebx = ebx ^ edi;
dwords[edx] = ebp;
dwords[edx + 1] = ebx;
edx += 2;
}
}
}
void PSOBBEncryption::encrypt(void* vdata, size_t size) {
if (size & 7) {
throw invalid_argument("size must be a multiple of 8");
}
size >>= 3;
void PSOBBEncryption::encrypt(void* vdata, size_t size, bool advance) {
if (this->state.subtype == Subtype::TFS1) {
if (size & 7) {
throw invalid_argument("size must be a multiple of 8");
}
uint8_t* data = reinterpret_cast<uint8_t*>(vdata);
uint32_t edx, ebx, ebp, esi, edi;
le_uint32_t* dwords = reinterpret_cast<le_uint32_t*>(vdata);
for (size_t x = 0; x < (size >> 2); x += 2) {
for (size_t y = 0; y < 4; y += 2) {
dwords[x] ^= this->state.initial_keys.as32[y];
dwords[x + 1] ^= ((this->state.private_keys.as32[dwords[x] >> 24] +
this->state.private_keys.as32[((dwords[x] >> 16) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((dwords[x] >> 8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(dwords[x] & 0xFF) + 0x300];
dwords[x + 1] ^= this->state.initial_keys.as32[y + 1];
dwords[x] ^= ((this->state.private_keys.as32[dwords[x + 1] >> 24] +
this->state.private_keys.as32[(dwords[x + 1] >> 16 & 0xFF) + 0x100]) ^
this->state.private_keys.as32[(dwords[x + 1] >> 8 & 0xFF) + 0x200]) +
this->state.private_keys.as32[(dwords[x + 1] & 0xFF) + 0x300];
}
dwords[x] ^= this->state.initial_keys.as32[4];
dwords[x + 1] ^= this->state.initial_keys.as32[5];
edx = 0;
while (edx < size) {
ebx = data[edx];
ebx = ebx ^ this->stream[0];
ebp = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112])
^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312];
ebp = ebp ^ this->stream[1];
ebp ^= data[edx + 1];
edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112])
^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312];
edi = edi ^ this->stream[2];
ebx = ebx ^ edi;
esi = ((this->stream[(ebx >> 0x18) + 0x12]+this->stream[((ebx >> 0x10)& 0xff) + 0x112])
^ this->stream[((ebx >> 0x8)& 0xff) + 0x212]) + this->stream[(ebx & 0xff) + 0x312];
ebp = ebp ^ esi ^ this->stream[3];
edi = ((this->stream[(ebp >> 0x18) + 0x12]+this->stream[((ebp >> 0x10)& 0xff) + 0x112])
^ this->stream[((ebp >> 0x8)& 0xff) + 0x212]) + this->stream[(ebp & 0xff) + 0x312];
edi = edi ^ this->stream[4];
ebp = ebp ^ this->stream[5];
ebx = ebx ^ edi;
data[edx] = ebp;
data[edx + 1] = ebx;
edx += 2;
uint32_t a = dwords[x];
dwords[x] = dwords[x + 1];
dwords[x + 1] = a;
}
} else if (this->state.subtype == Subtype::JSD1) {
if (size & 1) {
throw invalid_argument("size must be a multiple of 2");
}
if (!advance && (size > 0x100)) {
throw logic_error("JSD1 can only peek-encrypt up to 0x100 bytes");
}
uint8_t* bytes = reinterpret_cast<uint8_t*>(vdata);
for (size_t z = 0; z < size; z++) {
uint8_t v = bytes[z];
bytes[z] = v ^ this->state.private_keys.as8[this->state.initial_keys.jsd1_stream_offset];
if (advance) {
this->state.private_keys.as8[this->state.initial_keys.jsd1_stream_offset] -= v;
}
this->state.initial_keys.jsd1_stream_offset++;
}
if (!advance) {
this->state.initial_keys.jsd1_stream_offset -= size;
}
for (size_t z = 0; z < size; z += 2) {
uint8_t a = bytes[z];
uint8_t b = bytes[z + 1];
bytes[z] = (a & 0x55) | (b & 0xAA);
bytes[z + 1] = (a & 0xAA) | (b & 0x55);
}
} else { // STANDARD or MOCB1
if (size & 7) {
throw invalid_argument("size must be a multiple of 8");
}
size_t num_dwords = size >> 2;
le_uint32_t* data = reinterpret_cast<le_uint32_t*>(vdata);
uint32_t edx, ebx, ebp, esi, edi;
edx = 0;
while (edx < num_dwords) {
ebx = data[edx] ^ this->state.initial_keys.as32[0];
ebp = ((this->state.private_keys.as32[(ebx >> 0x18)] +
this->state.private_keys.as32[((ebx >> 0x10) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((ebx >> 0x8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(ebx & 0xFF) + 0x300];
ebp = ebp ^ this->state.initial_keys.as32[1];
ebp ^= data[edx + 1];
edi = ((this->state.private_keys.as32[(ebp >> 0x18)] +
this->state.private_keys.as32[((ebp >> 0x10) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((ebp >> 0x8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(ebp & 0xFF) + 0x300];
edi = edi ^ this->state.initial_keys.as32[2];
ebx = ebx ^ edi;
esi = ((this->state.private_keys.as32[(ebx >> 0x18)] +
this->state.private_keys.as32[((ebx >> 0x10) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((ebx >> 0x8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(ebx & 0xFF) + 0x300];
ebp = ebp ^ esi ^ this->state.initial_keys.as32[3];
edi = ((this->state.private_keys.as32[(ebp >> 0x18)] +
this->state.private_keys.as32[((ebp >> 0x10) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((ebp >> 0x8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(ebp & 0xFF) + 0x300];
edi = edi ^ this->state.initial_keys.as32[4];
ebp = ebp ^ this->state.initial_keys.as32[5];
ebx = ebx ^ edi;
data[edx] = ebp;
data[edx + 1] = ebx;
edx += 2;
}
}
}
PSOBBEncryption::PSOBBEncryption(const KeyFile& key, const void* original_seed,
size_t seed_size) : offset(0) {
if (seed_size % 3) {
throw invalid_argument("seed size must be divisible by 3");
}
PSOBBEncryption::PSOBBEncryption(
const KeyFile& key, const void* original_seed, size_t seed_size)
: state(key) {
this->apply_seed(original_seed, seed_size);
}
void PSOBBEncryption::tfs1_scramble(uint32_t* out1, uint32_t* out2) const {
uint32_t a = *out1;
uint32_t b = *out2;
for (size_t x = 0; x < 0x10; x += 2) {
a ^= this->state.initial_keys.as32[x];
b ^= (((this->state.private_keys.as32[a >> 24] +
this->state.private_keys.as32[((a >> 16) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((a >> 8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(a & 0xFF) + 0x300]) ^ this->state.initial_keys.as32[x + 1];
a ^= ((this->state.private_keys.as32[b >> 24] +
this->state.private_keys.as32[((b >> 16) & 0xFF) + 0x100]) ^
this->state.private_keys.as32[((b >> 8) & 0xFF) + 0x200]) +
this->state.private_keys.as32[(b & 0xFF) + 0x300];
}
*out1 = this->state.initial_keys.as32[0x11] ^ b;
*out2 = this->state.initial_keys.as32[0x10] ^ a;
}
void PSOBBEncryption::apply_seed(const void* original_seed, size_t seed_size) {
// Note: This part is done in the 03 command handler in the BB client, and
// isn't actually part of the encryption library. (Why did they do this?)
string seed;
const uint8_t* original_seed_data = reinterpret_cast<const uint8_t*>(
original_seed);
@@ -250,194 +404,326 @@ PSOBBEncryption::PSOBBEncryption(const KeyFile& key, const void* original_seed,
seed.push_back(original_seed_data[x + 2] ^ 0x18);
}
memcpy(this->stream, &key, sizeof(key));
if (this->state.subtype == Subtype::TFS1) {
for (size_t x = 0; x < 0x12; x++) {
uint32_t a = this->state.initial_keys.as32[x] & 0xFFFF;
this->state.initial_keys.as32[x] = ((a << 0x10) ^ (this->state.initial_keys.as32[x] & 0xFFFF0000)) + a;
};
this->postprocess_initial_stream(seed);
}
void PSOBBEncryption::postprocess_initial_stream(const string& seed) {
uint32_t eax, ecx, edx, ebx, ebp, esi, edi, ou, x;
ecx = 0;
ebx = 0;
while (ebx < (seed.size() / 4)) {
ebp = static_cast<uint32_t>(seed[ecx]) << 0x18;
eax = ecx + 1;
edx = eax % seed.size();
eax = (static_cast<uint32_t>(seed[edx]) << 0x10) & 0xFF0000;
ebp = (ebp | eax) & 0xffff00ff;
eax = ecx + 2;
edx = eax % seed.size();
eax = (static_cast<uint32_t>(seed[edx]) << 0x08) & 0xFF00;
ebp = (ebp | eax) & 0xffffff00;
eax = ecx + 3;
ecx = ecx + 4;
edx = eax % seed.size();
eax = static_cast<uint32_t>(seed[edx]);
ebp = ebp | eax;
eax = ecx;
edx = eax % seed.size();
this->stream[ebx] ^= ebp;
ecx = edx;
ebx++;
}
ebp = 0;
esi = 0;
ecx = 0;
edi = 0;
ebx = 0;
edx = 0x48;
while (edi < edx) {
esi = esi ^ this->stream[0];
eax = esi >> 0x18;
ebx = (esi >> 0x10) & 0xff;
eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112];
ebx = (esi >> 8) & 0xFF;
eax = eax ^ this->stream[ebx + 0x212];
ebx = esi & 0xFF;
eax = eax + this->stream[ebx + 0x312];
eax = eax ^ this->stream[1];
ecx = ecx ^ eax;
ebx = ecx >> 0x18;
eax = (ecx >> 0x10) & 0xFF;
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
eax = (ecx >> 8) & 0xFF;
ebx = ebx ^ this->stream[eax + 0x212];
eax = ecx & 0xFF;
ebx = ebx + this->stream[eax + 0x312];
for (x = 0; x <= 5; x++) {
ebx = ebx ^ this->stream[(x * 2) + 2];
esi = esi ^ ebx;
ebx = esi >> 0x18;
eax = (esi >> 0x10) & 0xFF;
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
eax = (esi >> 8) & 0xFF;
ebx = ebx ^ this->stream[eax + 0x212];
eax = esi & 0xFF;
ebx = ebx + this->stream[eax + 0x312];
ebx = ebx ^ this->stream[(x * 2) + 3];
ecx = ecx ^ ebx;
ebx = ecx >> 0x18;
eax = (ecx >> 0x10) & 0xFF;
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
eax = (ecx >> 8) & 0xFF;
ebx = ebx ^ this->stream[eax + 0x212];
eax = ecx & 0xff;
ebx = ebx + this->stream[eax + 0x312];
const uint8_t* useed = reinterpret_cast<const uint8_t*>(seed.data());
for (size_t x = 0; x < 0x48; x += 4) {
uint32_t seed_data =
(useed[x % seed_size] << 24) |
(useed[(x + 1) % seed_size] << 16) |
(useed[(x + 2) % seed_size] << 8) |
useed[(x + 3) % seed_size];
this->state.initial_keys.as32[x >> 2] ^= seed_data;
}
ebx = ebx ^ this->stream[14];
esi = esi ^ ebx;
eax = esi >> 0x18;
ebx = (esi >> 0x10) & 0xFF;
eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112];
ebx = (esi >> 8) & 0xff;
eax = eax ^ this->stream[ebx + 0x212];
ebx = esi & 0xff;
eax = eax + this->stream[ebx + 0x312];
uint32_t a = 0, b = 0;
for (size_t x = 0; x < 0x12; x += 2) {
this->tfs1_scramble(&a, &b);
this->state.initial_keys.as32[x] = a;
this->state.initial_keys.as32[x + 1] = b;
}
eax = eax ^ this->stream[15];
eax = ecx ^ eax;
ecx = eax >> 0x18;
ebx = (eax >> 0x10) & 0xFF;
ecx = this->stream[ecx + 0x12] + this->stream[ebx + 0x112];
ebx = (eax >> 8) & 0xFF;
ecx = ecx ^ this->stream[ebx + 0x212];
ebx = eax & 0xFF;
ecx = ecx + this->stream[ebx + 0x312];
for (size_t x = 0; x < 0x400; x += 2) {
this->tfs1_scramble(&a, &b);
this->state.private_keys.as32[x] = a;
this->state.private_keys.as32[x + 1] = b;
}
ecx = ecx ^ this->stream[16];
ecx = ecx ^ esi;
esi = this->stream[17];
esi = esi ^ eax;
this->stream[(edi / 4)] = esi;
this->stream[(edi / 4)+1] = ecx;
edi = edi + 8;
}
} else if (this->state.subtype == Subtype::JSD1) {
size_t seed_offset = 0;
for (size_t z = 0; z < 0x100; z++) {
this->state.private_keys.as8[z] = (z + seed[seed_offset]) ^ (static_cast<uint8_t>(seed[seed_offset]) >> 1);
seed_offset = (seed_offset + 1) % seed.size();
}
eax = 0;
edx = 0;
ou = 0;
while (ou < 0x1000) {
edi = 0x48;
edx = 0x448;
} else { // STANDARD or MOCB1 (they share most of their logic)
if (seed_size % 3) {
throw invalid_argument("seed size must be divisible by 3");
}
while (edi < edx) {
esi = esi ^ this->stream[0];
eax = esi >> 0x18;
ebx = (esi >> 0x10) & 0xff;
eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112];
ebx = (esi >> 8) & 0xFF;
eax = eax ^ this->stream[ebx + 0x212];
ebx = esi & 0xFF;
eax = eax + this->stream[ebx + 0x312];
if (this->state.subtype == Subtype::MOCB1) {
for (size_t x = 0; x < 0x12; x++) {
uint8_t a = this->state.initial_keys.as8[4 * x + 0];
uint8_t b = this->state.initial_keys.as8[4 * x + 1];
uint8_t c = this->state.initial_keys.as8[4 * x + 2];
uint8_t d = this->state.initial_keys.as8[4 * x + 3];
this->state.initial_keys.as32[x] = ((a ^ d) << 24) | ((b ^ c) << 16) | (a << 8) | b;
}
}
eax = eax ^ this->stream[1];
ecx = ecx ^ eax;
ebx = ecx >> 0x18;
eax = (ecx >> 0x10) & 0xFF;
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
eax = (ecx >> 8) & 0xFF;
ebx = ebx ^ this->stream[eax + 0x212];
eax = ecx & 0xFF;
ebx = ebx + this->stream[eax + 0x312];
// This block was formerly postprocess_initial_stream
{
uint32_t eax, ecx, edx, ebx, ebp, esi, edi, ou, x;
for (x = 0; x <= 5; x++) {
ebx = ebx ^ this->stream[(x * 2) + 2];
esi = esi ^ ebx;
ebx = esi >> 0x18;
eax = (esi >> 0x10) & 0xFF;
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
eax = (esi >> 8) & 0xFF;
ebx = ebx ^ this->stream[eax + 0x212];
eax = esi & 0xFF;
ebx = ebx + this->stream[eax + 0x312];
ecx = 0;
ebx = 0;
ebx = ebx ^ this->stream[(x * 2) + 3];
ecx = ecx ^ ebx;
ebx = ecx >> 0x18;
eax = (ecx >> 0x10) & 0xFF;
ebx = this->stream[ebx + 0x12] + this->stream[eax + 0x112];
eax = (ecx >> 8) & 0xFF;
ebx = ebx ^ this->stream[eax + 0x212];
eax = ecx & 0xFF;
ebx = ebx + this->stream[eax + 0x312];
while (ebx < 0x12) {
ebp = static_cast<uint32_t>(seed[ecx]) << 0x18;
eax = ecx + 1;
edx = eax % seed.size();
eax = (static_cast<uint32_t>(seed[edx]) << 0x10) & 0x00FF0000;
ebp = (ebp | eax) & 0xFFFF00FF;
eax = ecx + 2;
edx = eax % seed.size();
eax = (static_cast<uint32_t>(seed[edx]) << 0x08) & 0x0000FF00;
ebp = (ebp | eax) & 0xFFFFFF00;
eax = ecx + 3;
ecx = ecx + 4;
edx = eax % seed.size();
eax = static_cast<uint32_t>(seed[edx]) & 0x000000FF;
ebp = ebp | eax;
eax = ecx;
edx = eax % seed.size();
this->state.initial_keys.as32[ebx] ^= ebp;
ecx = edx;
ebx++;
}
ebx = ebx ^ this->stream[14];
esi = esi ^ ebx;
eax = esi >> 0x18;
ebx = (esi >> 0x10) & 0xFF;
eax = this->stream[eax + 0x12] + this->stream[ebx + 0x112];
ebx = (esi >> 8) & 0xFF;
eax = eax ^ this->stream[ebx + 0x212];
ebx = esi & 0xFF;
eax = eax + this->stream[ebx + 0x312];
ebp = 0;
esi = 0;
ecx = 0;
edi = 0;
ebx = 0;
edx = 0x48;
eax = eax ^ this->stream[15];
eax = ecx ^ eax;
ecx = eax >> 0x18;
ebx = (eax >> 0x10) & 0xFF;
ecx = this->stream[ecx + 0x12] + this->stream[ebx + 0x112];
ebx = (eax >> 8) & 0xFF;
ecx = ecx ^ this->stream[ebx + 0x212];
ebx = eax & 0xFF;
ecx = ecx + this->stream[ebx + 0x312];
while (edi < edx) {
esi = esi ^ this->state.initial_keys.as32[0];
eax = esi >> 0x18;
ebx = (esi >> 0x10) & 0xFF;
eax = this->state.private_keys.as32[eax] + this->state.private_keys.as32[ebx + 0x100];
ebx = (esi >> 8) & 0xFF;
eax = eax ^ this->state.private_keys.as32[ebx + 0x200];
ebx = esi & 0xFF;
eax = eax + this->state.private_keys.as32[ebx + 0x300];
ecx = ecx ^ this->stream[16];
ecx = ecx ^ esi;
esi = this->stream[17];
esi = esi ^ eax;
this->stream[(ou / 4) + (edi / 4)] = esi;
this->stream[(ou / 4) + (edi / 4) + 1] = ecx;
edi = edi + 8;
eax = eax ^ this->state.initial_keys.as32[1];
ecx = ecx ^ eax;
ebx = ecx >> 0x18;
eax = (ecx >> 0x10) & 0xFF;
ebx = this->state.private_keys.as32[ebx] + this->state.private_keys.as32[eax + 0x100];
eax = (ecx >> 8) & 0xFF;
ebx = ebx ^ this->state.private_keys.as32[eax + 0x200];
eax = ecx & 0xFF;
ebx = ebx + this->state.private_keys.as32[eax + 0x300];
for (x = 0; x <= 5; x++) {
ebx = ebx ^ this->state.initial_keys.as32[(x * 2) + 2];
esi = esi ^ ebx;
ebx = esi >> 0x18;
eax = (esi >> 0x10) & 0xFF;
ebx = this->state.private_keys.as32[ebx] + this->state.private_keys.as32[eax + 0x100];
eax = (esi >> 8) & 0xFF;
ebx = ebx ^ this->state.private_keys.as32[eax + 0x200];
eax = esi & 0xFF;
ebx = ebx + this->state.private_keys.as32[eax + 0x300];
ebx = ebx ^ this->state.initial_keys.as32[(x * 2) + 3];
ecx = ecx ^ ebx;
ebx = ecx >> 0x18;
eax = (ecx >> 0x10) & 0xFF;
ebx = this->state.private_keys.as32[ebx] + this->state.private_keys.as32[eax + 0x100];
eax = (ecx >> 8) & 0xFF;
ebx = ebx ^ this->state.private_keys.as32[eax + 0x200];
eax = ecx & 0xFF;
ebx = ebx + this->state.private_keys.as32[eax + 0x300];
}
ebx = ebx ^ this->state.initial_keys.as32[14];
esi = esi ^ ebx;
eax = esi >> 0x18;
ebx = (esi >> 0x10) & 0xFF;
eax = this->state.private_keys.as32[eax] + this->state.private_keys.as32[ebx + 0x100];
ebx = (esi >> 8) & 0xFF;
eax = eax ^ this->state.private_keys.as32[ebx + 0x200];
ebx = esi & 0xFF;
eax = eax + this->state.private_keys.as32[ebx + 0x300];
eax = eax ^ this->state.initial_keys.as32[15];
eax = ecx ^ eax;
ecx = eax >> 0x18;
ebx = (eax >> 0x10) & 0xFF;
ecx = this->state.private_keys.as32[ecx] + this->state.private_keys.as32[ebx + 0x100];
ebx = (eax >> 8) & 0xFF;
ecx = ecx ^ this->state.private_keys.as32[ebx + 0x200];
ebx = eax & 0xFF;
ecx = ecx + this->state.private_keys.as32[ebx + 0x300];
ecx = ecx ^ this->state.initial_keys.as32[16];
ecx = ecx ^ esi;
esi = this->state.initial_keys.as32[17];
esi = esi ^ eax;
this->state.initial_keys.as32[(edi / 4)] = esi;
this->state.initial_keys.as32[(edi / 4)+1] = ecx;
edi = edi + 8;
}
eax = 0;
edx = 0;
ou = 0;
while (ou < 0x1000) {
edi = 0;
edx = 0x400;
while (edi < edx) {
esi = esi ^ this->state.initial_keys.as32[0];
eax = esi >> 0x18;
ebx = (esi >> 0x10) & 0xFF;
eax = this->state.private_keys.as32[eax] + this->state.private_keys.as32[ebx + 0x100];
ebx = (esi >> 8) & 0xFF;
eax = eax ^ this->state.private_keys.as32[ebx + 0x200];
ebx = esi & 0xFF;
eax = eax + this->state.private_keys.as32[ebx + 0x300];
eax = eax ^ this->state.initial_keys.as32[1];
ecx = ecx ^ eax;
ebx = ecx >> 0x18;
eax = (ecx >> 0x10) & 0xFF;
ebx = this->state.private_keys.as32[ebx] + this->state.private_keys.as32[eax + 0x100];
eax = (ecx >> 8) & 0xFF;
ebx = ebx ^ this->state.private_keys.as32[eax + 0x200];
eax = ecx & 0xFF;
ebx = ebx + this->state.private_keys.as32[eax + 0x300];
for (x = 0; x <= 5; x++) {
ebx = ebx ^ this->state.initial_keys.as32[(x * 2) + 2];
esi = esi ^ ebx;
ebx = esi >> 0x18;
eax = (esi >> 0x10) & 0xFF;
ebx = this->state.private_keys.as32[ebx] + this->state.private_keys.as32[eax + 0x100];
eax = (esi >> 8) & 0xFF;
ebx = ebx ^ this->state.private_keys.as32[eax + 0x200];
eax = esi & 0xFF;
ebx = ebx + this->state.private_keys.as32[eax + 0x300];
ebx = ebx ^ this->state.initial_keys.as32[(x * 2) + 3];
ecx = ecx ^ ebx;
ebx = ecx >> 0x18;
eax = (ecx >> 0x10) & 0xFF;
ebx = this->state.private_keys.as32[ebx] + this->state.private_keys.as32[eax + 0x100];
eax = (ecx >> 8) & 0xFF;
ebx = ebx ^ this->state.private_keys.as32[eax + 0x200];
eax = ecx & 0xFF;
ebx = ebx + this->state.private_keys.as32[eax + 0x300];
}
ebx = ebx ^ this->state.initial_keys.as32[14];
esi = esi ^ ebx;
eax = esi >> 0x18;
ebx = (esi >> 0x10) & 0xFF;
eax = this->state.private_keys.as32[eax] + this->state.private_keys.as32[ebx + 0x100];
ebx = (esi >> 8) & 0xFF;
eax = eax ^ this->state.private_keys.as32[ebx + 0x200];
ebx = esi & 0xFF;
eax = eax + this->state.private_keys.as32[ebx + 0x300];
eax = eax ^ this->state.initial_keys.as32[15];
eax = ecx ^ eax;
ecx = eax >> 0x18;
ebx = (eax >> 0x10) & 0xFF;
ecx = this->state.private_keys.as32[ecx] + this->state.private_keys.as32[ebx + 0x100];
ebx = (eax >> 8) & 0xFF;
ecx = ecx ^ this->state.private_keys.as32[ebx + 0x200];
ebx = eax & 0xFF;
ecx = ecx + this->state.private_keys.as32[ebx + 0x300];
ecx = ecx ^ this->state.initial_keys.as32[16];
ecx = ecx ^ esi;
esi = this->state.initial_keys.as32[17];
esi = esi ^ eax;
this->state.private_keys.as32[(ou / 4) + (edi / 4)] = esi;
this->state.private_keys.as32[(ou / 4) + (edi / 4) + 1] = ecx;
edi = edi + 8;
}
ou = ou + 0x400;
}
}
ou = ou + 0x400;
}
}
PSOBBMultiKeyDetectorEncryption::PSOBBMultiKeyDetectorEncryption(
const vector<shared_ptr<const PSOBBEncryption::KeyFile>>& possible_keys,
const string& expected_first_data,
const void* seed,
size_t seed_size)
: possible_keys(possible_keys),
expected_first_data(expected_first_data),
seed(reinterpret_cast<const char*>(seed), seed_size) { }
void PSOBBMultiKeyDetectorEncryption::encrypt(void* data, size_t size, bool advance) {
if (!this->active_crypt.get()) {
throw logic_error("PSOBB multi-key encryption requires client input first");
}
this->active_crypt->encrypt(data, size, advance);
}
void PSOBBMultiKeyDetectorEncryption::decrypt(void* data, size_t size, bool advance) {
if (!this->active_crypt.get()) {
if (size != this->expected_first_data.size()) {
throw logic_error("initial decryption size does not match expected first data size");
}
for (const auto& key : this->possible_keys) {
this->active_key = key;
this->active_crypt.reset(new PSOBBEncryption(
*this->active_key, this->seed.data(), this->seed.size()));
string test_data(reinterpret_cast<const char*>(data), size);
this->active_crypt->decrypt(test_data.data(), test_data.size(), false);
if (test_data == this->expected_first_data) {
break;
}
this->active_key.reset();
this->active_crypt.reset();
}
if (!this->active_crypt.get()) {
throw runtime_error("none of the registered private keys are valid for this client");
}
}
this->active_crypt->decrypt(data, size, advance);
}
PSOBBMultiKeyImitatorEncryption::PSOBBMultiKeyImitatorEncryption(
shared_ptr<const PSOBBMultiKeyDetectorEncryption> detector_crypt,
const void* seed,
size_t seed_size,
bool jsd1_use_detector_seed)
: detector_crypt(detector_crypt),
seed(reinterpret_cast<const char*>(seed), seed_size),
jsd1_use_detector_seed(jsd1_use_detector_seed) { }
void PSOBBMultiKeyImitatorEncryption::encrypt(void* data, size_t size, bool advance) {
this->ensure_crypt()->encrypt(data, size, advance);
}
void PSOBBMultiKeyImitatorEncryption::decrypt(void* data, size_t size, bool advance) {
this->ensure_crypt()->decrypt(data, size, advance);
}
shared_ptr<PSOBBEncryption> PSOBBMultiKeyImitatorEncryption::ensure_crypt() {
if (!this->active_crypt.get()) {
auto key = this->detector_crypt->get_active_key();
if (!key.get()) {
throw logic_error("server crypt cannot be initialized because client crypt is not ready");
}
// Hack: JSD1 uses the client seed for both ends of the connection and
// ignores the server seed (though each end has its own state after that).
// To handle this, we use the other crypt's seed if the type is JSD1.
if ((key->subtype == PSOBBEncryption::Subtype::JSD1) && this->jsd1_use_detector_seed) {
const auto& detector_seed = this->detector_crypt->get_seed();
this->active_crypt.reset(new PSOBBEncryption(
*key, detector_seed.data(), detector_seed.size()));
} else {
this->active_crypt.reset(new PSOBBEncryption(
*key, this->seed.data(), this->seed.size()));
}
}
return this->active_crypt;
}
+102 -21
View File
@@ -3,11 +3,16 @@
#include <inttypes.h>
#include <stddef.h>
#include <memory>
#include <string>
#include <vector>
#include <phosg/Encoding.hh>
#include "Text.hh" // for parray
#define PC_STREAM_LENGTH 57
#define PC_STREAM_LENGTH 56
#define GC_STREAM_LENGTH 521
#define BB_STREAM_LENGTH 1042
@@ -15,8 +20,15 @@ class PSOEncryption {
public:
virtual ~PSOEncryption() = default;
virtual void encrypt(void* data, size_t size) = 0;
virtual void decrypt(void* data, size_t size);
virtual void encrypt(void* data, size_t size, bool advance = true) = 0;
virtual void decrypt(void* data, size_t size, bool advance = true);
inline void encrypt(std::string& data, bool advance = true) {
this->encrypt(data.data(), data.size(), advance);
}
inline void decrypt(std::string& data, bool advance = true) {
this->decrypt(data.data(), data.size(), advance);
}
protected:
PSOEncryption() = default;
@@ -26,25 +38,25 @@ class PSOPCEncryption : public PSOEncryption {
public:
explicit PSOPCEncryption(uint32_t seed);
virtual void encrypt(void* data, size_t size);
virtual void encrypt(void* data, size_t size, bool advance = true);
protected:
void update_stream();
uint32_t next();
uint32_t next(bool advance = true);
uint32_t stream[PC_STREAM_LENGTH];
uint16_t offset;
uint32_t stream[PC_STREAM_LENGTH + 1];
uint8_t offset;
};
class PSOGCEncryption : public PSOEncryption {
public:
explicit PSOGCEncryption(uint32_t key);
virtual void encrypt(void* data, size_t size);
virtual void encrypt(void* data, size_t size, bool advance = true);
protected:
void update_stream();
uint32_t next();
uint32_t next(bool advance = true);
uint32_t stream[GC_STREAM_LENGTH];
uint16_t offset;
@@ -52,23 +64,92 @@ protected:
class PSOBBEncryption : public PSOEncryption {
public:
struct KeyFile {
uint32_t initial_keys[18];
uint32_t private_keys[1024];
enum Subtype : uint8_t {
STANDARD = 0x00,
MOCB1 = 0x01,
JSD1 = 0x02,
TFS1 = 0x03,
};
struct KeyFile {
// initial_keys are actually a stream of uint32_ts, but we treat them as
// bytes for code simplicity
union InitialKeys {
uint8_t jsd1_stream_offset;
parray<uint8_t, 0x48> as8;
parray<le_uint32_t, 0x12> as32;
InitialKeys() : as32() { }
InitialKeys(const InitialKeys& other) : as32(other.as32) { }
} __attribute__((packed));
union PrivateKeys {
parray<uint8_t, 0x1000> as8;
parray<le_uint32_t, 0x400> as32;
PrivateKeys() : as32() { }
PrivateKeys(const PrivateKeys& other) : as32(other.as32) { }
} __attribute__((packed));
InitialKeys initial_keys;
PrivateKeys private_keys;
Subtype subtype;
} __attribute__((packed));
PSOBBEncryption(const KeyFile& key, const void* seed, size_t seed_size);
virtual void encrypt(void* data, size_t size);
virtual void decrypt(void* data, size_t size);
virtual void encrypt(void* data, size_t size, bool advance = true);
virtual void decrypt(void* data, size_t size, bool advance = true);
protected:
PSOBBEncryption() = default;
KeyFile state;
void postprocess_initial_stream(const std::string& seed);
void update_stream();
uint32_t stream[BB_STREAM_LENGTH];
uint16_t offset;
void tfs1_scramble(uint32_t* out1, uint32_t* out2) const;
void apply_seed(const void* original_seed, size_t seed_size);
};
// The following classes provide support for multiple PSOBB private keys, and
// the ability to automatically detect which key the client is using based on
// the first 8 bytes they send.
class PSOBBMultiKeyDetectorEncryption : public PSOEncryption {
public:
PSOBBMultiKeyDetectorEncryption(
const std::vector<std::shared_ptr<const PSOBBEncryption::KeyFile>>& possible_keys,
const std::string& expected_first_data,
const void* seed,
size_t seed_size);
virtual void encrypt(void* data, size_t size, bool advance = true);
virtual void decrypt(void* data, size_t size, bool advance = true);
inline std::shared_ptr<const PSOBBEncryption::KeyFile> get_active_key() const {
return this->active_key;
}
inline const std::string& get_seed() const {
return this->seed;
}
protected:
std::vector<std::shared_ptr<const PSOBBEncryption::KeyFile>> possible_keys;
std::shared_ptr<const PSOBBEncryption::KeyFile> active_key;
std::shared_ptr<PSOBBEncryption> active_crypt;
std::string expected_first_data;
std::string seed;
};
class PSOBBMultiKeyImitatorEncryption : public PSOEncryption {
public:
PSOBBMultiKeyImitatorEncryption(
std::shared_ptr<const PSOBBMultiKeyDetectorEncryption> client_crypt,
const void* seed,
size_t seed_size,
bool jsd1_use_detector_seed);
virtual void encrypt(void* data, size_t size, bool advance = true);
virtual void decrypt(void* data, size_t size, bool advance = true);
protected:
std::shared_ptr<PSOBBEncryption> ensure_crypt();
std::shared_ptr<const PSOBBMultiKeyDetectorEncryption> detector_crypt;
std::shared_ptr<PSOBBEncryption> active_crypt;
std::string seed;
bool jsd1_use_detector_seed;
};
+113 -15
View File
@@ -1,50 +1,148 @@
#include "PSOProtocol.hh"
#include <event2/buffer.h>
#include <stdexcept>
#include <phosg/Strings.hh>
#include "Text.hh"
using namespace std;
extern bool use_terminal_colors;
PSOCommandHeader::PSOCommandHeader() {
this->bb.size = 0;
this->bb.command = 0;
this->bb.flag = 0;
}
uint16_t PSOCommandHeader::command(GameVersion version) const {
switch (version) {
case GameVersion::DC:
return this->dc.command;
case GameVersion::GC:
return reinterpret_cast<const PSOCommandHeaderDCGC*>(this)->command;
return this->gc.command;
case GameVersion::PC:
case GameVersion::Patch:
return reinterpret_cast<const PSOCommandHeaderPC*>(this)->command;
case GameVersion::PATCH:
return this->pc.command;
case GameVersion::BB:
return reinterpret_cast<const PSOCommandHeaderBB*>(this)->command;
return this->bb.command;
default:
throw logic_error("unknown game version");
}
}
void PSOCommandHeader::set_command(GameVersion version, uint16_t command) {
switch (version) {
case GameVersion::DC:
this->dc.command = command;
break;
case GameVersion::GC:
this->gc.command = command;
break;
case GameVersion::PC:
case GameVersion::PATCH:
this->pc.command = command;
break;
case GameVersion::BB:
this->bb.command = command;
break;
default:
throw logic_error("unknown game version");
}
throw logic_error("unknown game version");
}
uint16_t PSOCommandHeader::size(GameVersion version) const {
switch (version) {
case GameVersion::DC:
return this->dc.size;
case GameVersion::GC:
return reinterpret_cast<const PSOCommandHeaderDCGC*>(this)->size;
return this->gc.size;
case GameVersion::PC:
case GameVersion::Patch:
return reinterpret_cast<const PSOCommandHeaderPC*>(this)->size;
case GameVersion::PATCH:
return this->pc.size;
case GameVersion::BB:
return reinterpret_cast<const PSOCommandHeaderBB*>(this)->size;
return this->bb.size;
default:
throw logic_error("unknown game version");
}
}
void PSOCommandHeader::set_size(GameVersion version, uint32_t size) {
switch (version) {
case GameVersion::DC:
this->dc.size = size;
break;
case GameVersion::GC:
this->gc.size = size;
break;
case GameVersion::PC:
case GameVersion::PATCH:
this->pc.size = size;
break;
case GameVersion::BB:
this->bb.size = size;
break;
default:
throw logic_error("unknown game version");
}
throw logic_error("unknown game version");
}
uint32_t PSOCommandHeader::flag(GameVersion version) const {
switch (version) {
case GameVersion::DC:
return this->dc.flag;
case GameVersion::GC:
return reinterpret_cast<const PSOCommandHeaderDCGC*>(this)->flag;
return this->gc.flag;
case GameVersion::PC:
case GameVersion::Patch:
return reinterpret_cast<const PSOCommandHeaderPC*>(this)->flag;
case GameVersion::PATCH:
return this->pc.flag;
case GameVersion::BB:
return reinterpret_cast<const PSOCommandHeaderBB*>(this)->flag;
return this->bb.flag;
default:
throw logic_error("unknown game version");
}
throw logic_error("unknown game version");
}
void PSOCommandHeader::set_flag(GameVersion version, uint32_t flag) {
switch (version) {
case GameVersion::DC:
this->dc.flag = flag;
break;
case GameVersion::GC:
this->gc.flag = flag;
break;
case GameVersion::PC:
case GameVersion::PATCH:
this->pc.flag = flag;
break;
case GameVersion::BB:
this->bb.flag = flag;
break;
default:
throw logic_error("unknown game version");
}
}
void check_size_v(size_t size, size_t min_size, size_t max_size) {
if (size < min_size) {
throw std::runtime_error(string_printf(
"command too small (expected at least 0x%zX bytes, received 0x%zX bytes)",
min_size, size));
}
if (max_size < min_size) {
max_size = min_size;
}
if (size > max_size) {
throw std::runtime_error(string_printf(
"command too large (expected at most 0x%zX bytes, received 0x%zX bytes)",
max_size, size));
}
}
+64 -12
View File
@@ -1,26 +1,31 @@
#pragma once
#include <inttypes.h>
#include <event2/bufferevent.h>
#include <functional>
#include <phosg/Strings.hh>
#include "Version.hh"
#include "PSOEncryption.hh"
struct PSOCommandHeaderPC {
uint16_t size;
le_uint16_t size;
uint8_t command;
uint8_t flag;
};
} __attribute__((packed));
struct PSOCommandHeaderDCGC {
uint8_t command;
uint8_t flag;
uint16_t size;
};
le_uint16_t size;
} __attribute__((packed));
struct PSOCommandHeaderBB {
uint16_t size;
uint16_t command;
uint32_t flag;
};
le_uint16_t size;
le_uint16_t command;
le_uint32_t flag;
} __attribute__((packed));
union PSOCommandHeader {
PSOCommandHeaderDCGC dc;
@@ -29,12 +34,59 @@ union PSOCommandHeader {
PSOCommandHeaderBB bb;
uint16_t command(GameVersion version) const;
void set_command(GameVersion version, uint16_t command);
uint16_t size(GameVersion version) const;
void set_size(GameVersion version, uint32_t size);
uint32_t flag(GameVersion version) const;
};
void set_flag(GameVersion version, uint32_t flag);
static inline size_t header_size(GameVersion version) {
return (version == GameVersion::BB) ? 8 : 4;
}
PSOCommandHeader();
} __attribute__((packed));
union PSOSubcommand {
uint8_t byte[4];
uint16_t word[2];
uint32_t dword;
};
le_uint16_t word[2];
le_uint32_t dword;
} __attribute__((packed));
// This function is used in a lot of places to check received command sizes and
// cast them to the appropriate type
template <typename T>
const T& check_size_t(
const std::string& data,
size_t min_size = sizeof(T),
size_t max_size = sizeof(T)) {
if (data.size() < min_size) {
throw std::runtime_error(string_printf(
"command too small (expected at least 0x%zX bytes, received 0x%zX bytes)",
min_size, data.size()));
}
if (data.size() > max_size) {
throw std::runtime_error(string_printf(
"command too large (expected at most 0x%zX bytes, received 0x%zX bytes)",
max_size, data.size()));
}
return *reinterpret_cast<const T*>(data.data());
}
template <typename T>
T& check_size_t(
std::string& data,
size_t min_size = sizeof(T),
size_t max_size = sizeof(T)) {
if (data.size() < min_size) {
throw std::runtime_error(string_printf(
"command too small (expected at least 0x%zX bytes, received 0x%zX bytes)",
min_size, data.size()));
}
if (data.size() > max_size) {
throw std::runtime_error(string_printf(
"command too large (expected at most 0x%zX bytes, received 0x%zX bytes)",
max_size, data.size()));
}
return *reinterpret_cast<T*>(data.data());
}
void check_size_v(size_t size, size_t min_size, size_t max_size = 0);
+465 -392
View File
File diff suppressed because it is too large Load Diff
+328 -290
View File
@@ -7,49 +7,52 @@
#include <vector>
#include <phosg/Encoding.hh>
#include "LevelTable.hh"
#include "Version.hh"
#include "Text.hh"
#include "Episode3.hh"
// raw item data
struct ItemData {
union {
uint8_t item_data1[12];
uint16_t item_data1w[6];
uint32_t item_data1d[3];
};
uint32_t item_id;
uint8_t data1[12];
le_uint16_t data1w[6];
le_uint32_t data1d[3];
} __attribute__((packed));
le_uint32_t id;
union {
uint8_t item_data2[4];
uint16_t item_data2w[2];
uint32_t item_data2d;
};
uint8_t data2[4];
le_uint16_t data2w[2];
le_uint32_t data2d;
} __attribute__((packed));
ItemData();
uint32_t primary_identifier() const;
};
} __attribute__((packed));
struct PlayerBankItem;
// an item in a player's inventory
struct PlayerInventoryItem {
uint16_t equip_flags;
uint16_t tech_flag;
uint32_t game_flags;
le_uint16_t equip_flags;
le_uint16_t tech_flag;
le_uint32_t game_flags;
ItemData data;
PlayerBankItem to_bank_item() const;
};
PlayerInventoryItem();
PlayerInventoryItem(const PlayerBankItem&);
} __attribute__((packed));
// an item in a player's bank
struct PlayerBankItem {
ItemData data;
uint16_t amount;
uint16_t show_flags;
le_uint16_t amount;
le_uint16_t show_flags;
PlayerInventoryItem to_inventory_item() const;
};
PlayerBankItem();
PlayerBankItem(const PlayerInventoryItem&);
} __attribute__((packed));
// a player's inventory (remarkably, the format is the same in all versions of PSO)
struct PlayerInventory {
uint8_t num_items;
uint8_t hp_materials_used;
@@ -57,380 +60,415 @@ struct PlayerInventory {
uint8_t language;
PlayerInventoryItem items[30];
size_t find_item(uint32_t item_id);
};
PlayerInventory();
size_t find_item(uint32_t item_id);
} __attribute__((packed));
// a player's bank
struct PlayerBank {
uint32_t num_items;
uint32_t meseta;
le_uint32_t num_items;
le_uint32_t meseta;
PlayerBankItem items[200];
void load(const std::string& filename);
void save(const std::string& filename) const;
bool switch_with_file(const char* save_filename, const char* load_filename);
bool switch_with_file(const std::string& save_filename,
const std::string& load_filename);
void add_item(const PlayerBankItem& item);
void remove_item(uint32_t item_id, uint32_t amount, PlayerBankItem* item);
PlayerBankItem remove_item(uint32_t item_id, uint32_t amount);
size_t find_item(uint32_t item_id);
};
} __attribute__((packed));
// simple player stats
struct PlayerStats {
uint16_t atp;
uint16_t mst;
uint16_t evp;
uint16_t hp;
uint16_t dfp;
uint16_t ata;
uint16_t lck;
};
struct PlayerDispDataBB;
// PC/GC player appearance and stats data
struct PlayerDispDataPCGC { // 0xD0 in size
PlayerStats stats;
uint16_t unknown1;
uint32_t unknown2[2];
uint32_t level;
uint32_t experience;
uint32_t meseta;
char name[16];
uint32_t unknown3[2];
uint32_t name_color;
parray<uint8_t, 0x0A> unknown_a1;
le_uint32_t level;
le_uint32_t experience;
le_uint32_t meseta;
ptext<char, 0x10> name;
uint64_t unknown_a2;
le_uint32_t name_color;
uint8_t extra_model;
uint8_t unused[15];
uint32_t name_color_checksum;
parray<uint8_t, 0x0F> unused;
le_uint32_t name_color_checksum;
uint8_t section_id;
uint8_t char_class;
uint8_t v2_flags;
uint8_t version;
uint32_t v1_flags;
uint16_t costume;
uint16_t skin;
uint16_t face;
uint16_t head;
uint16_t hair;
uint16_t hair_r;
uint16_t hair_g;
uint16_t hair_b;
float proportion_x;
float proportion_y;
uint8_t config[0x48];
uint8_t technique_levels[0x14];
le_uint32_t v1_flags;
le_uint16_t costume;
le_uint16_t skin;
le_uint16_t face;
le_uint16_t head;
le_uint16_t hair;
le_uint16_t hair_r;
le_uint16_t hair_g;
le_uint16_t hair_b;
le_float proportion_x;
le_float proportion_y;
parray<uint8_t, 0x48> config;
parray<uint8_t, 0x14> technique_levels;
// Note: This struct has a default constructor because it's used in a command
// that has a fixed-size array. If we didn't define this constructor, the
// trivial fields in that array's members would be uninitialized, and we could
// send uninitialized memory to the client.
PlayerDispDataPCGC() noexcept;
void enforce_pc_limits();
PlayerDispDataBB to_bb() const;
};
} __attribute__((packed));
// BB player preview format
struct PlayerDispDataBBPreview {
uint32_t experience;
uint32_t level;
char guild_card[16];
uint32_t unknown3[2];
uint32_t name_color;
le_uint32_t experience;
le_uint32_t level;
ptext<char, 0x10> guild_card;
uint64_t unknown_a2;
le_uint32_t name_color;
uint8_t extra_model;
uint8_t unused[15];
uint32_t name_color_checksum;
parray<uint8_t, 0x0F> unused;
le_uint32_t name_color_checksum;
uint8_t section_id;
uint8_t char_class;
uint8_t v2_flags;
uint8_t version;
uint32_t v1_flags;
uint16_t costume;
uint16_t skin;
uint16_t face;
uint16_t head;
uint16_t hair;
uint16_t hair_r;
uint16_t hair_g;
uint16_t hair_b;
float proportion_x;
float proportion_y;
char16_t name[16];
le_uint32_t v1_flags;
le_uint16_t costume;
le_uint16_t skin;
le_uint16_t face;
le_uint16_t head;
le_uint16_t hair;
le_uint16_t hair_r;
le_uint16_t hair_g;
le_uint16_t hair_b;
le_float proportion_x;
le_float proportion_y;
ptext<char16_t, 0x10> name;
uint32_t play_time;
};
PlayerDispDataBBPreview() noexcept;
} __attribute__((packed));
// BB player appearance and stats data
struct PlayerDispDataBB {
PlayerStats stats;
uint16_t unknown1;
uint32_t unknown2[2];
uint32_t level;
uint32_t experience;
uint32_t meseta;
char guild_card[16];
uint32_t unknown3[2];
uint32_t name_color;
parray<uint8_t, 0x0A> unknown_a1;
le_uint32_t level;
le_uint32_t experience;
le_uint32_t meseta;
ptext<char, 0x10> guild_card;
uint64_t unknown_a2;
le_uint32_t name_color;
uint8_t extra_model;
uint8_t unused[11];
uint32_t play_time; // not actually a game field; used only by my server
uint32_t name_color_checksum;
parray<uint8_t, 0x0F> unused;
le_uint32_t name_color_checksum;
uint8_t section_id;
uint8_t char_class;
uint8_t v2_flags;
uint8_t version;
uint32_t v1_flags;
uint16_t costume;
uint16_t skin;
uint16_t face;
uint16_t head;
uint16_t hair;
uint16_t hair_r;
uint16_t hair_g;
uint16_t hair_b;
float proportion_x;
float proportion_y;
char16_t name[0x10];
uint8_t config[0xE8];
uint8_t technique_levels[0x14];
le_uint32_t v1_flags;
le_uint16_t costume;
le_uint16_t skin;
le_uint16_t face;
le_uint16_t head;
le_uint16_t hair;
le_uint16_t hair_r;
le_uint16_t hair_g;
le_uint16_t hair_b;
le_float proportion_x;
le_float proportion_y;
ptext<char16_t, 0x10> name;
parray<uint8_t, 0xE8> config;
parray<uint8_t, 0x14> technique_levels;
PlayerDispDataBB() noexcept;
inline void enforce_pc_limits() { }
PlayerDispDataPCGC to_pcgc() const;
PlayerDispDataBBPreview to_preview() const;
void apply_preview(const PlayerDispDataBBPreview&);
};
} __attribute__((packed));
struct GuildCardGC {
uint32_t player_tag;
uint32_t serial_number;
char name[0x18];
char desc[0x6C];
le_uint32_t player_tag;
le_uint32_t serial_number;
ptext<char, 0x18> name;
ptext<char, 0x6C> desc;
uint8_t reserved1; // should be 1
uint8_t reserved2; // should be 1
uint8_t section_id;
uint8_t char_class;
};
GuildCardGC() noexcept;
} __attribute__((packed));
// BB guild card format
struct GuildCardBB {
uint32_t serial_number;
char16_t name[0x18];
char16_t teamname[0x10];
char16_t desc[0x58];
le_uint32_t serial_number;
ptext<char16_t, 0x18> name;
ptext<char16_t, 0x10> teamname;
ptext<char16_t, 0x58> desc;
uint8_t reserved1; // should be 1
uint8_t reserved2; // should be 1
uint8_t section_id;
uint8_t char_class;
};
GuildCardBB() noexcept;
} __attribute__((packed));
// an entry in the BB guild card file
struct GuildCardEntryBB {
GuildCardBB data;
uint8_t unknown[0xB4];
};
parray<uint8_t, 0xB4> unknown;
} __attribute__((packed));
// the format of the BB guild card file
struct GuildCardFileBB {
uint8_t unknown[0x1F84];
parray<uint8_t, 0x1F84> unknown_a1;
GuildCardEntryBB entry[0x0068]; // that's 104 of them in decimal
uint8_t unknown2[0x01AC];
};
parray<uint8_t, 0x01AC> unknown_a2;
} __attribute__((packed));
// PSOBB key config and team info
struct KeyAndTeamConfigBB {
uint8_t unknown[0x0114]; // 0000
uint8_t key_config[0x016C]; // 0114
uint8_t joystick_config[0x0038]; // 0280
uint32_t serial_number; // 02B8
uint32_t team_id; // 02BC
uint32_t team_info[2]; // 02C0
uint16_t team_privilege_level; // 02C8
uint16_t reserved; // 02CA
char16_t team_name[0x0010]; // 02CC
uint8_t team_flag[0x0800]; // 02EC
uint32_t team_rewards[2]; // 0AEC
};
// BB account data
struct PlayerAccountDataBB {
uint8_t symbol_chats[0x04E0];
KeyAndTeamConfigBB key_config;
GuildCardFileBB guild_cards;
uint32_t options;
uint8_t shortcuts[0x0A40]; // chat shortcuts (@1FB4 in E7 command)
};
parray<uint8_t, 0x0114> unknown_a1; // 0000
parray<uint8_t, 0x016C> key_config; // 0114
parray<uint8_t, 0x0038> joystick_config; // 0280
le_uint32_t serial_number; // 02B8
le_uint32_t team_id; // 02BC
le_uint64_t team_info; // 02C0
le_uint16_t team_privilege_level; // 02C8
le_uint16_t reserved; // 02CA
ptext<char16_t, 0x0010> team_name; // 02CC
parray<uint8_t, 0x0800> team_flag; // 02EC
le_uint32_t team_rewards; // 0AEC
} __attribute__((packed));
struct PlayerLobbyDataPC {
uint32_t player_tag;
uint32_t guild_card;
le_uint32_t player_tag;
le_uint32_t guild_card;
be_uint32_t ip_address;
uint32_t client_id;
char16_t name[16];
};
le_uint32_t client_id;
ptext<char16_t, 0x10> name;
PlayerLobbyDataPC() noexcept;
} __attribute__((packed));
struct PlayerLobbyDataGC {
uint32_t player_tag;
uint32_t guild_card;
le_uint32_t player_tag;
le_uint32_t guild_card;
be_uint32_t ip_address;
uint32_t client_id;
char name[16];
};
le_uint32_t client_id;
ptext<char, 0x10> name;
PlayerLobbyDataGC() noexcept;
} __attribute__((packed));
struct PlayerLobbyDataBB {
uint32_t player_tag;
uint32_t guild_card;
uint32_t unknown1[5];
uint32_t client_id;
char16_t name[16];
uint32_t unknown2;
};
le_uint32_t player_tag;
le_uint32_t guild_card;
be_uint32_t ip_address; // Guess - the official builds didn't use this, but all other versions have it
parray<uint8_t, 0x10> unknown_a1;
le_uint32_t client_id;
ptext<char16_t, 0x10> name;
le_uint32_t unknown2;
PlayerLobbyDataBB() noexcept;
} __attribute__((packed));
struct PSOPlayerDataPC { // for command 0x61
struct PSOPlayerDataPC { // For command 61
PlayerInventory inventory;
PlayerDispDataPCGC disp;
};
} __attribute__((packed));
struct PSOPlayerDataGC { // for command 0x61
struct PSOPlayerDataGC { // For command 61
PlayerInventory inventory;
PlayerDispDataPCGC disp;
char unknown[0x134];
char info_board[0xAC];
uint32_t blocked[0x1E];
uint32_t auto_reply_enabled;
parray<uint8_t, 0x134> unknown;
ptext<char, 0xAC> info_board;
parray<le_uint32_t, 0x1E> blocked_senders;
le_uint32_t auto_reply_enabled;
char auto_reply[0];
};
} __attribute__((packed));
struct PSOPlayerDataBB { // for command 0x61
PlayerInventory inventory;
PlayerDispDataBB disp;
char unused[0x174];
char16_t info_board[0xAC];
uint32_t blocked[0x1E];
uint32_t auto_reply_enabled;
char16_t auto_reply[0];
};
// PC/GC lobby player data (used in lobby/game join commands)
struct PlayerLobbyJoinDataPCGC {
struct PSOPlayerDataGCEp3 { // For command 61
PlayerInventory inventory;
PlayerDispDataPCGC disp;
};
parray<uint8_t, 0x134> unknown;
ptext<char, 0xAC> info_board;
parray<le_uint32_t, 0x1E> blocked_senders;
le_uint32_t auto_reply_enabled;
char auto_reply[0xAC];
Ep3Config ep3_config;
} __attribute__((packed));
// BB lobby player data (used in lobby/game join commands)
struct PlayerLobbyJoinDataBB {
struct PSOPlayerDataBB { // For command 61
PlayerInventory inventory;
PlayerDispDataBB disp;
};
ptext<char, 0x174> unused;
ptext<char16_t, 0xAC> info_board;
parray<le_uint32_t, 0x1E> blocked_senders;
le_uint32_t auto_reply_enabled;
char16_t auto_reply[0];
} __attribute__((packed));
// complete BB player data format (used in E7 command)
struct PlayerBB {
PlayerInventory inventory; // 0000 // player
PlayerDispDataBB disp; // 034C // player
uint8_t unknown[0x0010]; // 04DC //
uint32_t option_flags; // 04EC // account
uint8_t quest_data1[0x0208]; // 04F0 // player
PlayerBank bank; // 06F8 // player
uint32_t serial_number; // 19C0 // player
char16_t name[0x18]; // 19C4 // player
char16_t team_name[0x10]; // 19C4 // player
char16_t guild_card_desc[0x58]; // 1A14 // player
uint8_t reserved1; // 1AC4 // player
uint8_t reserved2; // 1AC5 // player
uint8_t section_id; // 1AC6 // player
uint8_t char_class; // 1AC7 // player
uint32_t unknown3; // 1AC8 //
uint8_t symbol_chats[0x04E0]; // 1ACC // account
uint8_t shortcuts[0x0A40]; // 1FAC // account
char16_t auto_reply[0x00AC]; // 29EC // player
char16_t info_board[0x00AC]; // 2B44 // player
uint8_t unknown5[0x001C]; // 2C9C //
uint8_t challenge_data[0x0140]; // 2CB8 // player
uint8_t tech_menu_config[0x0028]; // 2DF8 // player
uint8_t unknown6[0x002C]; // 2E20 //
uint8_t quest_data2[0x0058]; // 2E4C // player
KeyAndTeamConfigBB key_config; // 2EA4 // account
}; // total size: 39A0
struct PlayerBB { // Used in 00E7 command
PlayerInventory inventory; // player
PlayerDispDataBB disp; // player
parray<uint8_t, 0x0010> unknown; // not saved
le_uint32_t option_flags; // account
parray<uint8_t, 0x0208> quest_data1; // player
PlayerBank bank; // player
le_uint32_t serial_number; // player
ptext<char16_t, 0x18> name; // player
ptext<char16_t, 0x10> team_name; // player
ptext<char16_t, 0x58> guild_card_desc; // player
uint8_t reserved1; // player
uint8_t reserved2; // player
uint8_t section_id; // player
uint8_t char_class; // player
le_uint32_t unknown3; // not saved
parray<uint8_t, 0x04E0> symbol_chats; // account
parray<uint8_t, 0x0A40> shortcuts; // account
ptext<char16_t, 0x00AC> auto_reply; // player
ptext<char16_t, 0x00AC> info_board; // player
parray<uint8_t, 0x001C> unknown5; // not saved
parray<uint8_t, 0x0140> challenge_data; // player
parray<uint8_t, 0x0028> tech_menu_config; // player
parray<uint8_t, 0x002C> unknown6; // not saved
parray<uint8_t, 0x0058> quest_data2; // player
KeyAndTeamConfigBB key_config; // account
} __attribute__((packed));
struct SavedPlayerBB { // .nsc file format
char signature[0x40];
struct SavedPlayerDataBB { // .nsc file format
ptext<char, 0x40> signature;
PlayerDispDataBBPreview preview;
char16_t auto_reply[0x00AC];
PlayerBank bank;
uint8_t challenge_data[0x0140];
PlayerDispDataBB disp;
char16_t guild_card_desc[0x58];
char16_t info_board[0x00AC];
PlayerInventory inventory;
uint8_t quest_data1[0x0208];
uint8_t quest_data2[0x0058];
uint8_t tech_menu_config[0x0028];
};
struct SavedAccountBB { // .nsa file format
char signature[0x40];
uint32_t blocked[0x001E];
GuildCardFileBB guild_cards;
KeyAndTeamConfigBB key_config;
uint32_t option_flags;
uint8_t shortcuts[0x0A40];
uint8_t symbol_chats[0x04E0];
char16_t team_name[0x0010];
};
// complete player info stored by the server
struct Player {
uint32_t loaded_from_shipgate_time;
char16_t auto_reply[0x00AC]; // player
PlayerBank bank; // player
char bank_name[0x20];
uint32_t blocked[0x001E]; // account
uint8_t challenge_data[0x0140]; // player
PlayerDispDataBB disp; // player
uint8_t ep3_config[0x2408];
char16_t guild_card_desc[0x58]; // player
GuildCardFileBB guild_cards; // account
PlayerInventoryItem identify_result;
char16_t info_board[0x00AC]; // player
PlayerInventory inventory; // player
KeyAndTeamConfigBB key_config; // account
uint32_t option_flags; // account
uint8_t quest_data1[0x0208]; // player
uint8_t quest_data2[0x0058]; // player
uint32_t serial_number;
std::vector<ItemData> current_shop_contents;
uint8_t shortcuts[0x0A40]; // account
uint8_t symbol_chats[0x04E0]; // account
char16_t team_name[0x0010]; // account
uint8_t tech_menu_config[0x0028]; // player
void load_player_data(const std::string& filename);
void save_player_data(const std::string& filename) const;
void load_account_data(const std::string& filename);
void save_account_data(const std::string& filename) const;
void import(const PSOPlayerDataPC& pd);
void import(const PSOPlayerDataGC& pd);
void import(const PSOPlayerDataBB& pd);
PlayerLobbyJoinDataPCGC export_lobby_data_pc() const;
PlayerLobbyJoinDataPCGC export_lobby_data_gc() const;
PlayerLobbyJoinDataBB export_lobby_data_bb() const;
PlayerBB export_bb_player_data() const;
ptext<char16_t, 0x00AC> auto_reply;
PlayerBank bank;
parray<uint8_t, 0x0140> challenge_data;
PlayerDispDataBB disp;
ptext<char16_t, 0x0058> guild_card_desc;
ptext<char16_t, 0x00AC> info_board;
PlayerInventory inventory;
parray<uint8_t, 0x0208> quest_data1;
parray<uint8_t, 0x0058> quest_data2;
parray<uint8_t, 0x0028> tech_menu_config;
void add_item(const PlayerInventoryItem& item);
void remove_item(uint32_t item_id, uint32_t amount, PlayerInventoryItem* item);
size_t find_item(uint32_t item_id);
PlayerInventoryItem remove_item(uint32_t item_id, uint32_t amount);
void print_inventory(FILE* stream) const;
} __attribute__((packed));
struct SavedAccountDataBB { // .nsa file format
ptext<char, 0x40> signature;
parray<le_uint32_t, 0x001E> blocked_senders;
GuildCardFileBB guild_cards;
KeyAndTeamConfigBB key_config;
le_uint32_t unused;
le_uint32_t option_flags;
parray<uint8_t, 0x0A40> shortcuts;
parray<uint8_t, 0x04E0> symbol_chats;
ptext<char16_t, 0x0010> team_name;
} __attribute__((packed));
class ClientGameData {
private:
std::shared_ptr<SavedAccountDataBB> account_data;
std::shared_ptr<SavedPlayerDataBB> player_data;
public:
uint32_t serial_number;
// The following fields are not saved, and are only used in certain situations
// Null unless the client is Episode 3 and has sent its config already
std::shared_ptr<Ep3Config> ep3_config;
// These are only used if the client is BB
std::string bb_username;
size_t bb_player_index;
PlayerInventoryItem identify_result;
std::vector<ItemData> shop_contents;
ClientGameData() : serial_number(0), bb_player_index(0) { }
~ClientGameData();
std::shared_ptr<SavedAccountDataBB> account(bool should_load = true);
std::shared_ptr<SavedPlayerDataBB> player(bool should_load = true);
std::shared_ptr<const SavedAccountDataBB> account() const;
std::shared_ptr<const SavedPlayerDataBB> player() const;
std::string account_data_filename() const;
std::string player_data_filename() const;
static std::string player_template_filename(uint8_t char_class);
void create_player(
const PlayerDispDataBBPreview& preview,
std::shared_ptr<const LevelTable> level_table);
void load_account_data();
void save_account_data() const;
void load_player_data();
void save_player_data() const;
void import_player(const PSOPlayerDataPC& pd);
void import_player(const PSOPlayerDataGC& pd);
void import_player(const PSOPlayerDataBB& pd);
// Note: this function is not const because it can cause player and account
// data to be loaded
PlayerBB export_player_bb();
};
uint32_t compute_guild_card_checksum(const void* data, size_t size);
std::string filename_for_player_bb(const std::string& username, uint8_t player_index);
std::string filename_for_bank_bb(const std::string& username, const char* bank_name);
std::string filename_for_class_template_bb(uint8_t char_class);
std::string filename_for_account_bb(const std::string& username);
template <typename DestT, typename SrcT = DestT>
DestT convert_player_disp_data(const SrcT&) {
static_assert(always_false<DestT, SrcT>::v,
"unspecialized strcpy_t should never be called");
}
template <>
inline PlayerDispDataPCGC convert_player_disp_data<PlayerDispDataPCGC>(
const PlayerDispDataPCGC& src) {
return src;
}
template <>
inline PlayerDispDataPCGC convert_player_disp_data<PlayerDispDataPCGC, PlayerDispDataBB>(
const PlayerDispDataBB& src) {
return src.to_pcgc();
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB, PlayerDispDataPCGC>(
const PlayerDispDataPCGC& src) {
return src.to_bb();
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
const PlayerDispDataBB& src) {
return src;
}
+1302
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
#include <string>
#include "ServerState.hh"
#include "ProxyServer.hh"
void process_proxy_command(
std::shared_ptr<ServerState> s,
ProxyServer::LinkedSession& session,
bool from_server,
uint16_t command,
uint32_t flag,
std::string& data);
+607 -435
View File
File diff suppressed because it is too large Load Diff
+172 -53
View File
@@ -5,12 +5,15 @@
#include <map>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <vector>
#include <string>
#include <memory>
#include <phosg/Filesystem.hh>
#include "PSOEncryption.hh"
#include "PSOProtocol.hh"
#include "ServerState.hh"
@@ -19,74 +22,190 @@ public:
ProxyServer() = delete;
ProxyServer(const ProxyServer&) = delete;
ProxyServer(ProxyServer&&) = delete;
ProxyServer(std::shared_ptr<struct event_base> base,
const struct sockaddr_storage& initial_destination, GameVersion version);
ProxyServer(
std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state);
virtual ~ProxyServer() = default;
void listen(int port);
void listen(uint16_t port, GameVersion version,
const struct sockaddr_storage* default_destination = nullptr);
void connect_client(struct bufferevent* bev);
void connect_client(struct bufferevent* bev, uint16_t server_port);
void send_to_client(const std::string& data);
void send_to_server(const std::string& data);
struct LinkedSession {
ProxyServer* server;
uint64_t id;
PrefixedLogger log;
void set_save_quests(bool save_quests);
std::unique_ptr<struct event, void(*)(struct event*)> timeout_event;
std::shared_ptr<const License> license;
Channel client_channel;
Channel server_channel;
uint16_t local_port;
struct sockaddr_storage next_destination;
uint8_t prev_server_command_bytes[6];
uint32_t remote_ip_crc;
bool enable_remote_ip_crc_patch;
GameVersion version;
uint32_t sub_version;
std::string character_name;
std::string login_command_bb;
uint32_t remote_guild_card_number;
parray<uint8_t, 0x20> remote_client_config_data;
ClientConfigBB newserv_client_config;
bool enable_chat_filter;
bool switch_assist;
bool infinite_hp;
bool infinite_tp;
bool save_files;
int64_t function_call_return_value; // -1 = don't block function calls
G_SwitchStateChanged_6x05 last_switch_enabled_command;
int16_t override_section_id;
int16_t override_lobby_event;
int16_t override_lobby_number;
struct LobbyPlayer {
uint32_t guild_card_number;
std::string name;
LobbyPlayer() : guild_card_number(0) { }
};
std::vector<LobbyPlayer> lobby_players;
size_t lobby_client_id;
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt;
struct SavingFile {
std::string basename;
std::string output_filename;
uint32_t remaining_bytes;
std::unique_ptr<FILE, std::function<void(FILE*)>> f;
SavingFile(
const std::string& basename,
const std::string& output_filename,
uint32_t remaining_bytes);
};
std::unordered_map<std::string, SavingFile> saving_files;
// TODO: This first constructor should be private
LinkedSession(
ProxyServer* server,
uint64_t id,
uint16_t local_port,
GameVersion version);
LinkedSession(
ProxyServer* server,
uint16_t local_port,
GameVersion version,
std::shared_ptr<const License> license,
const ClientConfigBB& newserv_client_config);
LinkedSession(
ProxyServer* server,
uint16_t local_port,
GameVersion version,
std::shared_ptr<const License> license,
const struct sockaddr_storage& next_destination);
LinkedSession(
ProxyServer* server,
uint64_t id,
uint16_t local_port,
GameVersion version,
const struct sockaddr_storage& next_destination);
void resume(
Channel&& client_channel,
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
uint32_t sub_version,
const std::string& character_name);
void resume(
Channel&& client_channel,
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
std::string&& login_command_bb);
void resume(Channel&& client_channel);
void resume_inner(
Channel&& client_channel,
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt);
void connect();
static void dispatch_on_timeout(evutil_socket_t fd, short what, void* ctx);
static void on_input(Channel& ch, uint16_t, uint32_t, std::string& msg);
static void on_error(Channel& ch, short events);
void on_timeout();
void disconnect();
bool is_connected() const;
};
std::shared_ptr<LinkedSession> get_session();
std::shared_ptr<LinkedSession> create_licensed_session(
std::shared_ptr<const License> l,
uint16_t local_port,
GameVersion version,
const ClientConfigBB& newserv_client_config);
void delete_session(uint64_t id);
size_t delete_disconnected_sessions();
private:
std::shared_ptr<struct event_base> base;
std::map<int, std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)>> listeners;
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> client_bev;
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> server_bev;
struct sockaddr_storage next_destination;
int listen_port;
GameVersion version;
struct ListeningSocket {
ProxyServer* server;
size_t header_size;
PSOCommandHeader client_input_header;
PSOCommandHeader server_input_header;
std::shared_ptr<PSOEncryption> client_input_crypt;
std::shared_ptr<PSOEncryption> client_output_crypt;
std::shared_ptr<PSOEncryption> server_input_crypt;
std::shared_ptr<PSOEncryption> server_output_crypt;
PrefixedLogger log;
uint16_t port;
scoped_fd fd;
std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)> listener;
GameVersion version;
struct sockaddr_storage default_destination;
struct SavingQuestFile {
std::string basename;
std::string output_filename;
uint32_t remaining_bytes;
std::unique_ptr<FILE, std::function<void(FILE*)>> f;
ListeningSocket(
ProxyServer* server,
uint16_t port,
GameVersion version,
const struct sockaddr_storage* default_destination);
SavingQuestFile(
const std::string& basename,
const std::string& output_filename,
uint32_t remaining_bytes);
static void dispatch_on_listen_accept(struct evconnlistener* listener,
evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx);
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
void on_listen_accept(int fd);
void on_listen_error();
};
bool save_quests;
std::unordered_map<std::string, SavingQuestFile> saving_quest_files;
void send_to_end(const std::string& data, bool to_server);
struct UnlinkedSession {
ProxyServer* server;
static void dispatch_on_listen_accept(struct evconnlistener* listener,
evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx);
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
static void dispatch_on_client_error(struct bufferevent* bev, short events,
void* ctx);
static void dispatch_on_server_input(struct bufferevent* bev, void* ctx);
static void dispatch_on_server_error(struct bufferevent* bev, short events,
void* ctx);
PrefixedLogger log;
Channel channel;
uint16_t local_port;
GameVersion version;
struct sockaddr_storage next_destination;
void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd,
struct sockaddr *address, int socklen);
void on_listen_error(struct evconnlistener* listener);
void on_client_input(struct bufferevent* bev);
void on_client_error(struct bufferevent* bev, short events);
void on_server_input(struct bufferevent* bev);
void on_server_error(struct bufferevent* bev, short events);
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt;
void on_client_connect(struct bufferevent* bev);
UnlinkedSession(ProxyServer* server, struct bufferevent* bev, uint16_t port, GameVersion version);
size_t get_size_field(const PSOCommandHeader* header);
size_t get_command_field(const PSOCommandHeader* header);
void receive_and_process_commands();
void receive_and_process_commands(bool from_server);
static void on_input(Channel& ch, uint16_t command, uint32_t flag, std::string& msg);
static void on_error(Channel& ch, short events);
};
PrefixedLogger log;
std::shared_ptr<struct event_base> base;
std::shared_ptr<ServerState> state;
std::map<int, std::shared_ptr<ListeningSocket>> listeners;
std::unordered_map<struct bufferevent*, std::shared_ptr<UnlinkedSession>> bev_to_unlinked_session;
std::unordered_map<uint64_t, std::shared_ptr<LinkedSession>> id_to_session;
uint64_t next_unlicensed_session_id;
void on_client_connect(
struct bufferevent* bev,
uint16_t listen_port,
GameVersion version,
const struct sockaddr_storage* default_destination);
};
-146
View File
@@ -1,146 +0,0 @@
#include "ProxyShell.hh"
#include <event2/event.h>
#include <stdio.h>
#include <phosg/Strings.hh>
using namespace std;
ProxyShell::ProxyShell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state,
std::shared_ptr<ProxyServer> proxy_server) : Shell(base, state),
proxy_server(proxy_server) { }
void ProxyShell::execute_command(const string& command) {
// find the entry in the command table and run the command
size_t command_end = skip_non_whitespace(command, 0);
size_t args_begin = skip_whitespace(command, command_end);
string command_name = command.substr(0, command_end);
string command_args = command.substr(args_begin);
if (command_name == "exit") {
throw exit_shell();
} else if (command_name == "help") {
fprintf(stderr, "\
Commands:\n\
help\n\
You\'re reading it now.\n\
exit (or ctrl+d)\n\
Shut down the proxy.\n\
sc <data>\n\
Send a command to the client.\n\
ss <data>\n\
Send a command to the server.\n\
chat <text>\n\
Send a chat message to the server.\n\
dchat <data>\n\
Send a chat message to the server with arbitrary data in it.\n\
info-board <text>\n\
Set your info board contents.\n\
info-board-data <data>\n\
Set your info board contents with arbitrary data.\n\
marker <color-id>\n\
Send a lobby marker message to the server.\n\
event <event-id>\n\
Send a lobby event update to yourself.\n\
ship\n\
Request the ship select menu from the server.\n\
");
} else if ((command_name == "sc") || (command_name == "ss")) {
bool to_client = (command_name[1] == 'c');
string data = parse_data_string(command_args);
if (data.size() & 3) {
throw invalid_argument("data size is not a multiple of 4");
}
if (data.size() == 0) {
throw invalid_argument("no data given");
}
uint16_t* size_field = reinterpret_cast<uint16_t*>(data.data() + 2);
*size_field = data.size();
log(INFO, "%s (from proxy):", to_client ? "server" : "client");
print_data(stderr, data);
if (to_client) {
this->proxy_server->send_to_client(data);
} else {
this->proxy_server->send_to_server(data);
}
} else if ((command_name == "chat") || (command_name == "dchat")) {
string data(12, '\0');
data[0] = 0x06;
data.push_back('\x09');
data.push_back('E');
if (command_name == "dchat") {
data += parse_data_string(command_args);
} else {
data += command_args;
}
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
uint16_t* size_field = reinterpret_cast<uint16_t*>(data.data() + 2);
*size_field = data.size();
log(INFO, "Client (from proxy):");
print_data(stderr, data);
this->proxy_server->send_to_server(data);
} else if (command_name == "marker") {
string data("\x89\x00\x04\x00", 4);
data[1] = stod(command_args);
log(INFO, "Client (from proxy):");
print_data(stderr, data);
this->proxy_server->send_to_server(data);
} else if (command_name == "event") {
string data("\xDA\x00\x04\x00", 4);
data[1] = stod(command_args);
log(INFO, "Server (from proxy):");
print_data(stderr, data);
this->proxy_server->send_to_client(data);
} else if (command_name == "ship") {
static const string data("\xA0\x00\x04\x00", 4);
log(INFO, "Server (from proxy):");
print_data(stderr, data);
this->proxy_server->send_to_server(data);
} else if ((command_name == "info-board") || (command_name == "info-board-data")) {
string data(4, '\0');
data[0] = 0xD9;
if (command_name == "info-board-data") {
data += parse_data_string(command_args);
} else {
data += command_args;
}
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
uint16_t* size_field = reinterpret_cast<uint16_t*>(data.data() + 2);
*size_field = data.size();
log(INFO, "Client (from proxy):");
print_data(stderr, data);
this->proxy_server->send_to_server(data);
} else if (command_name == "set-save-quests") {
if (command_args == "on") {
this->proxy_server->set_save_quests(true);
} else if (command_args == "off") {
this->proxy_server->set_save_quests(false);
} else {
throw invalid_argument("argument must be \"on\" or \"off\"");
}
} else {
throw invalid_argument("unknown command; try \'help\'");
}
}
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include <memory>
#include <string>
#include <event2/event.h>
#include "Shell.hh"
#include "ProxyServer.hh"
class ProxyShell : public Shell {
public:
ProxyShell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state,
std::shared_ptr<ProxyServer> proxy_server);
virtual ~ProxyShell() = default;
ProxyShell(const ProxyShell&) = delete;
ProxyShell(ProxyShell&&) = delete;
ProxyShell& operator=(const ProxyShell&) = delete;
ProxyShell& operator=(ProxyShell&&) = delete;
protected:
std::shared_ptr<ProxyServer> proxy_server;
virtual void execute_command(const std::string& command);
};
+340 -124
View File
@@ -8,6 +8,7 @@
#include <phosg/Random.hh>
#include <phosg/Strings.hh>
#include "CommandFormats.hh"
#include "Compression.hh"
#include "PSOEncryption.hh"
#include "Text.hh"
@@ -19,54 +20,69 @@ using namespace std;
struct PSODownloadQuestHeader {
// When sending a DLQ to the client, this is the DECOMPRESSED size. When
// reading it from a GCI file, this is the COMPRESSED size.
be_uint32_t size;
le_uint32_t size;
// Note: use PSO PC encryption, even for GC quests.
be_uint32_t encryption_seed;
};
le_uint32_t encryption_seed;
} __attribute__((packed));
bool category_is_mode(QuestCategory category) {
return (category == QuestCategory::Battle) ||
(category == QuestCategory::Challenge) ||
(category == QuestCategory::Episode3);
return (category == QuestCategory::BATTLE) ||
(category == QuestCategory::CHALLENGE) ||
(category == QuestCategory::EPISODE_3);
}
const char* name_for_category(QuestCategory category) {
switch (category) {
case QuestCategory::Retrieval:
case QuestCategory::RETRIEVAL:
return "Retrieval";
case QuestCategory::Extermination:
case QuestCategory::EXTERMINATION:
return "Extermination";
case QuestCategory::Event:
case QuestCategory::EVENT:
return "Event";
case QuestCategory::Shop:
case QuestCategory::SHOP:
return "Shop";
case QuestCategory::VR:
return "VR";
case QuestCategory::Tower:
case QuestCategory::TOWER:
return "Tower";
case QuestCategory::GovernmentEpisode1:
case QuestCategory::GOVERNMENT_EPISODE_1:
return "GovernmentEpisode1";
case QuestCategory::GovernmentEpisode2:
case QuestCategory::GOVERNMENT_EPISODE_2:
return "GovernmentEpisode2";
case QuestCategory::GovernmentEpisode4:
case QuestCategory::GOVERNMENT_EPISODE_4:
return "GovernmentEpisode4";
case QuestCategory::Download:
case QuestCategory::DOWNLOAD:
return "Download";
case QuestCategory::Battle:
case QuestCategory::BATTLE:
return "Battle";
case QuestCategory::Challenge:
case QuestCategory::CHALLENGE:
return "Challenge";
case QuestCategory::Solo:
case QuestCategory::SOLO:
return "Solo";
case QuestCategory::Episode3:
case QuestCategory::EPISODE_3:
return "Episode3";
default:
return "Unknown";
}
}
static const char* name_for_episode(uint8_t episode) {
switch (episode) {
case 0:
return "Ep1";
case 1:
return "Ep2";
case 2:
return "Ep4";
case 0xFF:
return "Ep3";
default:
return "InvalidEpisode";
}
}
struct PSOQuestHeaderDC { // same for dc v1 and v2, thankfully
@@ -77,10 +93,10 @@ struct PSOQuestHeaderDC { // same for dc v1 and v2, thankfully
uint8_t is_download;
uint8_t unknown1;
uint16_t quest_number; // 0xFFFF for challenge quests
char name[0x20];
char short_description[0x80];
char long_description[0x120];
};
ptext<char, 0x20> name;
ptext<char, 0x80> short_description;
ptext<char, 0x120> long_description;
} __attribute__((packed));
struct PSOQuestHeaderPC {
uint32_t start_offset;
@@ -90,10 +106,10 @@ struct PSOQuestHeaderPC {
uint8_t is_download;
uint8_t unknown1;
uint16_t quest_number; // 0xFFFF for challenge quests
char16_t name[0x20];
char16_t short_description[0x80];
char16_t long_description[0x120];
};
ptext<char16_t, 0x20> name;
ptext<char16_t, 0x80> short_description;
ptext<char16_t, 0x120> long_description;
} __attribute__((packed));
struct PSOQuestHeaderGC {
uint32_t start_offset;
@@ -104,22 +120,22 @@ struct PSOQuestHeaderGC {
uint8_t unknown1;
uint8_t quest_number;
uint8_t episode; // 1 = ep2. apparently some quests have 0xFF here, which means ep1 (?)
char name[0x20];
char short_description[0x80];
char long_description[0x120];
};
ptext<char, 0x20> name;
ptext<char, 0x80> short_description;
ptext<char, 0x120> long_description;
} __attribute__((packed));
struct PSOQuestHeaderGCEpisode3 {
// there's actually a lot of other important stuff in here but I'm lazy. it
// looks like map data, cutscene data, and maybe special cards used during
// the quest
uint8_t unused[0x1DF0];
char name[0x14];
char location[0x14];
char location2[0x3C];
char description[0x190];
uint8_t unused2[0x3A34];
};
parray<uint8_t, 0x1DF0> unknown_a1;
ptext<char, 0x14> name;
ptext<char, 0x14> location;
ptext<char, 0x3C> location2;
ptext<char, 0x190> description;
parray<uint8_t, 0x3A34> unknown_a2;
} __attribute__((packed));
struct PSOQuestHeaderBB {
uint32_t start_offset;
@@ -132,24 +148,31 @@ struct PSOQuestHeaderBB {
uint8_t max_players;
uint8_t joinable_in_progress;
uint8_t unknown;
char16_t name[0x20];
char16_t short_description[0x80];
char16_t long_description[0x120];
};
ptext<char16_t, 0x20> name;
ptext<char16_t, 0x80> short_description;
ptext<char16_t, 0x120> long_description;
} __attribute__((packed));
Quest::Quest(const string& bin_filename)
: quest_id(-1),
category(QuestCategory::Unknown),
: internal_id(-1),
menu_item_id(0),
category(QuestCategory::UNKNOWN),
episode(0),
is_dcv1(false),
joinable(false),
gci_format(false) {
file_format(FileFormat::BIN_DAT) {
if (ends_with(bin_filename, ".bin.gci")) {
this->gci_format = true;
this->file_format = FileFormat::BIN_DAT_GCI;
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
} else if (ends_with(bin_filename, ".bin.dlq")) {
this->file_format = FileFormat::BIN_DAT_DLQ;
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
} else if (ends_with(bin_filename, ".qst")) {
this->file_format = FileFormat::QST;
this->file_basename = bin_filename.substr(0, bin_filename.size() - 4);
} else if (ends_with(bin_filename, ".bin")) {
this->file_basename = bin_filename.substr(0, bin_filename.size() - 4);
} else {
@@ -165,7 +188,9 @@ Quest::Quest(const string& bin_filename)
basename = bin_filename;
}
}
basename.resize(basename.size() - (this->gci_format ? 8 : 4));
bool has_short_extension = (this->file_format == FileFormat::BIN_DAT) ||
(this->file_format == FileFormat::QST);
basename.resize(basename.size() - (has_short_extension ? 4 : 8));
// quest filenames are like:
// b###-VV.bin for battle mode
@@ -178,11 +203,11 @@ Quest::Quest(const string& bin_filename)
}
if (basename[0] == 'b') {
this->category = QuestCategory::Battle;
this->category = QuestCategory::BATTLE;
} else if (basename[0] == 'c') {
this->category = QuestCategory::Challenge;
this->category = QuestCategory::CHALLENGE;
} else if (basename[0] == 'e') {
this->category = QuestCategory::Episode3;
this->category = QuestCategory::EPISODE_3;
} else if (basename[0] != 'q') {
throw invalid_argument("filename does not indicate mode");
}
@@ -190,38 +215,29 @@ Quest::Quest(const string& bin_filename)
// if the quest category is still unknown, expect 3 tokens (one of them will
// tell us the category)
vector<string> tokens = split(basename, '-');
if (tokens.size() != (2 + (this->category == QuestCategory::Unknown))) {
if (tokens.size() != (2 + (this->category == QuestCategory::UNKNOWN))) {
throw invalid_argument("incorrect filename format");
}
// parse the number out of the first token
this->quest_id = strtoull(tokens[0].c_str() + 1, nullptr, 10);
this->internal_id = strtoull(tokens[0].c_str() + 1, nullptr, 10);
// get the category from the second token if needed
if (this->category == QuestCategory::Unknown) {
if (tokens[1] == "gov") {
if (this->episode == 0) {
this->category = QuestCategory::GovernmentEpisode1;
} else if (this->episode == 1) {
this->category = QuestCategory::GovernmentEpisode2;
} else if (this->episode == 2) {
this->category = QuestCategory::GovernmentEpisode4;
} else {
throw invalid_argument("government quest has incorrect episode");
}
} else {
static const unordered_map<std::string, QuestCategory> name_to_category({
{"ret", QuestCategory::Retrieval},
{"ext", QuestCategory::Extermination},
{"evt", QuestCategory::Event},
{"shp", QuestCategory::Shop},
{"vr", QuestCategory::VR},
{"twr", QuestCategory::Tower},
{"dl", QuestCategory::Download},
{"1p", QuestCategory::Solo},
});
this->category = name_to_category.at(tokens[1]);
}
if (this->category == QuestCategory::UNKNOWN) {
static const unordered_map<std::string, QuestCategory> name_to_category({
{"ret", QuestCategory::RETRIEVAL},
{"ext", QuestCategory::EXTERMINATION},
{"evt", QuestCategory::EVENT},
{"shp", QuestCategory::SHOP},
{"vr", QuestCategory::VR},
{"twr", QuestCategory::TOWER},
// Note: This will be overwritten later for Episode 2 & 4 quests - we
// haven't parsed the episode from the quest script yet
{"gov", QuestCategory::GOVERNMENT_EPISODE_1},
{"dl", QuestCategory::DOWNLOAD},
{"1p", QuestCategory::SOLO},
});
this->category = name_to_category.at(tokens[1]);
tokens.erase(tokens.begin() + 1);
}
@@ -242,7 +258,7 @@ Quest::Quest(const string& bin_filename)
auto bin_decompressed = prs_decompress(*bin_compressed);
switch (this->version) {
case GameVersion::Patch:
case GameVersion::PATCH:
throw invalid_argument("patch server quests are not valid");
break;
@@ -274,7 +290,7 @@ Quest::Quest(const string& bin_filename)
}
case GameVersion::GC: {
if (this->category == QuestCategory::Episode3) {
if (this->category == QuestCategory::EPISODE_3) {
// these all appear to be the same size
if (bin_decompressed.size() != sizeof(PSOQuestHeaderGCEpisode3)) {
throw invalid_argument("file is incorrect size");
@@ -309,9 +325,20 @@ Quest::Quest(const string& bin_filename)
this->name = header->name;
this->short_description = header->short_description;
this->long_description = header->long_description;
if (this->category == QuestCategory::GOVERNMENT_EPISODE_1) {
if (this->episode == 1) {
this->category = QuestCategory::GOVERNMENT_EPISODE_2;
} else if (this->episode == 2) {
this->category = QuestCategory::GOVERNMENT_EPISODE_4;
} else if (this->episode != 0) {
throw invalid_argument("government quest has invalid episode number");
}
}
break;
}
default:
throw logic_error("invalid quest game version");
}
}
@@ -333,10 +360,24 @@ std::string Quest::dat_filename() const {
shared_ptr<const string> Quest::bin_contents() const {
if (!this->bin_contents_ptr) {
if (this->gci_format) {
this->bin_contents_ptr.reset(new string(this->decode_gci(this->file_basename + ".bin.gci")));
} else {
this->bin_contents_ptr.reset(new string(load_file(this->file_basename + ".bin")));
switch (this->file_format) {
case FileFormat::BIN_DAT:
this->bin_contents_ptr.reset(new string(load_file(this->file_basename + ".bin")));
break;
case FileFormat::BIN_DAT_GCI:
this->bin_contents_ptr.reset(new string(this->decode_gci(this->file_basename + ".bin.gci")));
break;
case FileFormat::BIN_DAT_DLQ:
this->bin_contents_ptr.reset(new string(this->decode_dlq(this->file_basename + ".bin.dlq")));
break;
case FileFormat::QST: {
auto result = this->decode_qst(this->file_basename + ".qst");
this->bin_contents_ptr.reset(new string(move(result.first)));
this->dat_contents_ptr.reset(new string(move(result.second)));
break;
}
default:
throw logic_error("invalid quest file format");
}
}
return this->bin_contents_ptr;
@@ -344,10 +385,24 @@ shared_ptr<const string> Quest::bin_contents() const {
shared_ptr<const string> Quest::dat_contents() const {
if (!this->dat_contents_ptr) {
if (this->gci_format) {
this->dat_contents_ptr.reset(new string(this->decode_gci(this->file_basename + ".dat.gci")));
} else {
this->dat_contents_ptr.reset(new string(load_file(this->file_basename + ".dat")));
switch (this->file_format) {
case FileFormat::BIN_DAT:
this->dat_contents_ptr.reset(new string(load_file(this->file_basename + ".dat")));
break;
case FileFormat::BIN_DAT_GCI:
this->dat_contents_ptr.reset(new string(this->decode_gci(this->file_basename + ".dat.gci")));
break;
case FileFormat::BIN_DAT_DLQ:
this->dat_contents_ptr.reset(new string(this->decode_dlq(this->file_basename + ".dat.dlq")));
break;
case FileFormat::QST: {
auto result = this->decode_qst(this->file_basename + ".qst");
this->bin_contents_ptr.reset(new string(move(result.first)));
this->dat_contents_ptr.reset(new string(move(result.second)));
break;
}
default:
throw logic_error("invalid quest file format");
}
}
return this->dat_contents_ptr;
@@ -375,7 +430,7 @@ string Quest::decode_gci(const string& filename) {
uint32_t unknown2;
uint32_t decompressed_size;
uint32_t unknown4;
};
} __attribute__((packed));
if (compressed_data_with_header.size() < sizeof(DecryptedHeader)) {
throw runtime_error("GCI file compressed data truncated during header");
}
@@ -400,12 +455,154 @@ string Quest::decode_gci(const string& filename) {
return data_to_decompress;
}
string Quest::decode_dlq(const string& filename) {
uint32_t decompressed_size;
uint32_t key;
string data;
{
auto f = fopen_unique(filename, "rb");
decompressed_size = freadx<le_uint32_t>(f.get());
key = freadx<le_uint32_t>(f.get());
data = read_all(f.get());
}
PSOPCEncryption encr(key);
// The compressed data size does not need to be a multiple of 4, but the PC
// encryption (which is used for all download quests, even in V3) requires the
// data size to be a multiple of 4. We'll just temporarily stick a few bytes
// on the end, then throw them away later if needed.
size_t original_size = data.size();
data.resize((data.size() + 3) & (~3));
encr.decrypt(data);
data.resize(original_size);
if (prs_decompress_size(data) != decompressed_size) {
throw runtime_error("decompressed size does not match size in header");
}
return data;
}
template <typename HeaderT, typename OpenFileT>
static pair<string, string> decode_qst_t(FILE* f) {
string qst_data = read_all(f);
StringReader r(qst_data);
string bin_contents;
string dat_contents;
string internal_bin_filename;
string internal_dat_filename;
uint32_t bin_file_size = 0;
uint32_t dat_file_size = 0;
while (!r.eof()) {
// Handle BB's implicit 8-byte command alignment
static constexpr size_t alignment = sizeof(HeaderT);
size_t next_command_offset = (r.where() + (alignment - 1)) & ~(alignment - 1);
r.go(next_command_offset);
if (r.eof()) {
break;
}
const auto& header = r.get<HeaderT>();
if (header.command == 0x44) {
if (header.size != sizeof(HeaderT) + sizeof(OpenFileT)) {
throw runtime_error("qst open file command has incorrect size");
}
const auto& cmd = r.get<OpenFileT>(f);
string internal_filename = cmd.filename;
if (ends_with(internal_filename, ".bin")) {
if (internal_bin_filename.empty()) {
internal_bin_filename = internal_filename;
} else {
throw runtime_error("qst contains multiple bin files");
}
bin_file_size = cmd.file_size;
} else if (ends_with(internal_filename, ".dat")) {
if (internal_dat_filename.empty()) {
internal_dat_filename = internal_filename;
} else {
throw runtime_error("qst contains multiple dat files");
}
dat_file_size = cmd.file_size;
} else {
throw runtime_error("qst contains non-bin, non-dat file");
}
} else if (header.command == 0x13) {
if (header.size != sizeof(HeaderT) + sizeof(S_WriteFile_13_A7)) {
throw runtime_error("qst write file command has incorrect size");
}
const auto& cmd = r.get<S_WriteFile_13_A7>();
string filename = cmd.filename;
string* dest = nullptr;
if (filename == internal_bin_filename) {
dest = &bin_contents;
} else if (filename == internal_dat_filename) {
dest = &dat_contents;
} else {
throw runtime_error("qst contains write commnd for non-open file");
}
if (cmd.data_size > 0x400) {
throw runtime_error("qst contains invalid write command");
}
if (dest->size() & 0x3FF) {
throw runtime_error("qst contains uneven chunks out of order");
}
if (header.flag != dest->size() / 0x400) {
throw runtime_error("qst contains chunks out of order");
}
dest->append(reinterpret_cast<const char*>(cmd.data), cmd.data_size);
} else {
throw runtime_error("invalid command in qst file");
}
}
if (bin_contents.size() != bin_file_size) {
throw runtime_error("bin file does not match expected size");
}
if (dat_contents.size() != dat_file_size) {
throw runtime_error("dat file does not match expected size");
}
return make_pair(bin_contents, dat_contents);
}
pair<string, string> Quest::decode_qst(const string& filename) {
auto f = fopen_unique(filename, "rb");
// qst files start with an open file command, but the format differs depending
// on the PSO version that the qst file is for. We can detect the format from
// the first 4 bytes in the file:
// - BB: 58 00 44 00
// - PC: 3C ?? 44 00
// - DC/GC: 44 ?? 3C 00
uint32_t signature = freadx<be_uint32_t>(f.get());
fseek(f.get(), 0, SEEK_SET);
if (signature == 0x58004400) {
return decode_qst_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(f.get());
} else if ((signature & 0xFF00FFFF) == 0x3C004400) {
return decode_qst_t<PSOCommandHeaderPC, S_OpenFile_PC_GC_44_A6>(f.get());
} else if ((signature & 0xFF00FFFF) == 0x44003C00) {
return decode_qst_t<PSOCommandHeaderDCGC, S_OpenFile_PC_GC_44_A6>(f.get());
} else {
throw runtime_error("invalid qst file format");
}
}
QuestIndex::QuestIndex(const char* directory) : directory(directory) {
QuestIndex::QuestIndex(const std::string& directory) : directory(directory) {
auto filename_set = list_directory(this->directory);
vector<string> filenames(filename_set.begin(), filename_set.end());
sort(filenames.begin(), filenames.end());
uint32_t next_menu_item_id = 1;
for (const auto& filename : filenames) {
string full_path = this->directory + "/" + filename;
@@ -415,15 +612,21 @@ QuestIndex::QuestIndex(const char* directory) : directory(directory) {
continue;
}
if (ends_with(filename, ".bin") || ends_with(filename, ".bin.gci")) {
if (ends_with(filename, ".bin") ||
ends_with(filename, ".bin.gci") ||
ends_with(filename, ".bin.dlq") ||
ends_with(filename, ".qst")) {
try {
shared_ptr<Quest> q(new Quest(full_path));
this->version_id_to_quest.emplace(make_pair(q->version, q->quest_id), q);
this->version_name_to_quest.emplace(make_pair(q->version, q->name), q);
q->menu_item_id = next_menu_item_id++;
string ascii_name = encode_sjis(q->name);
log(INFO, "Indexed quest %s (%s-%" PRId64 ", %s, episode=%hhu, joinable=%s, dcv1=%s)",
ascii_name.c_str(), name_for_version(q->version), q->quest_id,
name_for_category(q->category), q->episode,
if (!this->version_menu_item_id_to_quest.emplace(
make_pair(q->version, q->menu_item_id), q).second) {
throw logic_error("duplicate quest menu item id");
}
log(INFO, "Indexed quest %s (%s-%" PRId64 " => %" PRIu32 ", %s, %s, joinable=%s, dcv1=%s)",
ascii_name.c_str(), name_for_version(q->version), q->internal_id,
q->menu_item_id, name_for_category(q->category), name_for_episode(q->episode),
q->joinable ? "true" : "false", q->is_dcv1 ? "true" : "false");
} catch (const exception& e) {
log(WARNING, "Failed to parse quest file %s (%s)", filename.c_str(), e.what());
@@ -433,8 +636,8 @@ QuestIndex::QuestIndex(const char* directory) : directory(directory) {
}
shared_ptr<const Quest> QuestIndex::get(GameVersion version,
uint32_t id) const {
return this->version_id_to_quest.at(make_pair(version, id));
uint32_t menu_item_id) const {
return this->version_menu_item_id_to_quest.at(make_pair(version, menu_item_id));
}
shared_ptr<const string> QuestIndex::get_gba(const string& name) const {
@@ -442,9 +645,9 @@ shared_ptr<const string> QuestIndex::get_gba(const string& name) const {
}
vector<shared_ptr<const Quest>> QuestIndex::filter(GameVersion version,
bool is_dcv1, QuestCategory category, int16_t episode) const {
auto it = this->version_id_to_quest.lower_bound(make_pair(version, 0));
auto end_it = this->version_id_to_quest.upper_bound(make_pair(version, 0xFFFFFFFF));
bool is_dcv1, QuestCategory category) const {
auto it = this->version_menu_item_id_to_quest.lower_bound(make_pair(version, 0));
auto end_it = this->version_menu_item_id_to_quest.upper_bound(make_pair(version, 0xFFFFFFFF));
vector<shared_ptr<const Quest>> ret;
for (; it != end_it; it++) {
@@ -452,14 +655,6 @@ vector<shared_ptr<const Quest>> QuestIndex::filter(GameVersion version,
if ((q->is_dcv1 != is_dcv1) || (q->category != category)) {
continue;
}
// Only check episode and solo if the category isn't a mode (that is, ignore
// episode if querying for battle/challenge/solo quests). Also, ignore
// ignore episode if it's < 0 (e.g. for the download quest menu).
if ((episode >= 0) && !category_is_mode(category) && ((q->episode != episode))) {
continue;
}
ret.emplace_back(q);
}
@@ -469,38 +664,60 @@ vector<shared_ptr<const Quest>> QuestIndex::filter(GameVersion version,
static string create_download_quest_file(const string& compressed_data,
size_t decompressed_size) {
size_t decompressed_size, uint32_t encryption_seed = 0) {
// Download quest files are like normal (PRS-compressed) quest files, but they
// are encrypted with the PSOPC encryption (even on V3 / PSO GC), and a small
// header (PSODownloadQuestHeader) is prepended to the encrypted data.
if (encryption_seed == 0) {
encryption_seed = random_object<uint32_t>();
}
string data(8, '\0');
auto* header = reinterpret_cast<PSODownloadQuestHeader*>(data.data());
header->size = decompressed_size + sizeof(PSODownloadQuestHeader);
header->encryption_seed = random_object<uint32_t>();
header->size = decompressed_size;
header->encryption_seed = encryption_seed;
data += compressed_data;
// add extra bytes if necessary so encryption won't fail
// Add temporary extra bytes if necessary so encryption won't fail - the data
// size must be a multiple of 4 for PSO PC encryption.
size_t original_size = data.size();
data.resize((data.size() + 3) & (~3));
// TODO: for DC quests, do we use DC encryption?
PSOPCEncryption encr(header->encryption_seed);
PSOPCEncryption encr(encryption_seed);
encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader),
data.size() - sizeof(PSODownloadQuestHeader));
data.resize(original_size);
return data;
}
shared_ptr<Quest> Quest::create_download_quest() const {
if (this->category == QuestCategory::Download) {
throw invalid_argument("quest is already a download quest");
}
// The download flag needs to be set in the bin header, or else the client
// will ignore it when scanning for download quests in an offline game. To set
// this flag, we need to decompress the quest's .bin file, set the flag, then
// recompress it again.
string decompressed_bin = prs_decompress(*this->bin_contents());
void* data_ptr = decompressed_bin.data();
switch (this->version) {
case GameVersion::DC:
if (decompressed_bin.size() < sizeof(PSOQuestHeaderDC)) {
throw runtime_error("bin file is too small for header");
}
reinterpret_cast<PSOQuestHeaderDC*>(data_ptr)->is_download = 0x01;
break;
case GameVersion::PC:
if (decompressed_bin.size() < sizeof(PSOQuestHeaderPC)) {
throw runtime_error("bin file is too small for header");
}
reinterpret_cast<PSOQuestHeaderPC*>(data_ptr)->is_download = 0x01;
break;
case GameVersion::GC:
if (decompressed_bin.size() < sizeof(PSOQuestHeaderGC)) {
throw runtime_error("bin file is too small for header");
}
reinterpret_cast<PSOQuestHeaderGC*>(data_ptr)->is_download = 0x01;
break;
case GameVersion::BB:
@@ -509,15 +726,14 @@ shared_ptr<Quest> Quest::create_download_quest() const {
throw invalid_argument("unknown game version");
}
string compressed_bin = prs_compress(decompressed_bin);
// We'll create a new Quest object with appropriately-processed .bin and .dat
// file contents.
shared_ptr<Quest> dlq(new Quest(*this));
dlq->category = QuestCategory::Download;
dlq->bin_contents_ptr.reset(new string(create_download_quest_file(
prs_compress(decompressed_bin), decompressed_bin.size())));
auto dat_contents = this->dat_contents();
compressed_bin, decompressed_bin.size())));
dlq->dat_contents_ptr.reset(new string(create_download_quest_file(
*dat_contents, prs_decompress_size(*dat_contents))));
*this->dat_contents(), prs_decompress_size(*this->dat_contents()))));
return dlq;
}
+35 -27
View File
@@ -12,21 +12,21 @@
enum class QuestCategory {
Unknown = -1,
Retrieval = 0,
Extermination,
Event,
Shop,
UNKNOWN = -1,
RETRIEVAL = 0,
EXTERMINATION,
EVENT,
SHOP,
VR,
Tower,
GovernmentEpisode1,
GovernmentEpisode2,
GovernmentEpisode4,
Download,
Battle,
Challenge,
Solo,
Episode3,
TOWER,
GOVERNMENT_EPISODE_1,
GOVERNMENT_EPISODE_2,
GOVERNMENT_EPISODE_4,
DOWNLOAD,
BATTLE,
CHALLENGE,
SOLO,
EPISODE_3,
};
bool category_is_mode(QuestCategory category);
@@ -35,26 +35,26 @@ const char* name_for_category(QuestCategory category);
class Quest {
private:
static std::string decode_gci(const std::string& filename);
public:
int64_t quest_id;
enum class FileFormat {
BIN_DAT = 0,
BIN_DAT_GCI,
BIN_DAT_DLQ,
QST,
};
int64_t internal_id;
uint32_t menu_item_id;
QuestCategory category;
uint8_t episode; // 0 = ep1, 1 = ep2, 2 = ep4, 0xFF = ep3
bool is_dcv1;
bool joinable;
GameVersion version;
std::string file_basename; // we append -<version>.<bin/dat> when reading
bool gci_format;
FileFormat file_format;
std::u16string name;
std::u16string short_description;
std::u16string long_description;
// these are populated when requested
mutable std::shared_ptr<std::string> bin_contents_ptr;
mutable std::shared_ptr<std::string> dat_contents_ptr;
Quest(const std::string& file_basename);
Quest(const Quest&) = default;
Quest(Quest&&) = default;
@@ -68,22 +68,30 @@ public:
std::shared_ptr<const std::string> dat_contents() const;
std::shared_ptr<Quest> create_download_quest() const;
static std::string decode_gci(const std::string& filename);
static std::string decode_dlq(const std::string& filename);
static std::pair<std::string, std::string> decode_qst(const std::string& filename);
private:
// these are populated when requested
mutable std::shared_ptr<std::string> bin_contents_ptr;
mutable std::shared_ptr<std::string> dat_contents_ptr;
};
struct QuestIndex {
std::string directory;
std::map<std::pair<GameVersion, uint64_t>, std::shared_ptr<Quest>> version_id_to_quest;
std::map<std::pair<GameVersion, std::u16string>, std::shared_ptr<Quest>> version_name_to_quest;
std::map<std::pair<GameVersion, uint64_t>, std::shared_ptr<Quest>> version_menu_item_id_to_quest;
std::map<std::string, std::vector<std::shared_ptr<Quest>>> category_to_quests;
std::map<std::string, std::shared_ptr<std::string>> gba_file_contents;
QuestIndex(const char* directory);
QuestIndex(const std::string& directory);
std::shared_ptr<const Quest> get(GameVersion version, uint32_t id) const;
std::shared_ptr<const std::string> get_gba(const std::string& name) const;
std::vector<std::shared_ptr<const Quest>> filter(GameVersion version,
bool is_dcv1, QuestCategory category, int16_t episode) const;
bool is_dcv1, QuestCategory category) const;
};
+7 -2
View File
@@ -7,9 +7,14 @@
struct RareItemDrop {
uint8_t probability;
uint8_t item_code[3];
};
} __attribute__((packed));
struct RareItemSet {
// TODO: It looks like this structure can actually vary. We see the offsets
// 0194 and 01B2 in the unused section, along with the value 1E (number of box
// rares). In PSOGC, these all appear to be the same size/format, but that's
// probably not strictly required to be the case.
// 0x280 in size; describes one difficulty, section ID, and episode
RareItemDrop rares[0x65]; // 0000 - 0194 in file
uint8_t box_areas[0x1E]; // 0194 - 01B2 in file
RareItemDrop box_rares[0x1E]; // 01B2 - 022A in file
@@ -17,6 +22,6 @@ struct RareItemSet {
RareItemSet(const char* filename, uint8_t episode, uint8_t difficulty,
uint8_t secid);
}; // 0x280 in size; describes one difficulty, section ID, and episode
} __attribute__((packed));
bool sample_rare_item(uint8_t pc);
+1154 -912
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -5,8 +5,9 @@
#include "ServerState.hh"
void process_connect(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void process_disconnect(std::shared_ptr<ServerState> s,
std::shared_ptr<Client> c);
void process_command(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c,
uint16_t command, uint32_t flag, uint16_t size, const void* data);
uint16_t command, uint32_t flag, const std::string& data);
+882 -692
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -7,8 +7,9 @@
#include "ServerState.hh"
void check_size(uint16_t size, uint16_t min_size, uint16_t max_size = 0);
void process_subcommand(std::shared_ptr<ServerState> s,
std::shared_ptr<Lobby> l, std::shared_ptr<Client> c, uint8_t command,
uint8_t flag, const PSOSubcommand* sub, size_t count);
uint8_t flag, const std::string& data);
bool subcommand_is_implemented(uint8_t which);
+749 -1523
View File
File diff suppressed because it is too large Load Diff
+122 -65
View File
@@ -13,72 +13,109 @@
#include "Menu.hh"
#include "Quest.hh"
#include "Text.hh"
#include "CommandFormats.hh"
#include "FunctionCompiler.hh"
#define MAIN_MENU_ID 0x60000000
#define INFORMATION_MENU_ID 0x60000030
#define LOBBY_MENU_ID 0x60000060
#define GAME_MENU_ID 0x60000090
#define QUEST_MENU_ID 0x600000C0
#define QUEST_FILTER_MENU_ID 0x600000F0
#define MAIN_MENU_GO_TO_LOBBY 0x00000001
#define MAIN_MENU_INFORMATION 0x00000002
#define MAIN_MENU_DOWNLOAD_QUESTS 0x00000003
#define MAIN_MENU_DISCONNECT 0x00000004
#define INFORMATION_MENU_GO_BACK 0xFFFFFFFF
// TODO: Many of these functions should take a Channel& instead of a
// shared_ptr<Client>. Refactor functions appropriately.
// Note: There are so many versions of this function for a few reasons:
// - There are a lot of different target types (sometimes we want to send a
// command to one client, sometimes to everyone in a lobby, etc.)
// - For the const void* versions, the data and size arguments should not be
// independently optional - this can lead to bugs where a non-null data
// pointer is given but size is accidentally not given (e.g. if the type of
// data in the calling function is changed from string to void*).
void send_command(Channel& ch, uint16_t command, uint32_t flag,
const void* data, size_t size);
void send_command(std::shared_ptr<Client> c, uint16_t command,
uint32_t flag = 0, const void* data = nullptr, size_t size = 0);
uint32_t flag, const void* data, size_t size);
inline void send_command(std::shared_ptr<Client> c, uint16_t command,
uint32_t flag) {
send_command(c, command, flag, nullptr, 0);
}
void send_command_excluding_client(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c, uint16_t command, uint32_t flag = 0,
const void* data = nullptr, size_t size = 0);
std::shared_ptr<Client> c, uint16_t command, uint32_t flag,
const void* data, size_t size);
void send_command(std::shared_ptr<Lobby> l, uint16_t command, uint32_t flag = 0,
const void* data = nullptr, size_t size = 0);
inline void send_command_excluding_client(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c, uint16_t command, uint32_t flag) {
send_command_excluding_client(l, c, command, flag, nullptr, 0);
}
void send_command(std::shared_ptr<Lobby> l, uint16_t command, uint32_t flag,
const void* data, size_t size);
inline void send_command(std::shared_ptr<Lobby> l, uint16_t command, uint32_t flag) {
send_command(l, command, flag, nullptr, 0);
}
void send_command(std::shared_ptr<ServerState> s, uint16_t command,
uint32_t flag = 0, const void* data = nullptr, size_t size = 0);
uint32_t flag, const void* data, size_t size);
template <typename TARGET, typename STRUCT>
void send_command(std::shared_ptr<TARGET> c, uint16_t command, uint32_t flag,
const STRUCT& data) {
inline void send_command(std::shared_ptr<ServerState> s, uint16_t command,
uint32_t flag) {
send_command(s, command, flag, nullptr, 0);
}
template <typename TargetT, typename StructT>
static void send_command_t(std::shared_ptr<TargetT> c, uint16_t command,
uint32_t flag, const StructT& data) {
send_command(c, command, flag, &data, sizeof(data));
}
template <typename TARGET>
void send_command(std::shared_ptr<TARGET> c, uint16_t command, uint32_t flag,
const std::string& data) {
template <typename TargetT>
static void send_command(std::shared_ptr<TargetT> c, uint16_t command,
uint32_t flag, const std::string& data) {
send_command(c, command, flag, data.data(), data.size());
}
template <typename TARGET, typename STRUCT>
void send_command(std::shared_ptr<TARGET> c, uint16_t command, uint32_t flag,
const std::vector<STRUCT>& data) {
send_command(c, command, flag, data.data(), data.size() * sizeof(STRUCT));
template <typename TargetT, typename StructT>
void send_command_vt(std::shared_ptr<TargetT> c, uint16_t command,
uint32_t flag, const std::vector<StructT>& data) {
send_command(c, command, flag, data.data(), data.size() * sizeof(StructT));
}
template <typename TARGET, typename STRUCT, typename ENTRY>
void send_command(std::shared_ptr<TARGET> c, uint16_t command, uint32_t flag,
const STRUCT& data, const std::vector<ENTRY>& array_data) {
std::string all_data(reinterpret_cast<const char*>(&data), sizeof(STRUCT));
template <typename TargetT, typename StructT, typename EntryT>
void send_command_t_vt(std::shared_ptr<TargetT> c, uint16_t command,
uint32_t flag, const StructT& data, const std::vector<EntryT>& array_data) {
std::string all_data(reinterpret_cast<const char*>(&data), sizeof(StructT));
all_data.append(reinterpret_cast<const char*>(array_data.data()),
array_data.size() * sizeof(ENTRY));
array_data.size() * sizeof(EntryT));
send_command(c, command, flag, all_data.data(), all_data.size());
}
void send_command_with_header(std::shared_ptr<Client> c, const void* data,
size_t size);
S_ServerInit_DC_PC_GC_02_17_92_9B prepare_server_init_contents_dc_pc_gc(
bool initial_connection,
uint32_t server_key,
uint32_t client_key);
S_ServerInit_BB_03 prepare_server_init_contents_bb(
const parray<uint8_t, 0x30>& server_key,
const parray<uint8_t, 0x30>& client_key,
bool use_secondary_message);
void send_server_init(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c,
bool initial_connection);
bool initial_connection, bool use_secondary_message);
void send_update_client_config(std::shared_ptr<Client> c);
void send_function_call(
std::shared_ptr<Client> c,
std::shared_ptr<CompiledFunctionCode> code,
const std::unordered_map<std::string, uint32_t>& label_writes = {},
const std::string& suffix = "",
uint32_t checksum_addr = 0,
uint32_t checksum_size = 0);
void send_reconnect(std::shared_ptr<Client> c, uint32_t address, uint16_t port);
void send_pc_gc_split_reconnect(std::shared_ptr<Client> c, uint32_t address,
uint16_t pc_port, uint16_t gc_port);
@@ -90,28 +127,32 @@ void send_player_preview_bb(std::shared_ptr<Client> c, uint8_t player_index,
void send_accept_client_checksum_bb(std::shared_ptr<Client> c);
void send_guild_card_header_bb(std::shared_ptr<Client> c);
void send_guild_card_chunk_bb(std::shared_ptr<Client> c, size_t chunk_index);
void send_stream_file_bb(std::shared_ptr<Client> c);
void send_stream_file_index_bb(std::shared_ptr<Client> c);
void send_stream_file_chunk_bb(std::shared_ptr<Client> c, uint32_t chunk_index);
void send_approve_player_choice_bb(std::shared_ptr<Client> c);
void send_complete_player_bb(std::shared_ptr<Client> c);
void send_check_directory_patch(std::shared_ptr<Client> c, const char* dir);
void send_enter_directory_patch(std::shared_ptr<Client> c, const std::string& dir);
void send_message_box(std::shared_ptr<Client> c, const char16_t* text);
void send_lobby_name(std::shared_ptr<Client> c, const char16_t* text);
void send_quest_info(std::shared_ptr<Client> c, const char16_t* text);
void send_lobby_message_box(std::shared_ptr<Client> c, const char16_t* text);
void send_ship_info(std::shared_ptr<Client> c, const char16_t* text);
void send_text_message(std::shared_ptr<Client> c, const char16_t* text);
void send_text_message(std::shared_ptr<Lobby> l, const char16_t* text);
void send_text_message(std::shared_ptr<ServerState> l, const char16_t* text);
void send_message_box(std::shared_ptr<Client> c, const std::u16string& text);
void send_lobby_name(std::shared_ptr<Client> c, const std::u16string& text);
void send_quest_info(std::shared_ptr<Client> c, const std::u16string& text,
bool is_download_quest);
void send_lobby_message_box(std::shared_ptr<Client> c, const std::u16string& text);
void send_ship_info(std::shared_ptr<Client> c, const std::u16string& text);
void send_text_message(Channel& ch, const std::u16string& text);
void send_text_message(std::shared_ptr<Client> c, const std::u16string& text);
void send_text_message(std::shared_ptr<Lobby> l, const std::u16string& text);
void send_text_message(std::shared_ptr<ServerState> l, const std::u16string& text);
void send_chat_message(Channel& ch, const std::u16string& text);
void send_chat_message(std::shared_ptr<Client> c, uint32_t from_serial_number,
const char16_t* from_name, const char16_t* text);
const std::u16string& from_name, const std::u16string& text);
void send_simple_mail(std::shared_ptr<Client> c, uint32_t from_serial_number,
const char16_t* from_name, const char16_t* text);
const std::u16string& from_name, const std::u16string& text);
template <typename TARGET>
template <typename TargetT>
__attribute__((format(printf, 2, 3))) void send_text_message_printf(
std::shared_ptr<TARGET> t, const char* format, ...) {
TargetT& t, const char* format, ...) {
va_list va;
va_start(va, format);
std::string buf = string_vprintf(format, va);
@@ -122,12 +163,15 @@ __attribute__((format(printf, 2, 3))) void send_text_message_printf(
void send_info_board(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
void send_card_search_result(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c,
std::shared_ptr<Client> result, std::shared_ptr<Lobby> result_lobby);
void send_card_search_result(
std::shared_ptr<ServerState> s,
std::shared_ptr<Client> c,
std::shared_ptr<Client> result,
std::shared_ptr<Lobby> result_lobby);
void send_guild_card(std::shared_ptr<Client> c, std::shared_ptr<Client> source);
void send_menu(std::shared_ptr<Client> c, const char16_t* menu_name,
uint32_t menu_id, const std::vector<MenuItem>& items, bool is_info_menu);
void send_menu(std::shared_ptr<Client> c, const std::u16string& menu_name,
uint32_t menu_id, const std::vector<MenuItem>& items, bool is_info_menu = false);
void send_game_menu(std::shared_ptr<Client> c, std::shared_ptr<ServerState> s);
void send_quest_menu(std::shared_ptr<Client> c, uint32_t menu_id,
const std::vector<std::shared_ptr<const Quest>>& quests, bool is_download_menu);
@@ -140,6 +184,7 @@ void send_player_join_notification(std::shared_ptr<Client> c,
std::shared_ptr<Lobby> l, std::shared_ptr<Client> joining_client);
void send_player_leave_notification(std::shared_ptr<Lobby> l,
uint8_t leaving_client_id);
void send_self_leave_notification(std::shared_ptr<Client> c);
void send_get_player_info(std::shared_ptr<Client> c);
void send_arrow_update(std::shared_ptr<Lobby> l);
@@ -147,15 +192,16 @@ void send_resume_game(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> ready_client);
enum PlayerStatsChange {
SubtractHP = 0,
SubtractTP = 1,
SubtractMeseta = 2,
AddHP = 3,
AddTP = 4,
SUBTRACT_HP = 0,
SUBTRACT_TP = 1,
SUBTRACT_MESETA = 2,
ADD_HP = 3,
ADD_TP = 4,
};
void send_player_stats_change(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
PlayerStatsChange which, uint32_t amount);
void send_warp(Channel& ch, uint8_t client_id, uint32_t area);
void send_warp(std::shared_ptr<Client> c, uint32_t area);
void send_ep3_change_music(std::shared_ptr<Client> c, uint32_t song);
@@ -164,9 +210,9 @@ void send_set_player_visibility(std::shared_ptr<Lobby> l,
void send_revive_player(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c);
void send_drop_item(std::shared_ptr<Lobby> l, const ItemData& item,
bool from_enemy, uint8_t area, float x, float y, uint16_t request_id);
bool from_enemy, uint8_t area, float x, float z, uint16_t request_id);
void send_drop_stacked_item(std::shared_ptr<Lobby> l, const ItemData& item,
uint8_t area, float x, float y);
uint8_t area, float x, float z);
void send_pick_up_item(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c, uint32_t id,
uint8_t area);
void send_create_inventory_item(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
@@ -178,13 +224,24 @@ void send_shop(std::shared_ptr<Client> c, uint8_t shop_type);
void send_level_up(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c);
void send_give_experience(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
uint32_t amount);
void send_ep3_card_list_update(std::shared_ptr<Client> c);
void send_ep3_card_list_update(
std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void send_ep3_rank_update(std::shared_ptr<Client> c);
void send_ep3_map_list(std::shared_ptr<Lobby> l);
void send_ep3_map_data(std::shared_ptr<Lobby> l, uint32_t map_id);
void send_ep3_map_list(
std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l);
void send_ep3_map_data(
std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l, uint32_t map_id);
void send_quest_file(std::shared_ptr<Client> c, const std::string& basename,
const std::string& contents, bool is_download_quest, bool is_ep3_quest);
enum class QuestFileType {
ONLINE = 0,
DOWNLOAD,
EPISODE_3,
GBA_DEMO,
};
void send_quest_file(std::shared_ptr<Client> c, const std::string& quest_name,
const std::string& basename, const std::string& contents,
QuestFileType type);
void send_server_time(std::shared_ptr<Client> c);
+111 -183
View File
@@ -24,41 +24,29 @@
#include "ReceiveCommands.hh"
using namespace std;
using namespace std::placeholders;
void Server::disconnect_client(struct bufferevent* bev) {
this->disconnect_client(this->bev_to_client.at(bev));
}
void Server::disconnect_client(shared_ptr<Client> c) {
this->bev_to_client.erase(c->bev);
struct bufferevent* bev = c->bev;
c->bev = nullptr;
int fd = bufferevent_getfd(bev);
if (fd < 0) {
log(INFO, "[Server] Client on virtual connection %p disconnected", bev);
if (c->channel.is_virtual_connection) {
this->log(INFO, "Disconnecting client on virtual connection %p",
c->channel.bev.get());
} else {
log(INFO, "[Server] Client on fd %d disconnected", fd);
this->log(INFO, "Disconnecting client on fd %d",
bufferevent_getfd(c->channel.bev.get()));
}
// if the output buffer is not empty, move the client into the draining pool
// instead of disconnecting it, to make sure all the data gets sent
struct evbuffer* out_buffer = bufferevent_get_output(bev);
if (evbuffer_get_length(out_buffer) == 0) {
bufferevent_flush(bev, EV_WRITE, BEV_FINISHED);
bufferevent_free(bev);
} else {
// the callbacks will free it when all the data is sent or the client
// disconnects
bufferevent_setcb(bev, nullptr,
Server::dispatch_on_disconnecting_client_output,
Server::dispatch_on_disconnecting_client_error, this);
bufferevent_disable(bev, EV_READ);
}
this->channel_to_client.erase(&c->channel);
c->channel.disconnect();
process_disconnect(this->state, c);
try {
process_disconnect(this->state, c);
} catch (const exception& e) {
this->log(WARNING, "Error during client disconnect cleanup: %s", e.what());
}
// c is destroyed here (process_disconnect should remove any other references
// to it, e.g. from Lobby objects)
}
void Server::dispatch_on_listen_accept(
@@ -73,25 +61,6 @@ void Server::dispatch_on_listen_error(struct evconnlistener* listener,
reinterpret_cast<Server*>(ctx)->on_listen_error(listener);
}
void Server::dispatch_on_client_input(struct bufferevent* bev, void* ctx) {
reinterpret_cast<Server*>(ctx)->on_client_input(bev);
}
void Server::dispatch_on_client_error(struct bufferevent* bev, short events,
void* ctx) {
reinterpret_cast<Server*>(ctx)->on_client_error(bev, events);
}
void Server::dispatch_on_disconnecting_client_output(struct bufferevent* bev,
void* ctx) {
reinterpret_cast<Server*>(ctx)->on_disconnecting_client_output(bev);
}
void Server::dispatch_on_disconnecting_client_error(struct bufferevent* bev,
short events, void* ctx) {
reinterpret_cast<Server*>(ctx)->on_disconnecting_client_error(bev, events);
}
void Server::on_listen_accept(struct evconnlistener* listener,
evutil_socket_t fd, struct sockaddr*, int) {
@@ -100,199 +69,158 @@ void Server::on_listen_accept(struct evconnlistener* listener,
try {
listening_socket = &this->listening_sockets.at(listen_fd);
} catch (const out_of_range& e) {
log(WARNING, "[Server] Can\'t determine version for socket %d; disconnecting client",
this->log(WARNING, "Can\'t determine version for socket %d; disconnecting client",
listen_fd);
close(fd);
return;
}
log(INFO, "[Server] Client fd %d connected via fd %d", fd, listen_fd);
this->log(INFO, "Client fd %d connected via fd %d (%s)",
fd, listen_fd, listening_socket->name.c_str());
struct bufferevent *bev = bufferevent_socket_new(this->base.get(), fd,
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
shared_ptr<Client> c(new Client(bev, listening_socket->version,
listening_socket->behavior));
this->bev_to_client.emplace(make_pair(bev, c));
shared_ptr<Client> c(new Client(
bev, listening_socket->version, listening_socket->behavior));
c->channel.on_command_received = Server::on_client_input;
c->channel.on_error = Server::on_client_error;
c->channel.context_obj = this;
this->channel_to_client.emplace(&c->channel, c);
bufferevent_setcb(bev, &Server::dispatch_on_client_input, nullptr,
&Server::dispatch_on_client_error, this);
bufferevent_enable(bev, EV_READ | EV_WRITE);
process_connect(this->state, c);
try {
process_connect(this->state, c);
} catch (const exception& e) {
this->log(WARNING, "Error during client initialization: %s", e.what());
this->disconnect_client(c);
}
}
void Server::connect_client(
struct bufferevent* bev, uint32_t address, uint16_t port,
GameVersion version, ServerBehavior initial_state) {
log(INFO, "[Server] Client connected on virtual connection %p", bev);
this->log(INFO, "Client connected on virtual connection %p", bev);
shared_ptr<Client> c(new Client(bev, version, initial_state));
this->bev_to_client.emplace(make_pair(bev, c));
c->channel.on_command_received = Server::on_client_input;
c->channel.on_error = Server::on_client_error;
c->channel.context_obj = this;
this->channel_to_client.emplace(&c->channel, c);
// Manually set the remote address, since the bufferevent has no fd and the
// Client constructor can't figure out the virtual remote address
auto* sin = reinterpret_cast<sockaddr_in*>(&c->remote_addr);
// Channel constructor can't figure out the virtual remote address
auto* sin = reinterpret_cast<sockaddr_in*>(&c->channel.remote_addr);
sin->sin_family = AF_INET;
sin->sin_addr.s_addr = htonl(address);
sin->sin_port = htons(port);
bufferevent_setcb(bev, &Server::dispatch_on_client_input, nullptr,
&Server::dispatch_on_client_error, this);
bufferevent_enable(bev, EV_READ | EV_WRITE);
process_connect(this->state, c);
try {
process_connect(this->state, c);
} catch (const exception& e) {
this->log(WARNING, "Error during client initialization: %s", e.what());
this->disconnect_client(c);
}
}
void Server::on_listen_error(struct evconnlistener* listener) {
int err = EVUTIL_SOCKET_ERROR();
log(ERROR, "[Server] Failure on listening socket %d: %d (%s)",
this->log(ERROR, "Failure on listening socket %d: %d (%s)",
evconnlistener_get_fd(listener), err, evutil_socket_error_to_string(err));
event_base_loopexit(this->base.get(), nullptr);
}
void Server::on_client_input(struct bufferevent* bev) {
shared_ptr<Client> c;
try {
c = this->bev_to_client.at(bev);
} catch (const out_of_range& e) {
log(WARNING, "[Server] Received message from client with no configuration");
// ignore all the data
// TODO: we probably should disconnect them or something
struct evbuffer* in_buffer = bufferevent_get_input(bev);
evbuffer_drain(in_buffer, evbuffer_get_length(in_buffer));
return;
}
void Server::on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data) {
Server* server = reinterpret_cast<Server*>(ch.context_obj);
shared_ptr<Client> c = server->channel_to_client.at(&ch);
if (c->should_disconnect) {
this->disconnect_client(bev);
return;
}
c->last_recv_time = now();
this->receive_and_process_commands(c);
if (c->should_disconnect) {
this->disconnect_client(bev);
return;
}
}
void Server::on_disconnecting_client_output(struct bufferevent* bev) {
bufferevent_flush(bev, EV_WRITE, BEV_FINISHED);
bufferevent_free(bev);
}
void Server::on_client_error(struct bufferevent* bev, short events) {
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
log(WARNING, "[Server] Client caused error %d (%s)", err,
evutil_socket_error_to_string(err));
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
this->disconnect_client(bev);
}
}
void Server::on_disconnecting_client_error(struct bufferevent* bev,
short events) {
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
log(WARNING, "[Server] Disconnecting client caused error %d (%s)", err,
evutil_socket_error_to_string(err));
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
bufferevent_flush(bev, EV_WRITE, BEV_FINISHED);
bufferevent_free(bev);
}
}
void Server::receive_and_process_commands(shared_ptr<Client> c) {
struct evbuffer* buf = bufferevent_get_input(c->bev);
size_t header_size = (c->version == GameVersion::BB) ? 8 : 4;
// read as much data into recv_buffer as we can and decrypt it
size_t existing_bytes = c->recv_buffer.size();
size_t new_bytes = evbuffer_get_length(buf);
new_bytes &= ~(header_size - 1); // only read in multiples of header_size
c->recv_buffer.resize(existing_bytes + new_bytes);
void* recv_ptr = c->recv_buffer.data() + existing_bytes;
if (evbuffer_remove(buf, recv_ptr, new_bytes) != static_cast<ssize_t>(new_bytes)) {
throw runtime_error("some bytes could not be read from the receive buffer");
}
// decrypt the received data if encryption is enabled
if (c->crypt_in.get()) {
c->crypt_in->decrypt(recv_ptr, new_bytes);
}
// process as many commands as possible
size_t offset = 0;
while (offset < c->recv_buffer.size()) {
const PSOCommandHeader* header = reinterpret_cast<const PSOCommandHeader*>(
c->recv_buffer.data() + offset);
size_t size = header->size(c->version);
if (offset + size > c->recv_buffer.size()) {
break; // don't have a complete command; we're done for now
}
// if we get here, then we have a complete, decrypted command waiting to be
// processed. we copy it out and append zeroes on the end so that it's safe
// to call string functions on the buffer in command handlers
string data = c->recv_buffer.substr(offset + header_size, size - header_size);
data.append(4, '\0');
server->disconnect_client(c);
} else {
try {
process_command(this->state, c, header->command(c->version),
header->flag(c->version), size - header_size, data.data());
process_command(server->state, c, command, flag, data);
} catch (const exception& e) {
log(INFO, "[Server] Error in client stream: %s", e.what());
server->log(WARNING, "Error processing client command: %s", e.what());
c->should_disconnect = true;
return;
}
// BB pads commands to 8-byte boundaries, so if we see a shorter command,
// skip over the padding
offset += (size + header_size - 1) & ~(header_size - 1);
if (c->should_disconnect) {
server->disconnect_client(c);
}
}
// remove the processed commands from the receive buffer
c->recv_buffer = c->recv_buffer.substr(offset);
}
Server::Server(shared_ptr<struct event_base> base,
shared_ptr<ServerState> state) : base(base), state(state) { }
void Server::on_client_error(Channel& ch, short events) {
Server* server = reinterpret_cast<Server*>(ch.context_obj);
shared_ptr<Client> c = server->channel_to_client.at(&ch);
void Server::listen(const string& socket_path, GameVersion version,
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
server->log(WARNING, "Client caused error %d (%s)", err,
evutil_socket_error_to_string(err));
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
server->disconnect_client(c);
}
}
Server::Server(
shared_ptr<struct event_base> base,
shared_ptr<ServerState> state)
: log("[Server] "),
base(base),
state(state) { }
void Server::listen(
const std::string& name,
const string& socket_path,
GameVersion version,
ServerBehavior behavior) {
int fd = ::listen(socket_path, 0, SOMAXCONN);
log(INFO, "[Server] Listening on Unix socket %s (%s) on fd %d",
socket_path.c_str(), name_for_version(version), fd);
this->add_socket(fd, version, behavior);
this->log(INFO, "Listening on Unix socket %s (%s) on fd %d (name: %s)",
socket_path.c_str(), name_for_version(version), fd, name.c_str());
this->add_socket(name, fd, version, behavior);
}
void Server::listen(const string& addr, int port, GameVersion version,
void Server::listen(
const std::string& name,
const string& addr,
int port,
GameVersion version,
ServerBehavior behavior) {
int fd = ::listen(addr, port, SOMAXCONN);
string netloc_str = render_netloc(addr, port);
log(INFO, "[Server] Listening on TCP interface %s (%s) on fd %d",
netloc_str.c_str(), name_for_version(version), fd);
this->add_socket(fd, version, behavior);
this->log(INFO, "Listening on TCP interface %s (%s) on fd %d (name: %s)",
netloc_str.c_str(), name_for_version(version), fd, name.c_str());
this->add_socket(name, fd, version, behavior);
}
void Server::listen(int port, GameVersion version, ServerBehavior behavior) {
this->listen("", port, version, behavior);
void Server::listen(const std::string& name, int port, GameVersion version, ServerBehavior behavior) {
this->listen(name, "", port, version, behavior);
}
Server::ListeningSocket::ListeningSocket(Server* s, int fd,
GameVersion version, ServerBehavior behavior) :
fd(fd), version(version), behavior(behavior), listener(
Server::ListeningSocket::ListeningSocket(Server* s, const std::string& name,
int fd, GameVersion version, ServerBehavior behavior) :
name(name), fd(fd), version(version), behavior(behavior), listener(
evconnlistener_new(s->base.get(), Server::dispatch_on_listen_accept, s,
LEV_OPT_REUSEABLE, 0, this->fd), evconnlistener_free) {
evconnlistener_set_error_cb(this->listener.get(),
Server::dispatch_on_listen_error);
}
void Server::add_socket(int fd, GameVersion version, ServerBehavior behavior) {
void Server::add_socket(
const std::string& name,
int fd,
GameVersion version,
ServerBehavior behavior) {
this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd),
forward_as_tuple(this, fd, version, behavior));
forward_as_tuple(this, name, fd, version, behavior));
}
shared_ptr<Client> Server::get_client() const {
if (this->channel_to_client.empty()) {
throw runtime_error("no clients on game server");
}
if (this->channel_to_client.size() > 1) {
throw runtime_error("multiple clients on game server");
}
return this->channel_to_client.begin()->second;
}
+16 -19
View File
@@ -21,52 +21,49 @@ public:
std::shared_ptr<ServerState> state);
virtual ~Server() = default;
void listen(const std::string& socket_path, GameVersion version, ServerBehavior initial_state);
void listen(const std::string& addr, int port, GameVersion version, ServerBehavior initial_state);
void listen(int port, GameVersion version, ServerBehavior initial_state);
void add_socket(int fd, GameVersion version, ServerBehavior initial_state);
void listen(const std::string& name, const std::string& socket_path, GameVersion version, ServerBehavior initial_state);
void listen(const std::string& name, const std::string& addr, int port, GameVersion version, ServerBehavior initial_state);
void listen(const std::string& name, int port, GameVersion version, ServerBehavior initial_state);
void add_socket(const std::string& name, int fd, GameVersion version, ServerBehavior initial_state);
void connect_client(struct bufferevent* bev, uint32_t address, uint16_t port,
GameVersion version, ServerBehavior initial_state);
std::shared_ptr<Client> get_client() const;
private:
PrefixedLogger log;
std::shared_ptr<struct event_base> base;
struct ListeningSocket {
std::string name;
int fd;
GameVersion version;
ServerBehavior behavior;
std::unique_ptr<struct evconnlistener, void(*)(struct evconnlistener*)> listener;
ListeningSocket(Server* s, int fd, GameVersion version,
ListeningSocket(
Server* s,
const std::string& name,
int fd,
GameVersion version,
ServerBehavior behavior);
};
std::unordered_map<int, ListeningSocket> listening_sockets;
std::unordered_map<struct bufferevent*, std::shared_ptr<Client>> bev_to_client;
std::unordered_map<Channel*, std::shared_ptr<Client>> channel_to_client;
std::shared_ptr<ServerState> state;
static void dispatch_on_listen_accept(struct evconnlistener* listener,
evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx);
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
static void dispatch_on_client_error(struct bufferevent* bev, short events,
void* ctx);
static void dispatch_on_disconnecting_client_output(struct bufferevent* bev,
void* ctx);
static void dispatch_on_disconnecting_client_error(struct bufferevent* bev,
short events, void* ctx);
void disconnect_client(struct bufferevent* bev);
void disconnect_client(std::shared_ptr<Client> c);
void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd,
struct sockaddr *address, int socklen);
void on_listen_error(struct evconnlistener* listener);
void on_client_input(struct bufferevent* bev);
void on_client_error(struct bufferevent* bev, short events);
void on_disconnecting_client_output(struct bufferevent* bev);
void on_disconnecting_client_error(struct bufferevent* bev, short events);
void receive_and_process_commands(std::shared_ptr<Client> c);
static void on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data);
static void on_client_error(Channel& ch, short events);
};
+235 -14
View File
@@ -6,22 +6,41 @@
#include <phosg/Strings.hh>
#include "ChatCommands.hh"
#include "ServerState.hh"
#include "SendCommands.hh"
#include "StaticGameData.hh"
using namespace std;
ServerShell::ServerShell(std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state) : Shell(base, state) { }
ServerShell::ServerShell(
shared_ptr<struct event_base> base,
shared_ptr<ServerState> state)
: Shell(base, state) { }
void ServerShell::print_prompt() {
fwrite("newserv> ", 9, 1, stdout);
fflush(stdout);
}
shared_ptr<ProxyServer::LinkedSession> ServerShell::get_proxy_session() {
if (!this->state->proxy_server.get()) {
throw runtime_error("the proxy server is disabled");
}
return this->state->proxy_server->get_session();
}
static void set_boolean(bool* target, const string& args) {
if (args == "on") {
*target = true;
} else if (args == "off") {
*target = false;
} else {
throw invalid_argument("argument must be \"on\" or \"off\"");
}
}
void ServerShell::execute_command(const string& command) {
// find the entry in the command table and run the command
size_t command_end = skip_non_whitespace(command, 0);
@@ -34,16 +53,20 @@ void ServerShell::execute_command(const string& command) {
} else if (command_name == "help") {
fprintf(stderr, "\
Commands:\n\
General commands:\n\
help\n\
You\'re reading it now.\n\
\n\
Server commands:\n\
exit (or ctrl+d)\n\
Shut down the server.\n\
reload <item> ...\n\
Reload data. <item> can be licenses, battle-params, level-table, or quests.\n\
Reloading will not affect items that are in use; for example, if a client\'s\n\
license is deleted by reloading, they will not be disconnected immediately.\n\
add-license <parameters>\n\
Add a license to the server. <parameters> is some subset of the following:\n\
username=<username> (BB username)\n\
bb-username=<username> (BB username)\n\
bb-password=<password> (BB password)\n\
gc-password=<password> (GC password)\n\
access-key=<access-key> (GC/PC access key)\n\
@@ -66,8 +89,70 @@ Commands:\n\
Song IDs are 0 through 51; the default song is -1.\n\
announce <message>\n\
Send an announcement message to all players.\n\
\n\
Proxy commands (these will only work when exactly one client is connected):\n\
sc <data>\n\
Send a command to the client.\n\
ss <data>\n\
Send a command to the server.\n\
chat <text>\n\
Send a chat message to the server.\n\
dchat <data>\n\
Send a chat message to the server with arbitrary data in it.\n\
info-board <text>\n\
Set your info board contents. This will affect the current session only,\n\
and will not be saved for future sessions.\n\
info-board-data <data>\n\
Set your info board contents with arbitrary data. Like the above, affects\n\
the current session only.\n\
marker <color-id>\n\
Change your lobby marker color.\n\
warp <area-id>\n\
Send yourself to a specific area.\n\
set-override-section-id [section-id]\n\
Override the section ID for games you create or join. This affects the\n\
active drop chart if you are the leader of the game and the server doesn't\n\
override drops entirely. If no argument is given, clears the override.\n\
set-override-event [event]\n\
Override the lobby event for all lobbies and games you join. This applies\n\
only to you; other players do not see this override. If no argument is\n\
given, clears the override.\n\
set-override-lobby-number [number]\n\
Override the lobby type for all lobbies you join. This applies only to you;\n\
other players do not see this override. If no argument is given, clears the\n\
override.\n\
set-chat-filter <on|off>\n\
Enable or disable chat filtering (enabled by default). Chat filtering\n\
applies newserv\'s standard character replacements to chat messages; for\n\
example, $ becomes a tab character and # becomes a newline.\n\
set-infinite-hp <on|off>\n\
set-infinite-tp <on|off>\n\
Enable or disable infinite HP or TP. When infinite HP is enabled, attacks\n\
that would kill you in one hit will still do so.\n\
set-switch-assist <on|off>\n\
Enable or disable switch assist. When switch assist is on, the proxy will\n\
remember the last \"enable switch\" command that you send, and will send it\n\
to you and the server when you step on another switch. Using this, you can\n\
unlock any doors that require two players to stand on switches by touching\n\
both switches yourself. With this, all online maps can be completed solo.\n\
set-save-files <on|off>\n\
Enable or disable saving of game files (disabled by default). When this is\n\
on, any file that the remote server sends to the client will be saved to\n\
the current directory. This includes data like quests, Episode 3 card\n\
definitions, and GBA games.\n\
set-block-function-calls [return-value]\n\
Enable blocking of function calls from the server. When enabled, the proxy\n\
responds as if the function was called (with the given return value), but\n\
does not send the code to the client. To stop blocking function calls, omit\n\
the return value.\n\
close-idle-sessions\n\
Closes all sessions that don\'t have a client and server connected.\n\
");
// SERVER COMMANDS
} else if (command_name == "reload") {
auto types = split(command_args, ' ');
if (types.empty()) {
@@ -95,29 +180,29 @@ Commands:\n\
shared_ptr<License> l(new License());
for (const string& token : split(command_args, ' ')) {
if (starts_with(token, "username=")) {
if (token.size() >= 29) {
if (starts_with(token, "bb-username=")) {
if (token.size() >= 32) {
throw invalid_argument("username too long");
}
strcpy(l->username, token.c_str() + 9);
l->username = token.substr(12);
} else if (starts_with(token, "bb-password=")) {
if (token.size() >= 32) {
throw invalid_argument("bb-password too long");
}
strcpy(l->bb_password, token.c_str() + 12);
l->bb_password = token.substr(12);
} else if (starts_with(token, "gc-password=")) {
if (token.size() > 20) {
throw invalid_argument("gc-password too long");
}
strcpy(l->gc_password, token.c_str() + 12);
l->gc_password = token.substr(12);
} else if (starts_with(token, "access-key=")) {
if (token.size() > 23) {
throw invalid_argument("access-key is too long");
}
strcpy(l->access_key, token.c_str() + 11);
l->access_key = token.substr(11);
} else if (starts_with(token, "serial=")) {
l->serial_number = stoul(token.substr(7));
@@ -127,11 +212,11 @@ Commands:\n\
if (mask == "normal") {
l->privileges = 0;
} else if (mask == "mod") {
l->privileges = Privilege::Moderator;
l->privileges = Privilege::MODERATOR;
} else if (mask == "admin") {
l->privileges = Privilege::Administrator;
l->privileges = Privilege::ADMINISTRATOR;
} else if (mask == "root") {
l->privileges = Privilege::Root;
l->privileges = Privilege::ROOT;
} else {
l->privileges = stoul(mask);
}
@@ -189,6 +274,142 @@ Commands:\n\
u16string message16 = decode_sjis(command_args);
send_text_message(this->state, message16.c_str());
// PROXY COMMANDS
} else if ((command_name == "sc") || (command_name == "ss")) {
string data = parse_data_string(command_args);
if (data.size() & 3) {
throw invalid_argument("data size is not a multiple of 4");
}
if (data.size() == 0) {
throw invalid_argument("no data given");
}
shared_ptr<ProxyServer::LinkedSession> proxy_session;
try {
proxy_session = this->get_proxy_session();
} catch (const exception&) { }
if (proxy_session.get()) {
if (command_name[1] == 's') {
proxy_session->server_channel.send(data);
} else {
proxy_session->client_channel.send(data);
}
} else {
if (command_name [1] == 's') {
throw runtime_error("cannot send to server in non-proxy session");
}
auto c = this->state->game_server->get_client();
send_command_with_header(c, data.data(), data.size());
}
} else if ((command_name == "chat") || (command_name == "dchat")) {
auto session = this->get_proxy_session();
string data(8, '\0');
data.push_back('\x09');
data.push_back('E');
if (command_name == "dchat") {
data += parse_data_string(command_args);
} else {
data += command_args;
}
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
session->server_channel.send(0x06, 0x00, data);
} else if (command_name == "marker") {
auto session = this->get_proxy_session();
session->server_channel.send(0x89, stoul(command_args));
} else if (command_name == "warp") {
auto session = this->get_proxy_session();
PSOSubcommand cmds[2];
cmds[0].word[0] = 0x0294;
cmds[0].word[1] = session->lobby_client_id;
cmds[1].dword = stoul(command_args);
session->client_channel.send(0x60, 0x00, &cmds, sizeof(cmds));
session->server_channel.send(0x60, 0x00, &cmds, sizeof(cmds));
} else if ((command_name == "info-board") || (command_name == "info-board-data")) {
auto session = this->get_proxy_session();
string data;
if (command_name == "info-board-data") {
data += parse_data_string(command_args);
} else {
data += command_args;
}
data.push_back('\0');
data.resize((data.size() + 3) & (~3));
session->server_channel.send(0xD9, 0x00, data);
} else if (command_name == "set-override-section-id") {
auto session = this->get_proxy_session();
if (command_args.empty()) {
session->override_section_id = -1;
} else {
session->override_section_id = section_id_for_name(command_args);
}
} else if (command_name == "set-override-event") {
auto session = this->get_proxy_session();
if (command_args.empty()) {
session->override_lobby_event = -1;
} else {
session->override_lobby_event = event_for_name(command_args);
session->client_channel.send(0xDA, session->override_lobby_event);
}
} else if (command_name == "set-override-lobby-number") {
auto session = this->get_proxy_session();
if (command_args.empty()) {
session->override_lobby_number = -1;
} else {
session->override_lobby_number = lobby_type_for_name(command_args);
}
} else if (command_name == "set-chat-filter") {
auto session = this->get_proxy_session();
set_boolean(&session->enable_chat_filter, command_args);
} else if (command_name == "set-infinite-hp") {
auto session = this->get_proxy_session();
set_boolean(&session->infinite_hp, command_args);
} else if (command_name == "set-infinite-tp") {
auto session = this->get_proxy_session();
set_boolean(&session->infinite_tp, command_args);
} else if (command_name == "set-switch-assist") {
auto session = this->get_proxy_session();
set_boolean(&session->switch_assist, command_args);
} else if (command_name == "set-save-files") {
auto session = this->get_proxy_session();
set_boolean(&session->save_files, command_args);
} else if (command_name == "set-block-function-calls") {
auto session = this->get_proxy_session();
if (command_args.empty()) {
session->function_call_return_value = -1;
} else {
session->function_call_return_value = stoul(command_args);
}
} else if (command_name == "close-idle-sessions") {
size_t count = this->state->proxy_server->delete_disconnected_sessions();
fprintf(stderr, "%zu sessions closed\n", count);
} else {
throw invalid_argument("unknown command; try \'help\'");
}
+5 -1
View File
@@ -6,12 +6,14 @@
#include <event2/event.h>
#include "Shell.hh"
#include "ProxyServer.hh"
class ServerShell : public Shell {
public:
ServerShell(std::shared_ptr<struct event_base> base,
ServerShell(
std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state);
virtual ~ServerShell() = default;
ServerShell(const ServerShell&) = delete;
@@ -20,6 +22,8 @@ public:
ServerShell& operator=(ServerShell&&) = delete;
protected:
std::shared_ptr<ProxyServer::LinkedSession> get_proxy_session();
virtual void print_prompt();
virtual void execute_command(const std::string& command);
};
+198 -40
View File
@@ -3,10 +3,11 @@
#include <string.h>
#include <memory>
#include <phosg/Network.hh>
#include "SendCommands.hh"
#include "NetworkAddresses.hh"
#include "IPStackSimulator.hh"
#include "NetworkAddresses.hh"
#include "SendCommands.hh"
#include "Text.hh"
using namespace std;
@@ -17,57 +18,66 @@ ServerState::ServerState()
: dns_server_port(0),
ip_stack_debug(false),
allow_unregistered_users(false),
run_shell_behavior(RunShellBehavior::Default), next_lobby_id(1),
item_tracking_enabled(true),
run_shell_behavior(RunShellBehavior::DEFAULT), next_lobby_id(1),
pre_lobby_event(0),
ep3_menu_song(-1) {
memset(&this->default_key_file, 0, sizeof(this->default_key_file));
this->main_menu.emplace_back(MAIN_MENU_GO_TO_LOBBY, u"Go to lobby",
u"Join the lobby.", 0);
this->main_menu.emplace_back(MAIN_MENU_INFORMATION, u"Information",
u"View server information.", MenuItemFlag::RequiresMessageBoxes);
this->main_menu.emplace_back(MAIN_MENU_DOWNLOAD_QUESTS, u"Download quests",
u"Download quests.", 0);
this->main_menu.emplace_back(MAIN_MENU_DISCONNECT, u"Disconnect",
u"Disconnect.", 0);
vector<shared_ptr<Lobby>> ep3_only_lobbies;
for (size_t x = 0; x < 20; x++) {
auto lobby_name = decode_sjis(string_printf("LOBBY%zu", x + 1));
bool is_ep3_only = (x > 14);
shared_ptr<Lobby> l(new Lobby());
l->flags |= LobbyFlag::Public | LobbyFlag::Default | LobbyFlag::Persistent |
((x > 14) ? LobbyFlag::Episode3 : 0);
l->flags |= Lobby::Flag::PUBLIC | Lobby::Flag::DEFAULT | Lobby::Flag::PERSISTENT |
(is_ep3_only ? Lobby::Flag::EPISODE_3_ONLY : 0);
l->block = x + 1;
l->type = x;
char16cpy(l->name, lobby_name.c_str(), 0x24);
l->name = lobby_name;
l->max_clients = 12;
this->add_lobby(l);
if (!is_ep3_only) {
this->public_lobby_search_order.emplace_back(l);
} else {
ep3_only_lobbies.emplace_back(l);
}
}
this->public_lobby_search_order_ep3 = this->public_lobby_search_order;
this->public_lobby_search_order_ep3.insert(
this->public_lobby_search_order_ep3.begin(),
ep3_only_lobbies.begin(),
ep3_only_lobbies.end());
}
void ServerState::add_client_to_available_lobby(shared_ptr<Client> c) {
auto it = this->id_to_lobby.lower_bound(0);
for (; it != this->id_to_lobby.end(); it++) {
if (!(it->second->flags & LobbyFlag::Public)) {
continue;
}
const auto& search_order = (c->flags & Client::Flag::EPISODE_3)
? this->public_lobby_search_order_ep3
: this->public_lobby_search_order;
shared_ptr<Lobby> added_to_lobby;
for (const auto& l : search_order) {
try {
it->second->add_client(c);
l->add_client(c);
added_to_lobby = l;
break;
} catch (const out_of_range&) { }
}
if (it == this->id_to_lobby.end()) {
if (!added_to_lobby) {
// TODO: Add the user to a dynamically-created private lobby instead
throw out_of_range("all lobbies full");
}
// send a join message to the joining player, and notifications to all others
this->send_lobby_join_notifications(it->second, c);
// Send a join message to the joining player, and notifications to all others
this->send_lobby_join_notifications(added_to_lobby, c);
}
void ServerState::remove_client_from_lobby(shared_ptr<Client> c) {
auto l = this->id_to_lobby.at(c->lobby_id);
l->remove_client(c);
if (!(l->flags & LobbyFlag::Persistent) && (l->count_clients() == 0)) {
if (!(l->flags & Lobby::Flag::PERSISTENT) && (l->count_clients() == 0)) {
this->remove_lobby(l->lobby_id);
} else {
send_player_leave_notification(l, c->lobby_client_id);
@@ -90,7 +100,7 @@ void ServerState::change_client_lobby(shared_ptr<Client> c, shared_ptr<Lobby> ne
}
if (current_lobby) {
if (!(current_lobby->flags & LobbyFlag::Persistent) && (current_lobby->count_clients() == 0)) {
if (!(current_lobby->flags & Lobby::Flag::PERSISTENT) && (current_lobby->count_clients() == 0)) {
this->remove_lobby(current_lobby->lobby_id);
} else {
send_player_leave_notification(current_lobby, old_lobby_client_id);
@@ -136,13 +146,12 @@ void ServerState::remove_lobby(uint32_t lobby_id) {
this->id_to_lobby.erase(lobby_id);
}
shared_ptr<Client> ServerState::find_client(const char16_t* identifier,
shared_ptr<Client> ServerState::find_client(const std::u16string* identifier,
uint64_t serial_number, shared_ptr<Lobby> l) {
if ((serial_number == 0) && identifier) {
try {
string encoded = encode_sjis(identifier);
serial_number = stoull(encoded, nullptr, 0);
serial_number = stoull(encode_sjis(*identifier), nullptr, 0);
} catch (const exception&) { }
}
@@ -167,17 +176,17 @@ shared_ptr<Client> ServerState::find_client(const char16_t* identifier,
}
uint32_t ServerState::connect_address_for_client(std::shared_ptr<Client> c) {
if (c->is_virtual_connection) {
if (c->remote_addr.ss_family != AF_INET) {
if (c->channel.is_virtual_connection) {
if (c->channel.remote_addr.ss_family != AF_INET) {
throw logic_error("virtual connection is missing remote IPv4 address");
}
const auto* sin = reinterpret_cast<const sockaddr_in*>(&c->remote_addr);
const auto* sin = reinterpret_cast<const sockaddr_in*>(&c->channel.remote_addr);
return IPStackSimulator::connect_address_for_remote_address(
ntohl(sin->sin_addr.s_addr));
} else {
// TODO: we can do something smarter here, like use the sockname to find
// out which interface the client is connected to, and return that address
if (is_local_address(c->remote_addr)) {
if (is_local_address(c->channel.remote_addr)) {
return this->local_address;
} else {
return this->external_address;
@@ -187,13 +196,162 @@ uint32_t ServerState::connect_address_for_client(std::shared_ptr<Client> c) {
shared_ptr<const vector<MenuItem>> ServerState::information_menu_for_version(GameVersion version) {
if (version == GameVersion::PC) {
return this->information_menu_pc;
} else if (version == GameVersion::GC) {
return this->information_menu_gc;
}
throw out_of_range("no information menu exists for this version");
}
const vector<MenuItem>& ServerState::proxy_destinations_menu_for_version(GameVersion version) {
if (version == GameVersion::PC) {
return this->proxy_destinations_menu_pc;
} else if (version == GameVersion::GC) {
return this->proxy_destinations_menu_gc;
}
throw out_of_range("no proxy destinations menu exists for this version");
}
const vector<pair<string, uint16_t>>& ServerState::proxy_destinations_for_version(GameVersion version) {
if (version == GameVersion::PC) {
return this->proxy_destinations_pc;
} else if (version == GameVersion::GC) {
return this->proxy_destinations_gc;
}
throw out_of_range("no proxy destinations menu exists for this version");
}
void ServerState::set_port_configuration(
const std::unordered_map<std::string, PortConfiguration>& named_port_configuration) {
this->named_port_configuration = named_port_configuration;
this->numbered_port_configuration.clear();
for (const auto& it : this->named_port_configuration) {
if (!this->numbered_port_configuration.emplace(it.second.port, it.second).second) {
throw runtime_error("duplicate port in configuration");
const vector<PortConfiguration>& port_configs) {
this->name_to_port_config.clear();
this->number_to_port_config.clear();
for (const auto& pc : port_configs) {
shared_ptr<PortConfiguration> spc(new PortConfiguration(pc));
if (!this->name_to_port_config.emplace(spc->name, spc).second) {
throw logic_error("duplicate name in port configuration");
}
if (!this->number_to_port_config.emplace(spc->port, spc).second) {
throw logic_error("duplicate number in port configuration");
}
}
}
void ServerState::create_menus(shared_ptr<const JSONObject> config_json) {
const auto& d = config_json->as_dict();
shared_ptr<vector<MenuItem>> information_menu_pc(new vector<MenuItem>());
shared_ptr<vector<MenuItem>> information_menu_gc(new vector<MenuItem>());
shared_ptr<vector<u16string>> information_contents(new vector<u16string>());
information_menu_gc->emplace_back(InformationMenuItemID::GO_BACK, u"Go back",
u"Return to the\nmain menu", 0);
{
uint32_t item_id = 0;
for (const auto& item : d.at("InformationMenuContents")->as_list()) {
auto& v = item->as_list();
information_menu_pc->emplace_back(item_id, decode_sjis(v.at(0)->as_string()),
decode_sjis(v.at(1)->as_string()), 0);
information_menu_gc->emplace_back(item_id, decode_sjis(v.at(0)->as_string()),
decode_sjis(v.at(1)->as_string()), MenuItem::Flag::REQUIRES_MESSAGE_BOXES);
information_contents->emplace_back(decode_sjis(v.at(2)->as_string()));
item_id++;
}
}
this->information_menu_pc = information_menu_pc;
this->information_menu_gc = information_menu_gc;
this->information_contents = information_contents;
auto generate_proxy_destinations_menu = +[](
vector<MenuItem>& ret_menu,
vector<pair<string, uint16_t>>& ret_pds,
const unordered_map<string, shared_ptr<JSONObject>>& d) {
ret_menu.clear();
ret_pds.clear();
ret_menu.emplace_back(ProxyDestinationsMenuItemID::GO_BACK, u"Go back",
u"Return to the\nmain menu", 0);
uint32_t item_id = 0;
for (const auto& item : d) {
const string& netloc_str = item.second->as_string();
const string& description = "$C7Remote server:\n$C6" + netloc_str;
ret_menu.emplace_back(item_id, decode_sjis(item.first),
decode_sjis(description), 0);
ret_pds.emplace_back(parse_netloc(netloc_str));
item_id++;
}
};
generate_proxy_destinations_menu(
this->proxy_destinations_menu_pc,
this->proxy_destinations_pc,
d.at("ProxyDestinations-PC")->as_dict());
generate_proxy_destinations_menu(
this->proxy_destinations_menu_gc,
this->proxy_destinations_gc,
d.at("ProxyDestinations-GC")->as_dict());
try {
const string& netloc_str = d.at("ProxyDestination-Patch")->as_string();
this->proxy_destination_patch = parse_netloc(netloc_str);
log(INFO, "Patch server proxy is enabled with destination %s", netloc_str.c_str());
for (auto& it : this->name_to_port_config) {
if (it.second->version == GameVersion::PATCH) {
it.second->behavior = ServerBehavior::PROXY_SERVER;
}
}
} catch (const out_of_range&) {
this->proxy_destination_patch.first = "";
this->proxy_destination_patch.second = 0;
}
try {
const string& netloc_str = d.at("ProxyDestination-BB")->as_string();
this->proxy_destination_bb = parse_netloc(netloc_str);
log(INFO, "BB proxy is enabled with destination %s", netloc_str.c_str());
for (auto& it : this->name_to_port_config) {
if (it.second->version == GameVersion::BB) {
it.second->behavior = ServerBehavior::PROXY_SERVER;
}
}
} catch (const out_of_range&) {
this->proxy_destination_bb.first = "";
this->proxy_destination_bb.second = 0;
}
this->main_menu.emplace_back(MainMenuItemID::GO_TO_LOBBY, u"Go to lobby",
u"Join the lobby", 0);
this->main_menu.emplace_back(MainMenuItemID::INFORMATION, u"Information",
u"View server\ninformation", MenuItem::Flag::REQUIRES_MESSAGE_BOXES);
if (!this->proxy_destinations_pc.empty()) {
this->main_menu.emplace_back(MainMenuItemID::PROXY_DESTINATIONS, u"Proxy server",
u"Connect to another\nserver", MenuItem::Flag::PC_ONLY);
}
if (!this->proxy_destinations_gc.empty()) {
this->main_menu.emplace_back(MainMenuItemID::PROXY_DESTINATIONS, u"Proxy server",
u"Connect to another\nserver", MenuItem::Flag::GC_ONLY);
}
this->main_menu.emplace_back(MainMenuItemID::DOWNLOAD_QUESTS, u"Download quests",
u"Download quests", MenuItem::Flag::INVISIBLE_ON_BB);
if (!this->dol_file_index->empty()) {
this->main_menu.emplace_back(MainMenuItemID::PATCHES, u"Patches",
u"Change game\nbehaviors", MenuItem::Flag::GC_ONLY | MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL);
}
if (!this->dol_file_index->empty()) {
this->main_menu.emplace_back(MainMenuItemID::PROGRAMS, u"Programs",
u"Run GameCube\nprograms", MenuItem::Flag::GC_ONLY | MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL | MenuItem::Flag::REQUIRES_SAVE_DISABLED);
}
this->main_menu.emplace_back(MainMenuItemID::DISCONNECT, u"Disconnect",
u"Disconnect", 0);
this->main_menu.emplace_back(MainMenuItemID::CLEAR_LICENSE, u"Clear license",
u"Disconnect with an\ninvalid license error\nso you can enter a\ndifferent serial\nnumber, access key,\nor password", 0);
try {
this->welcome_message = decode_sjis(d.at("WelcomeMessage")->as_string());
} catch (const out_of_range&) { }
}
+44 -13
View File
@@ -2,13 +2,15 @@
#include <atomic>
#include <map>
#include <unordered_map>
#include <string>
#include <memory>
#include <vector>
#include <phosg/JSON.hh>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
#include "Client.hh"
#include "FunctionCompiler.hh"
#include "Items.hh"
#include "LevelTable.hh"
#include "License.hh"
@@ -18,7 +20,12 @@
// Forwawrd declarations due to reference cycles
class ProxyServer;
class Server;
struct PortConfiguration {
std::string name;
uint16_t port;
GameVersion version;
ServerBehavior behavior;
@@ -26,21 +33,25 @@ struct PortConfiguration {
struct ServerState {
enum class RunShellBehavior {
Default = 0,
Always,
Never,
DEFAULT = 0,
ALWAYS,
NEVER,
};
std::u16string name;
std::unordered_map<std::string, PortConfiguration> named_port_configuration;
std::unordered_map<uint16_t, PortConfiguration> numbered_port_configuration;
std::unordered_map<std::string, std::shared_ptr<PortConfiguration>> name_to_port_config;
std::unordered_map<uint16_t, std::shared_ptr<PortConfiguration>> number_to_port_config;
std::string username;
uint16_t dns_server_port;
std::vector<std::string> ip_stack_addresses;
bool ip_stack_debug;
bool allow_unregistered_users;
bool item_tracking_enabled;
RunShellBehavior run_shell_behavior;
PSOBBEncryption::KeyFile default_key_file;
std::vector<std::shared_ptr<const PSOBBEncryption::KeyFile>> bb_private_keys;
std::shared_ptr<const FunctionCodeIndex> function_code_index;
std::shared_ptr<const DOLFileIndex> dol_file_index;
std::shared_ptr<const Ep3DataIndex> ep3_data_index;
std::shared_ptr<const QuestIndex> quest_index;
std::shared_ptr<const LevelTable> level_table;
std::shared_ptr<const BattleParamTable> battle_params;
@@ -49,11 +60,20 @@ struct ServerState {
std::shared_ptr<LicenseManager> license_manager;
std::vector<MenuItem> main_menu;
std::shared_ptr<std::vector<MenuItem>> information_menu;
std::shared_ptr<std::vector<MenuItem>> information_menu_pc;
std::shared_ptr<std::vector<MenuItem>> information_menu_gc;
std::shared_ptr<std::vector<std::u16string>> information_contents;
std::vector<MenuItem> proxy_destinations_menu_pc;
std::vector<MenuItem> proxy_destinations_menu_gc;
std::vector<std::pair<std::string, uint16_t>> proxy_destinations_pc;
std::vector<std::pair<std::string, uint16_t>> proxy_destinations_gc;
std::pair<std::string, uint16_t> proxy_destination_patch;
std::pair<std::string, uint16_t> proxy_destination_bb;
std::u16string welcome_message;
std::map<int64_t, std::shared_ptr<Lobby>> id_to_lobby;
std::vector<std::shared_ptr<Lobby>> public_lobby_search_order;
std::vector<std::shared_ptr<Lobby>> public_lobby_search_order_ep3;
std::atomic<int32_t> next_lobby_id;
uint8_t pre_lobby_event;
int32_t ep3_menu_song;
@@ -62,6 +82,9 @@ struct ServerState {
uint32_t local_address;
uint32_t external_address;
std::shared_ptr<ProxyServer> proxy_server;
std::shared_ptr<Server> game_server;
ServerState();
void add_client_to_available_lobby(std::shared_ptr<Client> c);
@@ -78,11 +101,19 @@ struct ServerState {
void add_lobby(std::shared_ptr<Lobby> l);
void remove_lobby(uint32_t lobby_id);
std::shared_ptr<Client> find_client(const char16_t* identifier = nullptr,
uint64_t serial_number = 0, std::shared_ptr<Lobby> l = nullptr);
std::shared_ptr<Client> find_client(
const std::u16string* identifier = nullptr,
uint64_t serial_number = 0,
std::shared_ptr<Lobby> l = nullptr);
uint32_t connect_address_for_client(std::shared_ptr<Client> c);
std::shared_ptr<const std::vector<MenuItem>> information_menu_for_version(GameVersion version);
const std::vector<MenuItem>& proxy_destinations_menu_for_version(GameVersion version);
const std::vector<std::pair<std::string, uint16_t>>& proxy_destinations_for_version(GameVersion version);
void set_port_configuration(
const std::unordered_map<std::string, PortConfiguration>& named_port_configuration);
const std::vector<PortConfiguration>& port_configs);
void create_menus(std::shared_ptr<const JSONObject> config_json);
};
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include <stdint.h>
#include <unordered_map>
#include "Player.hh"
size_t stack_size_for_item(uint8_t data0, uint8_t data1);
size_t stack_size_for_item(const ItemData& item);
extern const std::unordered_map<uint8_t, const char*> name_for_weapon_special;
extern const std::unordered_map<uint8_t, const char*> name_for_s_rank_special;
extern const std::unordered_map<uint32_t, const char*> name_for_primary_identifier;
const std::string& name_for_technique(uint8_t tech);
std::u16string u16name_for_technique(uint8_t tech);
uint8_t technique_for_name(const std::string& name);
uint8_t technique_for_name(const std::u16string& name);
const std::string& name_for_section_id(uint8_t section_id);
std::u16string u16name_for_section_id(uint8_t section_id);
uint8_t section_id_for_name(const std::string& name);
uint8_t section_id_for_name(const std::u16string& name);
const std::string& name_for_event(uint8_t event);
std::u16string u16name_for_event(uint8_t event);
uint8_t event_for_name(const std::string& name);
uint8_t event_for_name(const std::u16string& name);
const std::string& name_for_lobby_type(uint8_t type);
std::u16string u16name_for_lobby_type(uint8_t type);
uint8_t lobby_type_for_name(const std::string& name);
uint8_t lobby_type_for_name(const std::u16string& name);
const std::string& name_for_npc(uint8_t npc);
std::u16string u16name_for_npc(uint8_t npc);
uint8_t npc_for_name(const std::string& name);
uint8_t npc_for_name(const std::u16string& name);
const char* name_for_char_class(uint8_t cls);
const char* abbreviation_for_char_class(uint8_t cls);
const char* name_for_difficulty(uint8_t difficulty);
char abbreviation_for_difficulty(uint8_t difficulty);
const char* abbreviation_for_game_mode(uint8_t);
std::string name_for_item(const ItemData& item, bool include_color_codes);
+106 -158
View File
@@ -13,7 +13,7 @@ using namespace std;
int char16cmp(const char16_t* s1, const char16_t* s2, size_t count) {
int char16ncmp(const char16_t* s1, const char16_t* s2, size_t count) {
size_t x;
for (x = 0; x < count && s1[x] != 0 && s2[x] != 0; x++) {
if (s1[x] < s2[x]) {
@@ -30,30 +30,14 @@ int char16cmp(const char16_t* s1, const char16_t* s2, size_t count) {
return 0;
}
void char16cpy(char16_t* dest, const char16_t* src, size_t count) {
size_t x;
for (x = 0; x < count && src[x] != 0; x++) {
dest[x] = src[x];
}
if (x < count) {
dest[x] = 0;
}
}
size_t char16len(const char16_t* s) {
size_t x;
for (x = 0; s[x] != 0; x++);
return x;
}
static vector<char16_t> unicode_to_sjis_table_data;
static vector<char16_t> sjis_to_unicode_table_data;
static void load_sjis_tables() {
unicode_to_sjis_table_data.resize(0x10000);
sjis_to_unicode_table_data.resize(0x10000);
unicode_to_sjis_table_data.resize(0x10000, 0);
sjis_to_unicode_table_data.resize(0x10000, 0);
// TODO: this is inefficient; it makes multiple copies of the string
auto file_contents = load_file("system/sjis-table.ini");
@@ -85,155 +69,119 @@ static const vector<char16_t>& unicode_to_sjis_table() {
return unicode_to_sjis_table_data;
}
void encode_sjis(char* dest, const char16_t* source, size_t max) {
std::string encode_sjis(const char16_t* src, size_t src_count) {
const auto& table = unicode_to_sjis_table();
while (*source && (--max)) {
*(dest++) = table[*(source++)];
const char16_t* src_end = src + src_count;
string ret;
while ((src != src_end) && *src) {
uint16_t ch = *(src++);
uint16_t translated_c = table[ch];
if (translated_c == 0) {
throw runtime_error("untranslatable unicode character");
} else if (translated_c & 0xFF00) {
ret.push_back((translated_c >> 8) & 0xFF);
ret.push_back(translated_c & 0xFF);
} else {
ret.push_back(translated_c & 0xFF);
}
};
*dest = 0;
return ret;
}
void decode_sjis(char16_t* dest, const char* source, size_t max) {
size_t encode_sjis(
char* dest,
size_t dest_count,
const char16_t* src,
size_t src_count,
bool allow_skip_terminator) {
const auto& table = unicode_to_sjis_table();
if (dest_count == 0) {
throw logic_error("cannot encode into zero-length buffer");
}
const char* dest_start = dest;
const char16_t* src_end = src + src_count;
const char* dest_end = dest + (allow_skip_terminator ? dest_count : (dest_count - 1));
while ((dest != dest_end) && (src != src_end) && *src) {
uint16_t ch = *(src++);
uint16_t translated_c = table[ch];
if (translated_c == 0) {
throw runtime_error("untranslatable unicode character");
} else if (translated_c & 0xFF00) {
*(dest++) = (translated_c >> 8) & 0xFF;
// If the second byte of this character would cause the null to overrun
// the buffer, erase the first byte instead and return early
if (dest == dest_end) {
*(dest - 1) = 0;
} else {
*(dest++) = translated_c & 0xFF;
}
} else {
*(dest++) = translated_c & 0xFF;
}
}
if (!allow_skip_terminator || (dest != dest_end)) {
*dest = 0;
dest++;
}
return dest - dest_start;
}
std::u16string decode_sjis(const char* src, size_t src_count) {
const auto& table = sjis_to_unicode_table();
while (*source && (--max)) {
char16_t src_char = *(source++);
const char* src_end = src + src_count;
u16string ret;
while ((src != src_end) && *src) {
uint16_t src_char = *(src++);
if (src_char & 0x80) {
src_char = (src_char << 8) | *(source++);
if (src == src_end) {
throw runtime_error("incomplete extended character");
}
src_char = (src_char << 8) | *(src++);
if ((src_char & 0xFF) == 0) {
return;
throw runtime_error("incomplete extended character");
}
}
ret.push_back(table[src_char]);
};
return ret;
}
size_t decode_sjis(
char16_t* dest,
size_t dest_count,
const char* src,
size_t src_count,
bool allow_skip_terminator) {
const auto& table = sjis_to_unicode_table();
if (dest_count == 0) {
throw logic_error("cannot decode into zero-length buffer");
}
const char16_t* dest_start = dest;
const char* src_end = src + src_count;
const char16_t* dest_end = dest + (allow_skip_terminator ? dest_count : (dest_count - 1));
while ((dest != dest_end) && (src != src_end) && *src) {
uint16_t src_char = *(src++);
if (src_char & 0x80) {
if (src == src_end) {
throw runtime_error("incomplete extended character");
}
src_char = (src_char << 8) | *(src++);
if ((src_char & 0xFF) == 0) {
throw runtime_error("incomplete extended character");
}
}
*(dest++) = table[src_char];
};
*dest = 0;
}
std::string encode_sjis(const char16_t* source) {
const auto& table = unicode_to_sjis_table();
string ret;
while (*source) {
ret.push_back(table[*(source++)]);
};
return ret;
}
std::u16string decode_sjis(const char* source) {
const auto& table = sjis_to_unicode_table();
u16string ret;
while (*source) {
char16_t src_char = *(source++);
if (src_char & 0x80) {
src_char = (src_char << 8) | *(source++);
if ((src_char & 0xFF) == 0) {
return ret;
}
}
ret.push_back(table[src_char]);
};
return ret;
}
std::string encode_sjis(const std::u16string& source) {
const auto& table = unicode_to_sjis_table();
string ret;
for (char16_t ch : source) {
ret.push_back(table[ch]);
};
return ret;
}
std::u16string decode_sjis(const std::string& source) {
const auto& table = sjis_to_unicode_table();
u16string ret;
for (size_t x = 0; x < source.size(); x++) {
char16_t src_char = source[x];
if (src_char & 0x80) {
src_char = (src_char << 8) | source[++x];
if ((src_char & 0xFF) == 0) {
return ret;
}
}
ret.push_back(table[src_char]);
};
return ret;
}
void add_language_marker_inplace(char* a, char e, size_t dest_count) {
if ((a[0] == '\t') && (a[1] != 'C')) {
return;
if (!allow_skip_terminator || (dest != dest_end)) {
*(dest++) = 0;
}
size_t existing_count = strlen(a);
if (existing_count > dest_count - 3) {
existing_count = dest_count - 3;
}
memmove(&a[2], a, (existing_count + 1) * sizeof(char));
a[0] = '\t';
a[1] = e;
a[existing_count + 2] = 0;
}
void add_language_marker_inplace(char16_t* a, char16_t e, size_t dest_count) {
if ((a[0] == '\t') && (a[1] != 'C')) {
return;
}
size_t existing_count = char16len(a);
if (existing_count > dest_count - 3) {
existing_count = dest_count - 3;
}
memmove(&a[2], a, (existing_count + 1) * sizeof(char16_t));
a[0] = '\t';
a[1] = e;
a[existing_count + 2] = 0;
}
void remove_language_marker_inplace(char* a) {
if ((a[0] == '\t') && (a[1] != 'C')) {
strcpy(a, &a[2]);
}
}
void remove_language_marker_inplace(char16_t* a) {
if ((a[0] == '\t') && (a[1] != 'C')) {
char16cpy(a, &a[2], char16len(a) - 2);
}
}
std::string add_language_marker(const std::string& s, char marker) {
if ((s.size() >= 2) && (s[0] == '\t') && (s[1] != 'C')) {
return s;
}
string ret;
ret.push_back('\t');
ret.push_back(marker);
return ret + s;
}
std::u16string add_language_marker(const std::u16string& s, char16_t marker) {
if ((s.size() >= 2) && (s[0] == L'\t') && (s[1] != L'C')) {
return s;
}
u16string ret;
ret.push_back(L'\t');
ret.push_back(marker);
return ret + s;
}
std::string remove_language_marker(const std::string& s) {
if ((s.size() < 2) || (s[0] != '\t') || (s[1] == 'C')) {
return s;
}
return s.substr(2);
}
std::u16string remove_language_marker(const std::u16string& s) {
if ((s.size() < 2) || (s[0] != L'\t') || (s[1] == L'C')) {
return s;
}
return s.substr(2);
return dest - dest_start;
}
+558 -22
View File
@@ -2,32 +2,508 @@
#include <inttypes.h>
#include <stddef.h>
#include <string.h>
#include <string>
#include <stdexcept>
#include <phosg/Encoding.hh>
#include <phosg/Strings.hh>
int char16cmp(const char16_t* s1, const char16_t* s2, size_t count);
void char16cpy(char16_t* dest, const char16_t* src, size_t count);
size_t char16len(const char16_t* s);
// TODO: delete these if not needed
// int char16ncmp(const char16_t* s1, const char16_t* s2, size_t count);
// size_t char16len(const char16_t* s);
void encode_sjis(char* dest, const char16_t* source, size_t dest_count);
void decode_sjis(char16_t* dest, const char* source, size_t dest_count);
std::string encode_sjis(const char16_t* source);
std::u16string decode_sjis(const char* source);
std::string encode_sjis(const std::u16string& source);
std::u16string decode_sjis(const std::string& source);
// (1a) Conversion functions
// These return the number of characters written, including the terminating null
// character. In the case of encode_sjis, two-byte characters count as two
// characters, so the returned number is the number of bytes written.
// allow_skip_terminator means no null byte will be written if dest_count
// characters are written to the output. If this argument is false, a null
// terminator is always written, even if the string is truncated.
size_t encode_sjis(
char* dest, size_t dest_count,
const char16_t* src, size_t src_count,
bool allow_skip_terminator = false);
size_t decode_sjis(
char16_t* dest, size_t dest_count,
const char* src, size_t src_count,
bool allow_skip_terminator = false);
std::string encode_sjis(const char16_t* source, size_t src_count);
std::u16string decode_sjis(const char* source, size_t src_count);
inline std::string encode_sjis(const std::u16string& s) {
return encode_sjis(s.data(), s.size());
}
inline std::u16string decode_sjis(const std::string& s) {
return decode_sjis(s.data(), s.size());
}
// (1b) Type-independent utility functions
template <typename T>
size_t text_strlen_t(const T* s) {
size_t ret = 0;
for (; s[ret] != 0; ret++) { }
return ret;
}
template <typename T>
size_t text_strnlen_t(const T* s, size_t count) {
size_t ret = 0;
for (; s[ret] != 0 && ret < count; ret++) { }
return ret;
}
template <typename T>
size_t text_streq_t(const T* a, const T* b) {
for (;;) {
if (*a != *b) {
return false;
}
if (*a == 0) {
return true;
}
a++;
b++;
}
}
template <typename T>
size_t text_strneq_t(const T* a, const T* b, size_t count) {
for (; count; count--) {
if (*a != *b) {
return false;
}
if (*a == 0) {
return true;
}
a++;
b++;
}
return true;
}
template <typename T>
size_t text_strncpy_t(T* dest, const T* src, size_t count) {
size_t x;
for (x = 0; x < count && src[x] != 0; x++) {
dest[x] = src[x];
}
if (x < count) {
dest[x++] = 0;
}
return x;
}
// Like strncpy, but *always* null-terminates the string, even if it has to
// truncate it.
template <typename T>
size_t text_strnzcpy_t(T* dest, const T* src, size_t count) {
size_t x;
for (x = 0; x < count - 1 && src[x] != 0; x++) {
dest[x] = src[x];
}
dest[x++] = 0;
return x;
}
void add_language_marker_inplace(char* s, char marker, size_t dest_count);
void add_language_marker_inplace(char16_t* s, char16_t marker, size_t dest_count);
void remove_language_marker_inplace(char* s);
void remove_language_marker_inplace(char16_t* s);
std::string add_language_marker(const std::string& s, char marker);
std::u16string add_language_marker(const std::u16string& s, char16_t marker);
std::string remove_language_marker(const std::string& s);
std::u16string remove_language_marker(const std::u16string& s);
// (2) Type conversion functions
template <typename DestT, typename SrcT = DestT>
size_t text_strncpy_t(DestT*, size_t, const SrcT*, size_t) {
static_assert(always_false<DestT, SrcT>::v,
"unspecialized text_strncpy_t should never be called");
return 0;
}
template <>
inline size_t text_strncpy_t<char>(
char* dest, size_t dest_count, const char* src, size_t src_count) {
size_t count = std::min<size_t>(dest_count, src_count);
return text_strncpy_t(dest, src, count);
}
template <>
inline size_t text_strncpy_t<char, char16_t>(
char* dest, size_t dest_count, const char16_t* src, size_t src_count) {
return encode_sjis(dest, dest_count, src, src_count, true);
}
template <>
inline size_t text_strncpy_t<char16_t, char>(
char16_t* dest, size_t dest_count, const char* src, size_t src_count) {
return decode_sjis(dest, dest_count, src, src_count, true);
}
template <>
inline size_t text_strncpy_t<char16_t>(
char16_t* dest, size_t dest_count, const char16_t* src, size_t src_count) {
size_t count = std::min<size_t>(dest_count, src_count);
return text_strncpy_t(dest, src, count);
}
template <typename DestT, typename SrcT = DestT>
size_t text_strnzcpy_t(DestT*, size_t, const SrcT*, size_t) {
static_assert(always_false<DestT, SrcT>::v,
"unspecialized text_strnzcpy_t should never be called");
return 0;
}
template <>
inline size_t text_strnzcpy_t<char>(
char* dest, size_t dest_count, const char* src, size_t src_count) {
size_t count = std::min<size_t>(dest_count, src_count);
return text_strnzcpy_t(dest, src, count);
}
template <>
inline size_t text_strnzcpy_t<char, char16_t>(
char* dest, size_t dest_count, const char16_t* src, size_t src_count) {
return encode_sjis(dest, dest_count, src, src_count);
}
template <>
inline size_t text_strnzcpy_t<char16_t, char>(
char16_t* dest, size_t dest_count, const char* src, size_t src_count) {
return decode_sjis(dest, dest_count, src, src_count);
}
template <>
inline size_t text_strnzcpy_t<char16_t>(
char16_t* dest, size_t dest_count, const char16_t* src, size_t src_count) {
size_t count = std::min<size_t>(dest_count, src_count);
return text_strnzcpy_t(dest, src, count);
}
// (3) Packed text objects for use in protocol structs
template <typename ItemT, size_t Count>
struct parray {
ItemT items[Count];
parray() {
this->clear();
}
parray(const parray& other) {
this->operator=(other);
}
parray(parray&& s) = delete;
template <size_t OtherCount>
parray(const parray<ItemT, OtherCount>& s) {
this->operator=(s);
}
constexpr size_t size() {
return Count;
}
constexpr size_t bytes() {
return Count * sizeof(ItemT);
}
ItemT* data() {
return this->items;
}
const ItemT* data() const {
return this->items;
}
ItemT& operator[](size_t index) {
if (index >= Count) {
throw std::out_of_range("array index out of bounds");
}
return this->items[index];
}
const ItemT& operator[](size_t index) const {
if (index >= Count) {
throw std::out_of_range("array index out of bounds");
}
return this->items[index];
}
// TODO: These can be made faster by only clearing the unused space after the
// strncpy_t (if any) instead of clearing all the space every time
parray& operator=(const parray& s) {
for (size_t x = 0; x < Count; x++) {
this->items[x] = s.items[x];
}
return *this;
}
parray& operator=(parray&& s) = delete;
template <size_t OtherCount>
parray& operator=(const parray<ItemT, OtherCount>& s) {
if (OtherCount <= Count) {
size_t x;
for (x = 0; x < OtherCount; x++) {
this->items[x] = s.items[x];
}
for (; x < Count; x++) {
this->items[x] = 0;
}
} else {
for (size_t x = 0; x < Count; x++) {
this->items[x] = s.items[x];
}
}
return *this;
}
parray& operator=(const ItemT* s) {
if (!s) {
throw std::logic_error("attempted to assign nullptr to parray");
}
for (size_t x = 0; x < Count; x++) {
this->items[x] = s[x];
}
return *this;
}
bool operator==(const parray& s) const {
for (size_t x = 0; x < Count; x++) {
if (this->items[x] != s.items[x]) {
return false;
}
}
return true;
}
bool operator!=(const parray& s) const {
return !this->operator==(s);
}
void clear(ItemT v = 0) {
for (size_t x = 0; x < Count; x++) {
this->items[x] = v;
}
}
void clear_after(size_t position, ItemT v = 0) {
for (size_t x = position; x < Count; x++) {
this->items[x] = v;
}
}
bool is_filled_with(ItemT v) const {
for (size_t x = 0; x < Count; x++) {
if (this->items[x] != v) {
return false;
}
}
return true;
}
} __attribute__((packed));
// TODO: It appears that these actually do not have to be null-terminated in PSO
// commands some of the time. As an example, creating a game with a name with
// the maximum length results in a C1 command with no null byte between the game
// name and the password. We should be able to handle this by making ptexts not
// required to be null-terminated in storage - this will still be safe if we
// limit all operations by Count.
template <typename CharT, size_t Count>
struct ptext : parray<CharT, Count> {
ptext() {
this->clear();
}
ptext(const ptext& other) : parray<CharT, Count>(other) { }
ptext(ptext&& s) = delete;
template <typename OtherCharT>
ptext(const OtherCharT* s) {
if (!s) {
throw std::logic_error("attempted to assign nullptr to ptext");
}
this->operator=(s);
}
template <typename OtherCharT>
ptext(const OtherCharT* s, size_t count) {
if (!s) {
throw std::logic_error("attempted to assign nullptr to ptext");
}
this->assign(s, count);
}
template <typename OtherCharT>
ptext(const std::basic_string<OtherCharT>& s) {
this->operator=(s);
}
template <typename OtherCharT, size_t OtherCount>
ptext(const ptext<OtherCharT, OtherCount>& s) {
this->operator=(s);
}
size_t len() const {
return text_strnlen_t(this->items, Count);
}
// Q: Why is there no c_str() here?
// A: Because the contents of a ptext don't have to be null-terminated.
ptext& operator=(const ptext& s) {
memcpy(this->items, s.items, sizeof(CharT) * Count);
return *this;
}
ptext& operator=(ptext&& s) = delete;
template <typename OtherCharT>
ptext& operator=(const OtherCharT* s) {
if (!s) {
throw std::logic_error("attempted to assign nullptr to ptext");
}
size_t chars_written = text_strncpy_t(this->items, Count, s, Count);
this->clear_after(chars_written);
return *this;
}
template <typename OtherCharT>
ptext& assign(const OtherCharT* s, size_t s_count) {
if (!s) {
throw std::logic_error("attempted to assign nullptr to ptext");
}
size_t chars_written = text_strncpy_t(this->items, Count, s, s_count);
this->clear_after(chars_written);
return *this;
}
template <typename OtherCharT>
ptext& operator=(const std::basic_string<OtherCharT>& s) {
size_t chars_written = text_strncpy_t(this->items, Count, s.c_str(), s.size());
this->clear_after(chars_written);
return *this;
}
template <typename OtherCharT, size_t OtherCount>
ptext& operator=(const ptext<OtherCharT, OtherCount>& s) {
size_t chars_written = text_strncpy_t(this->items, Count, s.items, OtherCount);
this->clear_after(chars_written);
return *this;
}
template <typename OtherCharT>
bool operator==(const OtherCharT* s) const {
if (!s) {
throw std::logic_error("attempted to compare ptext to nullptr");
}
return text_strneq_t(this->items, s, Count);
}
template <typename OtherCharT>
bool operator==(const std::basic_string<OtherCharT>& s) const {
return text_strneq_t(this->items, s.c_str(), Count);
}
template <typename OtherCharT, size_t OtherCount>
bool operator==(const ptext<OtherCharT, OtherCount>& s) const {
return text_strneq_t(this->items, s.items, std::min<size_t>(Count, OtherCount));
}
template <typename OtherCharT>
bool operator!=(const OtherCharT* s) const {
if (!s) {
throw std::logic_error("attempted to compare ptext to nullptr");
}
return !this->operator==(s);
}
template <typename OtherCharT>
bool operator!=(const std::basic_string<OtherCharT>& s) const {
return !this->operator==(s);
}
template <typename OtherCharT, size_t OtherCount>
bool operator!=(const ptext<OtherCharT, OtherCount>& s) const {
return !this->operator==(s);
}
template <typename OtherCharT>
bool eq_n(const OtherCharT* s, size_t count) const {
if (!s) {
throw std::logic_error("attempted to compare ptext to nullptr");
}
return text_strneq_t(this->items, s, count);
}
template <typename OtherCharT>
bool eq_n(const std::basic_string<OtherCharT>& s, size_t count) const {
return text_strneq_t(this->items, s.c_str(), count);
}
template <typename OtherCharT, size_t OtherCount>
bool eq_n(const ptext<OtherCharT, OtherCount>& s, size_t count) const {
return text_strneq_t(this->items, s.items, count);
}
operator std::basic_string<CharT>() const {
return std::basic_string<CharT>(this->items, this->len());
}
bool empty() const {
return (this->items[0] == 0);
}
} __attribute__((packed));
// (4) Markers and character replacement
template <typename CharT>
std::basic_string<CharT> add_language_marker(
const std::basic_string<CharT>& s, CharT marker) {
if ((s.size() >= 2) && (s[0] == '\t') && (s[1] != 'C')) {
return s;
}
std::basic_string<CharT> ret;
ret.push_back('\t');
ret.push_back(marker);
ret += s;
return ret;
}
template <typename CharT, size_t Count>
std::basic_string<CharT> add_language_marker(
const ptext<CharT, Count>& s, CharT marker) {
if ((s.items[0] == '\t') && (s.items[1] != 'C')) {
return s;
}
std::basic_string<CharT> ret;
ret.push_back('\t');
ret.push_back(marker);
ret += s;
return ret;
}
template <typename CharT>
const CharT* remove_language_marker(const CharT* s) {
if ((s[0] != '\t') || (s[1] == 'C')) {
return s;
}
return s + 2;
}
template <typename CharT, size_t Count>
std::basic_string<CharT> remove_language_marker(const ptext<CharT, Count>& s) {
if ((s.items[0] != '\t') || (s.items[1] == L'C')) {
return s;
}
return &s.items[2];
}
template <typename CharT>
std::basic_string<CharT> remove_language_marker(
const std::basic_string<CharT>& s) {
if ((s.size() < 2) || (s[0] != L'\t') || (s[1] == L'C')) {
return s;
}
return s.substr(2);
}
template <typename CharT, size_t Count>
void remove_language_marker_inplace(ptext<CharT, Count>& a) {
if ((a.items[0] == '\t') && (a.items[1] != 'C')) {
text_strnzcpy_t(a.items, Count, &a.items[2], Count);
a.items[text_strlen_t(a.items) + 1] = 0;
}
}
template <typename T>
void replace_char_inplace(T* a, T f, T r) {
@@ -40,23 +516,26 @@ void replace_char_inplace(T* a, T f, T r) {
}
template <typename T>
size_t add_color_inplace(T* a) {
size_t add_color_inplace(T* a, size_t max_chars) {
T* d = a;
T* orig_d = d;
while (*a) {
for (size_t x = 0; (x < max_chars) && *a; x++) {
if (*a == '$') {
*(d++) = '\t';
} else if (*a == '#') {
*(d++) = '\n';
} else if (*a == '%') {
a++;
x++;
if (*a == 's') {
*(d++) = '$';
} else if (*a == '%') {
*(d++) = '%';
} else if (*a == 'n') {
*(d++) = '#';
} else if (*a == '\0') {
break;
} else {
*(d++) = *a;
}
@@ -66,12 +545,69 @@ size_t add_color_inplace(T* a) {
a++;
}
*d = 0;
// TODO: we should clear the chars after the null if the new string is shorter
// than the original
return d - orig_d;
}
template <typename T>
void add_color_inplace(std::basic_string<T>& a, size_t header_bytes) {
size_t count = add_color_inplace(a.data() + header_bytes);
a.resize(count + header_bytes);
void add_color(StringWriter& w, const T* src, size_t max_input_chars) {
for (size_t x = 0; (x < max_input_chars) && *src; x++) {
if (*src == '$') {
w.put<T>('\t');
} else if (*src == '#') {
w.put<T>('\n');
} else if (*src == '%') {
src++;
x++;
if (*src == 's') {
w.put<T>('$');
} else if (*src == '%') {
w.put<T>('%');
} else if (*src == 'n') {
w.put<T>('#');
} else if (*src == '\0') {
break;
} else {
w.put<T>(*src);
}
} else {
w.put<T>(*src);
}
src++;
}
w.put<T>(0);
}
template <typename CharT, size_t Count>
void add_color_inplace(ptext<CharT, Count>& t) {
size_t sx = 0;
size_t dx = 0;
for (; (sx < Count - 1) && t.items[sx]; sx++) {
if (t.items[sx] == '$') {
t.items[dx] = '\t';
} else if (t.items[sx] == '#') {
t.items[dx] = '\n';
} else if (t.items[sx] == '%') {
sx++;
if ((sx == Count - 1) || (t.items[sx] == '\0')) {
break;
} else if (t.items[sx] == 's') {
t.items[dx] = '$';
} else if (t.items[sx] == '%') {
t.items[dx] = '%';
} else if (t.items[sx] == 'n') {
t.items[dx] = '#';
} else {
t.items[dx] = t.items[sx];
}
} else {
t.items[dx] = t.items[sx];
}
dx++;
}
for (; dx < Count; dx++) {
t.items[dx] = 0;
}
}
+58 -19
View File
@@ -1,8 +1,10 @@
#include "Version.hh"
#include <strings.h>
#include <stdexcept>
#include <strings.h>
#include "Client.hh"
using namespace std;
@@ -13,34 +15,34 @@ uint16_t flags_for_version(GameVersion version, uint8_t sub_version) {
case 0x00: // initial check (before 9E recognition)
switch (version) {
case GameVersion::DC:
return ClientFlag::DefaultV2DC;
return Client::Flag::DEFAULT_V2_DC;
case GameVersion::GC:
return ClientFlag::DefaultV3GC;
return Client::Flag::DEFAULT_V3_GC;
case GameVersion::PC:
return ClientFlag::DefaultV2PC;
case GameVersion::Patch:
return ClientFlag::DefaultV2PC;
return Client::Flag::DEFAULT_V2_PC;
case GameVersion::PATCH:
return Client::Flag::DEFAULT_V2_PC;
case GameVersion::BB:
return ClientFlag::DefaultV3BB;
return Client::Flag::DEFAULT_V4_BB;
}
break;
case 0x29: // PSO PC
return ClientFlag::DefaultV2PC;
return Client::Flag::DEFAULT_V2_PC;
case 0x30: // ???
case 0x31: // PSO Ep1&2 US10, US11, EU10, JP10
case 0x33: // PSO Ep1&2 EU50HZ
case 0x34: // PSO Ep1&2 JP11
return ClientFlag::DefaultV3GC;
return Client::Flag::DEFAULT_V3_GC;
case 0x32: // PSO Ep1&2 US12, JP12
case 0x35: // PSO Ep1&2 US12, JP12
case 0x36: // PSO Ep1&2 US12, JP12
case 0x39: // PSO Ep1&2 US12, JP12
return ClientFlag::DefaultV3GCPlus;
return Client::Flag::DEFAULT_V3_GC_PLUS;
case 0x40: // PSO Ep3 trial
case 0x41: // PSO Ep3 US
case 0x42: // PSO Ep3 JP
case 0x43: // PSO Ep3 UK
return ClientFlag::DefaultV4;
return Client::Flag::DEFAULT_V3_GC_EP3;
}
return 0;
}
@@ -55,7 +57,7 @@ const char* name_for_version(GameVersion version) {
return "BB";
case GameVersion::DC:
return "DC";
case GameVersion::Patch:
case GameVersion::PATCH:
return "Patch";
default:
return "Unknown";
@@ -65,16 +67,53 @@ const char* name_for_version(GameVersion version) {
GameVersion version_for_name(const char* name) {
if (!strcasecmp(name, "DC") || !strcasecmp(name, "DreamCast")) {
return GameVersion::DC;
}
if (!strcasecmp(name, "PC")) {
} else if (!strcasecmp(name, "PC")) {
return GameVersion::PC;
}
if (!strcasecmp(name, "GC") || !strcasecmp(name, "GameCube")) {
} else if (!strcasecmp(name, "GC") || !strcasecmp(name, "GameCube")) {
return GameVersion::GC;
}
if (!strcasecmp(name, "BB") || !strcasecmp(name, "BlueBurst") ||
} else if (!strcasecmp(name, "BB") || !strcasecmp(name, "BlueBurst") ||
!strcasecmp(name, "Blue Burst")) {
return GameVersion::BB;
} else if (!strcasecmp(name, "Patch")) {
return GameVersion::PATCH;
} else {
throw invalid_argument("incorrect version name");
}
throw invalid_argument("incorrect version name");
}
const char* name_for_server_behavior(ServerBehavior behavior) {
switch (behavior) {
case ServerBehavior::SPLIT_RECONNECT:
return "split_reconnect";
case ServerBehavior::LOGIN_SERVER:
return "login_server";
case ServerBehavior::LOBBY_SERVER:
return "lobby_server";
case ServerBehavior::DATA_SERVER_BB:
return "data_server_bb";
case ServerBehavior::PATCH_SERVER:
return "patch_server";
case ServerBehavior::PROXY_SERVER:
return "proxy_server";
default:
throw logic_error("invalid server behavior");
}
}
ServerBehavior server_behavior_for_name(const char* name) {
if (!strcasecmp(name, "split_reconnect")) {
return ServerBehavior::SPLIT_RECONNECT;
} else if (!strcasecmp(name, "login_server") || !strcasecmp(name, "login")) {
return ServerBehavior::LOGIN_SERVER;
} else if (!strcasecmp(name, "lobby_server") || !strcasecmp(name, "lobby")) {
return ServerBehavior::LOBBY_SERVER;
} else if (!strcasecmp(name, "data_server_bb") || !strcasecmp(name, "data_server") || !strcasecmp(name, "data")) {
return ServerBehavior::DATA_SERVER_BB;
} else if (!strcasecmp(name, "patch_server") || !strcasecmp(name, "patch")) {
return ServerBehavior::PATCH_SERVER;
} else if (!strcasecmp(name, "proxy_server") || !strcasecmp(name, "proxy")) {
return ServerBehavior::PROXY_SERVER;
} else {
throw invalid_argument("incorrect server behavior name");
}
}
+11 -29
View File
@@ -7,41 +7,23 @@
enum class GameVersion {
DC = 0,
PC,
Patch,
PATCH,
GC,
BB,
};
enum ClientFlag {
// After joining a lobby, client will no longer send D6 commands when they close message boxes
NoMessageBoxCloseConfirmationAfterLobbyJoin = 0x0004,
// Client has the above flag and has already joined a lobby
NoMessageBoxCloseConfirmation = 0x0008,
// Client can see Ep3 lobbies
CanSeeExtraLobbies = 0x0010,
// Client is episode 3 and should use its game mechanic
Episode3Games = 0x0020,
// Client is DC v1 (disables some features)
IsDCv1 = 0x0040,
// Client is loading into a game
Loading = 0x0080,
// Client is in the information menu (login server only)
InInformationMenu = 0x0100,
// Client is at the welcome message (login server only)
AtWelcomeMessage = 0x0200,
// Note: There isn't a good way to detect Episode 3 until the player data is
// sent (via a 61 command), so the Episode3Games flag is set in that handler
DefaultV1 = IsDCv1,
DefaultV2DC = 0x0000,
DefaultV2PC = 0x0000,
DefaultV3GC = 0x0000,
DefaultV3GCPlus = NoMessageBoxCloseConfirmationAfterLobbyJoin,
DefaultV3BB = NoMessageBoxCloseConfirmationAfterLobbyJoin | NoMessageBoxCloseConfirmation,
DefaultV4 = NoMessageBoxCloseConfirmationAfterLobbyJoin | CanSeeExtraLobbies | Episode3Games,
enum class ServerBehavior {
SPLIT_RECONNECT = 0,
LOGIN_SERVER,
LOBBY_SERVER,
DATA_SERVER_BB,
PATCH_SERVER,
PROXY_SERVER,
};
uint16_t flags_for_version(GameVersion version, uint8_t sub_version);
const char* name_for_version(GameVersion version);
GameVersion version_for_name(const char* name);
const char* name_for_server_behavior(ServerBehavior behavior);
ServerBehavior server_behavior_for_name(const char* name);
View File
View File
View File
View File
View File
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File

Some files were not shown because too many files have changed in this diff Show More