Compare commits

..

281 Commits

Author SHA1 Message Date
Martin Michelsen 81edf93e3b handle V2 mag encoding properly 2023-10-21 11:48:31 -07:00
Martin Michelsen 05d508230b remove unneeded include 2023-10-20 16:27:55 -07:00
Martin Michelsen 27734a6944 use correct ItemPT table in Challenge mode 2023-10-20 16:23:23 -07:00
Martin Michelsen bf346d3f95 fix Word Select mapping across versions 2023-10-20 16:19:03 -07:00
Martin Michelsen 6933a4338b fix early idle timeout in IP stack simulator 2023-10-19 23:16:16 -07:00
Martin Michelsen 08361ad597 add extract-afs command 2023-10-19 18:55:11 -07:00
Martin Michelsen fbefb1fb21 fix comment about enemy DAR 2023-10-19 18:43:08 -07:00
Matt 08dd67d894 Correct EU Episode III connection port 2023-10-19 18:28:35 -07:00
Martin Michelsen de0e56f37c fix EXP multiplier command 2023-10-19 17:24:16 -07:00
Martin Michelsen 57a5476ff8 remove some done items from to-do list 2023-10-19 15:54:46 -07:00
Martin Michelsen a211bd07ac implement 6xD2 command 2023-10-19 15:45:32 -07:00
Martin Michelsen 5a30272869 implement some BB quest commands 2023-10-19 15:34:06 -07:00
Martin Michelsen 4bc5f1b90f describe entire battle rules structure 2023-10-18 23:30:27 -07:00
Martin Michelsen c0c7bf9b16 fix incorrect game base_version setting 2023-10-18 21:15:45 -07:00
Martin Michelsen 6ec41a279e add more missing includes 2023-10-18 17:33:46 -07:00
Martin Michelsen 519933c44d add missing include 2023-10-18 17:27:14 -07:00
Martin Michelsen 9d0ba3a97b fix class replacement for v3/v2 compatibility 2023-10-18 17:22:26 -07:00
Martin Michelsen 2e36cebbcc implement redirect destinations 2023-10-18 17:22:07 -07:00
Martin Michelsen e8d8b94ffa implement character overlays for challenge mode 2023-10-18 17:16:51 -07:00
Martin Michelsen 8c2ce5210d implement battle rules and character replacement 2023-10-18 11:57:13 -07:00
Martin Michelsen 13dacc013a document CardDefinition and a few other things 2023-10-17 15:51:29 -07:00
Martin Michelsen 85ef84a6d5 fix wording in Ep3 card transformation comment 2023-10-17 10:24:11 -07:00
Martin Michelsen 08a1bf3238 implement server drop tables 2023-10-16 23:10:13 -07:00
Martin Michelsen d66c1f5de9 add ItemRT.afs decoder 2023-10-16 21:14:38 -07:00
Martin Michelsen ba09188b82 add TODO about Ep3 ranks 2023-10-16 19:37:38 -07:00
Martin Michelsen 0bb9718da3 add JSON rare table conversion 2023-10-16 19:37:29 -07:00
Martin Michelsen 04d92d93e5 don't show weapon percents if they're all zero 2023-10-16 19:36:39 -07:00
Martin Michelsen e2f72f3088 split Ep3 and Ep1&2 quest indexes 2023-10-16 17:54:04 -07:00
Martin Michelsen 22ceb2d1f7 fix cross-language quest loading 2023-10-16 11:43:06 -07:00
Martin Michelsen 112896bb34 add non-English versions of several GC quests 2023-10-16 11:34:11 -07:00
Martin Michelsen 5d71b66f84 implement quest version separation 2023-10-16 00:20:38 -07:00
Martin Michelsen 7005b573f5 add names to IPS listen sockets 2023-10-14 21:28:02 -07:00
Martin Michelsen 7d95efa803 don't warn for DHCP release commands 2023-10-13 10:07:43 -07:00
Martin Michelsen 0a3528b978 explicitly clear unused_bootp_legacy 2023-10-13 00:54:46 -07:00
Martin Michelsen 78698a0a89 implement DHCP in IPStackSimulator 2023-10-13 00:45:27 -07:00
Martin Michelsen 01033287f2 add AITalk.bin format notes 2023-10-13 00:37:32 -07:00
Martin Michelsen 1d8c78166d allow trap cards to be customized 2023-10-12 17:55:37 -07:00
Martin Michelsen 4e29f22655 rename Leader/Group function in Ep3 server 2023-10-12 11:46:33 -07:00
Martin Michelsen 31c0a35bb6 reorganize and expand save file tests 2023-10-12 10:57:47 -07:00
Martin Michelsen 9fd19d2676 name more fields in save structs 2023-10-12 10:55:42 -07:00
Martin Michelsen bb89bc9b7b document flags field in character save file struct 2023-10-10 23:53:56 -07:00
Martin Michelsen 76ad50886f add $matcount command 2023-10-10 23:53:33 -07:00
Martin Michelsen 8b1fab916d update some effect names and descriptions 2023-10-10 15:45:27 -07:00
Martin Michelsen 16bb320ed8 add yet another spectator metadata send 2023-10-10 15:14:20 -07:00
Martin Michelsen 453a05fb8c reformat comment 2023-10-10 11:50:33 -07:00
Martin Michelsen c33af99ae5 name some previously-unknown fields 2023-10-10 10:55:49 -07:00
Martin Michelsen 8ad27e9001 fix trial quest for downloading 2023-10-09 21:41:38 -07:00
Martin Michelsen 132daf2c0e retitle Pinz's Shop code appropriately 2023-10-09 20:53:36 -07:00
Martin Michelsen d39f1eb74c add Ep3 proxy option for infinite time 2023-10-09 18:51:19 -07:00
Martin Michelsen 9da756cc14 fix control characters in text-english.json 2023-10-09 18:39:54 -07:00
Martin Michelsen a693fcd48e update note on simple mail command 2023-10-09 18:06:15 -07:00
Martin Michelsen 462f4842aa add unused sound effects code 2023-10-09 18:05:53 -07:00
Martin Michelsen 99fff5baf2 add default dice text files 2023-10-09 11:07:18 -07:00
Martin Michelsen 40da9e5604 add TextDice to disassembly 2023-10-09 11:06:47 -07:00
Martin Michelsen 41c07a3da8 update format of cards.html and make generation faster 2023-10-09 10:00:00 -07:00
Martin Michelsen 9677d0fca4 add default text archive for ep3 card disassembly 2023-10-09 09:10:39 -07:00
Martin Michelsen a674721727 add text archive encoder/decoder 2023-10-08 23:00:18 -07:00
Martin Michelsen aa76631073 update some comments 2023-10-08 17:57:40 -07:00
Martin Michelsen 3902c64424 fix $spec in tournaments 2023-10-08 17:57:24 -07:00
Martin Michelsen 226140deb7 load correct version of quest in cross-play games 2023-10-08 15:45:42 -07:00
Martin Michelsen 812310054c fix 6x70 handling in dc/pc cross-play 2023-10-08 15:20:48 -07:00
Martin Michelsen 5673de78be fix up to-do list 2023-10-08 15:20:15 -07:00
Martin Michelsen 32af88cd9b undo accidental language tag change 2023-10-08 14:53:52 -07:00
Martin Michelsen 3bb8ac5c43 fix BB play_time handling with long char names 2023-10-08 14:50:32 -07:00
Martin Michelsen ea7f655408 implement Episode 3 download quest categories 2023-10-08 14:19:31 -07:00
Martin Michelsen 948985b057 describe card effect name_index field 2023-10-08 12:35:34 -07:00
Martin Michelsen 8df36ea3c2 index quests by number, then by version 2023-10-08 11:14:46 -07:00
Martin Michelsen e723e80171 fix show-ep3-maps 2023-10-08 09:25:01 -07:00
Martin Michelsen 29dd0caaab fix QST encoding issues 2023-10-08 09:15:19 -07:00
Martin Michelsen 30394e7120 consolidate args in quest disassembler 2023-10-07 22:38:51 -07:00
Martin Michelsen eee420f2e1 update to-do list 2023-10-07 21:51:02 -07:00
Martin Michelsen 065c11ac90 only show leader change if player notifs are on 2023-10-07 21:51:02 -07:00
Martin Michelsen 6bebcc841e implement overflow lobbies 2023-10-07 21:10:08 -07:00
Martin Michelsen c2b2239df0 bring back patch_flycast_memory.py 2023-10-07 21:10:08 -07:00
Martin Michelsen abd87054ac add cross-play options 2023-10-07 20:43:35 -07:00
Martin Michelsen 07b1e9cde3 enforce 6x command size limit 2023-10-07 18:22:58 -07:00
Martin Michelsen d5cc91a9bf handle inventory extension data properly 2023-10-07 18:17:54 -07:00
Martin Michelsen 9fd90ee324 update names used in Ep3 disassembly 2023-10-07 13:59:48 -07:00
Martin Michelsen 8a6a7fb47f update some command notes 2023-10-07 12:27:23 -07:00
Martin Michelsen f77e21800c fix QST encoder 2023-10-07 08:38:06 -07:00
Martin Michelsen 2478f18298 don't forward Ep3 server commands sent by clients 2023-10-07 08:38:06 -07:00
Martin Michelsen bb1c0f1d1a add shell server TODO 2023-10-07 08:38:06 -07:00
Matt 9cf53c85a2 pls work 2023-10-07 08:36:38 -07:00
Matt ab5d8e4522 Correct Readme spacing 2023-10-07 08:36:38 -07:00
Matt e4bb5bc28c Remove patch_flycast_memory.py 2023-10-07 08:36:38 -07:00
Matt 1cb0d5bcec Update Flycast connection instructions 2023-10-07 08:36:38 -07:00
Martin Michelsen 88d887a58a combine maps-free and maps-quest into one directory 2023-10-05 22:49:54 -07:00
Martin Michelsen 77f64d3496 document PC save encrypt/decrypt actions 2023-10-05 22:49:54 -07:00
Martin Michelsen cdb3943d9f rename and document DC serial number functions 2023-10-05 22:49:54 -07:00
Martin Michelsen 532bcab0b6 add debug messages for previously-unused CAx commands 2023-10-05 10:38:40 -07:00
Martin Michelsen ab3c27772e document more Ep3 battle subcommands 2023-10-05 10:28:34 -07:00
Martin Michelsen 682632f1c5 implement GBA file not found command 2023-10-05 10:28:04 -07:00
Martin Michelsen 6850bc0e06 update some command notes 2023-10-05 10:27:38 -07:00
Martin Michelsen 6368ebcd71 use spectator text field for something 2023-10-05 00:06:34 -07:00
Martin Michelsen a23dabd58e fix spectator count on joining a spectator team 2023-10-05 00:00:18 -07:00
Martin Michelsen da37fc1fee document spectator message in 6xB4x52 command 2023-10-05 00:00:18 -07:00
Martin Michelsen 15c08c0101 add more info to 6xB4x46 version messages 2023-10-05 00:00:18 -07:00
Martin Michelsen 7e84a5cb6a update some comments 2023-10-04 10:40:18 -07:00
Martin Michelsen 3c4019f705 add simulator AR code 2023-10-04 10:40:05 -07:00
Martin Michelsen 040356d365 fix silly gcc warning 2023-10-03 21:20:06 -07:00
Martin Michelsen f0c339e040 make tournament deck selection start at the same time for all players 2023-10-03 21:13:09 -07:00
Martin Michelsen 38aaffd4bd add TODO for DC/PC crossplay 2023-10-03 21:13:09 -07:00
Matt e81e60b543 Unhide VR category for V1/2 2023-10-03 19:40:06 -07:00
Martin Michelsen da48712449 absolve myself of some longstanding laziness 2023-10-03 19:35:39 -07:00
Martin Michelsen ceefe44b96 add some to-do items 2023-10-03 19:35:39 -07:00
Martin Michelsen bc22327361 add support for all DC versions 2023-10-03 17:58:24 -07:00
Martin Michelsen 37c4cbd8f3 undo battle table fast loading change 2023-10-02 16:58:14 -07:00
Martin Michelsen d90fc2a543 make encryption objects serializable 2023-10-01 22:44:56 -07:00
Martin Michelsen 2dca523a4b clean up comment about Ep3 reverse-engineering 2023-10-01 22:44:56 -07:00
Martin Michelsen 4aa156a322 only show recording messages if Ep3 debug messages are on 2023-10-01 19:27:59 -07:00
Martin Michelsen e9b6b681bd fix Ep3 spectator test 2023-10-01 15:51:53 -07:00
Martin Michelsen 8cf0b9f947 add initializer for SpectatorEntry::name_color 2023-10-01 08:55:08 -07:00
Martin Michelsen bbe42b765c fix spectators not seeing each other's names when joining spectator team 2023-10-01 08:45:13 -07:00
Martin Michelsen 507b2fbcac fix disconnect when viewing Ep3 team info with missing player 2023-10-01 07:52:38 -07:00
Martin Michelsen 5fe21b8eec add initializers to ClientConfig structs 2023-09-30 09:40:50 -07:00
Martin Michelsen d488ccd100 fix type in GVM entry struct 2023-09-30 00:45:28 -07:00
Martin Michelsen 403c17b42d show defeat status in tournament team info 2023-09-29 23:36:08 -07:00
Martin Michelsen a0ff0cf8e7 add some reloading TODOs 2023-09-29 19:00:34 -07:00
Martin Michelsen feded3e891 make tournament and battle table matches load faster 2023-09-29 19:00:25 -07:00
Martin Michelsen 74307ea7a2 maybe fix multiplayer tournament matches 2023-09-29 18:20:36 -07:00
Martin Michelsen 45ea21860d fix spectator list in game details display 2023-09-28 23:19:02 -07:00
Martin Michelsen 6a6fb91acb explain overall_time_limit more clearly 2023-09-28 15:44:18 -07:00
Martin Michelsen 8aaadf81ac update notes about before_message 2023-09-28 14:51:16 -07:00
Martin Michelsen 1f34b6bb90 add TODO for nonblocking reloads 2023-09-28 14:51:07 -07:00
Martin Michelsen fbdfdb085a add learnings from Ep3 Trial Edition download quest 2023-09-28 14:50:52 -07:00
Martin Michelsen 5c5da8e10b add converted Ep3 Trial Edition download quest 2023-09-28 14:46:24 -07:00
Martin Michelsen 103e5325a3 fix CAx1B client ID check 2023-09-27 10:51:18 -07:00
Martin Michelsen 02584e4458 add card list HTML generator 2023-09-27 10:00:33 -07:00
Martin Michelsen 263e9114c5 add TODO for detector encryption 2023-09-26 20:11:34 -07:00
Martin Michelsen fed50aec6b fix typo in readme 2023-09-26 20:11:21 -07:00
Martin Michelsen b9057cf562 add DC NTE UDP-off variants 2023-09-26 12:17:53 -07:00
Martin Michelsen 63f6aff4ed add decoder for Ep3 trial download quests 2023-09-26 12:12:41 -07:00
Martin Michelsen a4961ad69d fix DCv1 login with UDP off 2023-09-26 12:10:04 -07:00
Martin Michelsen f0bd2c7aa6 make incorrect password errors let you re-enter your password 2023-09-26 10:47:16 -07:00
Martin Michelsen ac13bf13b2 update readme 2023-09-26 10:46:57 -07:00
Martin Michelsen 98dc2af278 support decompressed card text archives 2023-09-26 10:46:46 -07:00
Martin Michelsen b7ceeb029a fix battle record loading 2023-09-25 22:37:44 -07:00
Martin Michelsen f036f137f7 fix some wording in example config 2023-09-25 22:28:55 -07:00
Martin Michelsen 187bfa1756 fix download quests on proxy server 2023-09-25 22:28:39 -07:00
Martin Michelsen 5e14a8449c add $meseta command 2023-09-25 21:42:36 -07:00
Martin Michelsen 65f8dea0da add $call command 2023-09-25 09:47:56 -07:00
Martin Michelsen 995a05c409 prefix battle record filenames with serial number 2023-09-25 09:26:48 -07:00
Martin Michelsen 885d125fc4 eliminate the concept of temporary licenses 2023-09-25 09:26:48 -07:00
Martin Michelsen 949ad0d260 fix minor isses in battle replays 2023-09-24 23:17:22 -07:00
Martin Michelsen 9272feff8f add json licenses to gitignore 2023-09-24 16:38:12 -07:00
Martin Michelsen 058b040975 implement Episode 3 meseta 2023-09-24 16:29:58 -07:00
Martin Michelsen 8b544830a0 delete unused enum 2023-09-24 16:29:58 -07:00
Martin Michelsen 0c2ecd4ebb save player names along with tournament entries and show them in info window 2023-09-24 16:29:58 -07:00
Martin Michelsen 6b5e672ebb move VMS structure into SaveFileFormats 2023-09-24 16:29:58 -07:00
Martin Michelsen 7f7aaf920b make reload config affect welcome message and information menu 2023-09-24 09:22:49 -07:00
Martin Michelsen 5c48c75fdc fix decode_sjis 2023-09-24 08:57:07 -07:00
Martin Michelsen 2846e73710 rewrite default information menu contents 2023-09-23 22:33:06 -07:00
Martin Michelsen 2ee1891153 fix PC system filename 2023-09-23 18:06:49 -07:00
Martin Michelsen cc70280761 add PC save file formats and encrypt/decrypt functions 2023-09-23 17:08:37 -07:00
Martin Michelsen 85897baaeb ignore client rules in tournament matches 2023-09-23 09:11:22 -07:00
Martin Michelsen 14973f7453 don't allow multiple simultaneous tournaments with the same name 2023-09-23 08:47:17 -07:00
Martin Michelsen fe984a4284 fix pending match state when loading in-progress tournament 2023-09-23 08:46:58 -07:00
Martin Michelsen 99b508a256 allow tournament matches to start at any battle table 2023-09-23 08:18:36 -07:00
Martin Michelsen 6e522459ae add ability to specify separate DEF dice range 2023-09-22 22:00:17 -07:00
Martin Michelsen be0e616df7 allow players to register for another tournament if they lose their current tournament 2023-09-22 18:11:05 -07:00
Martin Michelsen 1bf3e6869d fix dice option in create-tournament 2023-09-22 17:50:10 -07:00
Martin Michelsen 0df670893f fix log settings not applying to command data 2023-09-22 17:45:31 -07:00
Martin Michelsen de9d52b352 make card drop rate explanation easier to read 2023-09-22 10:58:19 -07:00
Martin Michelsen 3542200379 improve random loading sounds code 2023-09-21 22:43:12 -07:00
Martin Michelsen 82c877f55d document how card transformation works 2023-09-21 21:18:13 -07:00
Martin Michelsen 19499bf23d update notes about D3 rank 2023-09-21 21:14:52 -07:00
Martin Michelsen 4cf1895f4d enforce ep3 lobby restrictions during Meet User 2023-09-21 18:13:01 -07:00
Martin Michelsen aa25f7e79a make compression tests not fail when run in parallel 2023-09-21 17:08:18 -07:00
Martin Michelsen 93906f8ff3 default-clear all converted_endian parrays to zero 2023-09-21 17:07:58 -07:00
Martin Michelsen 931258e8ac fix uninitialized memory in E8 command 2023-09-21 10:39:02 -07:00
Martin Michelsen 5b907d4413 add Ep3 battle test with spectator 2023-09-21 10:22:00 -07:00
Martin Michelsen a8c7da70e0 fix patch ping event case 2023-09-20 23:11:24 -07:00
Martin Michelsen 3682c082ea fix some struct notes 2023-09-20 18:27:43 -07:00
Martin Michelsen de110a1c88 don't repeat ping and idle timeout events 2023-09-20 08:28:05 -07:00
Martin Michelsen 7e4664ea25 handle ping exceptions 2023-09-20 08:03:19 -07:00
Martin Michelsen 3d0a842496 don't allow *this to be destroyed too early on idle timeout 2023-09-19 23:40:31 -07:00
Martin Michelsen 64bbeb0f70 add $stat for live ep3 battle stats 2023-09-19 22:37:19 -07:00
Martin Michelsen 2eb429436f add parent pointers to eliminate code duplication in many places 2023-09-19 22:15:41 -07:00
Martin Michelsen adad870aff annotate more fields in PlayerConfig 2023-09-19 10:05:56 -07:00
Martin Michelsen ecaea3fe49 extend full dressing room codes 2023-09-19 10:05:56 -07:00
Martin Michelsen 4f16243e41 fix incorrect type in Ep3 PlayerConfig 2023-09-19 09:16:10 -07:00
Martin Michelsen 7706adc7cb document more fields in Ep3PlayerConfig 2023-09-19 00:11:13 -07:00
Martin Michelsen 3cf39887e8 add offset comments in CameraSpec 2023-09-18 13:39:26 -07:00
Martin Michelsen c65b012ea5 add full dressing room codes 2023-09-18 13:29:14 -07:00
Martin Michelsen ed97279436 add comment about snapshot format 2023-09-18 11:06:09 -07:00
Martin Michelsen 9cb9e8064a make Rules debug string show open cases 2023-09-18 10:22:22 -07:00
Martin Michelsen 80b9af46db write a bit more about AI params 2023-09-18 09:37:09 -07:00
Martin Michelsen 83ecbf77ab add information about Ep3 camera and AI data in map files 2023-09-18 00:20:49 -07:00
Martin Michelsen 8952a4d56b don't allow toggling spectator flag in a spectator team 2023-09-17 20:47:58 -07:00
Martin Michelsen 4575adea11 fix chat message forwarding to spectators 2023-09-17 19:20:18 -07:00
Martin Michelsen 9e8a59798c explain more about how card drops work 2023-09-17 16:27:34 -07:00
Martin Michelsen bb92feb9a5 add disable chat filter AR code 2023-09-17 16:22:51 -07:00
Martin Michelsen 72155939d5 don't send spectator join commands if battle is already finished 2023-09-17 12:49:18 -07:00
Martin Michelsen 3c1c63f24e make spectator joining more robust 2023-09-17 12:36:05 -07:00
Martin Michelsen ef7f5fb798 maybe fix spectator team map loading 2023-09-17 12:02:58 -07:00
Martin Michelsen 49be421ff4 add wchat shell command 2023-09-17 11:43:35 -07:00
Martin Michelsen e27bce9313 fix spectator count when joining an existing spectator team 2023-09-17 11:43:23 -07:00
Martin Michelsen fbe621173f use correct credentials in test 2023-09-17 10:36:49 -07:00
Martin Michelsen ae518eaaf6 fix accidentally-switched tables in drop rate explanation 2023-09-17 10:33:44 -07:00
Martin Michelsen e858b79b33 use latest official card definitions file 2023-09-17 10:26:13 -07:00
Martin Michelsen 04c34e1b22 update TODO.md 2023-09-17 10:04:46 -07:00
Martin Michelsen f799cfe87c add TODO.md 2023-09-16 11:51:00 -07:00
Martin Michelsen 24f3ddef40 add puyo_j 2023-09-16 11:51:00 -07:00
Martin Michelsen 30e1aacaf0 fix tournament commands on Ep3 trial edition 2023-09-16 10:22:25 -07:00
Martin Michelsen 4741091b9f fix client crash when creating spectator team 2023-09-16 10:00:36 -07:00
Martin Michelsen 4ddc4fce1d add shuffle and resize options in tournaments 2023-09-16 10:00:36 -07:00
Martin Michelsen 1d45c18ce8 keep tournament state consistent on clients 2023-09-16 10:00:36 -07:00
Martin Michelsen 5caa21bccb disband spectator teams when primary players go to results screen 2023-09-15 20:27:23 -07:00
Martin Michelsen 9cef4a14f8 add a tournament option to disable COM entries 2023-09-15 20:27:23 -07:00
Martin Michelsen 27081bd3da add comments for better searchability 2023-09-13 18:22:57 -07:00
Martin Michelsen 2115f188d1 minor formatting 2023-09-13 12:24:18 -07:00
Martin Michelsen bf55da55bf fix segfault on insufficient level for game creation 2023-09-12 20:33:39 -07:00
Martin Michelsen 550b62dec9 add cheat command to remove an FC in an Ep3 battle 2023-09-12 19:49:38 -07:00
Martin Michelsen 215c181798 add fallback map loading in BB solo mode 2023-09-12 19:49:38 -07:00
Martin Michelsen 2f663ef2b3 add missing BB maps 2023-09-12 19:49:38 -07:00
Martin Michelsen b07748d07f fix Madness not skipping HUNTERS_SC with items equipped 2023-09-12 17:54:57 -07:00
Martin Michelsen f708ecc035 strip trailing whitespace from card text 2023-09-12 17:31:11 -07:00
Martin Michelsen fb52047e7c revert card definitions file again 2023-09-12 14:59:27 -07:00
Martin Michelsen a8d09363f1 add Ep3 flag to allow interference for human teams 2023-09-12 14:53:16 -07:00
Martin Michelsen 15566f7143 fix chained action card conditions not applying 2023-09-12 10:30:33 -07:00
Martin Michelsen 7657d4f2fc update Ep3 BB command format 2023-09-11 17:24:13 -07:00
Martin Michelsen d843a54245 update comment on map_category 2023-09-11 12:33:25 -07:00
Martin Michelsen df013784fc document map_category field in MapDefinition 2023-09-11 11:24:57 -07:00
Martin Michelsen 1f6f76a6dc fix uninitialized value used in attack env stats computation 2023-09-10 22:06:24 -07:00
Martin Michelsen b885442a4b remove client ID checks during registration phase 2023-09-10 14:56:06 -07:00
Martin Michelsen e64fa10a58 fix Raspberry Pi build 2023-09-10 13:52:49 -07:00
Martin Michelsen 66ca3ed6dd update to-do list 2023-09-10 10:50:53 -07:00
Martin Michelsen 013e099f50 update to-do list 2023-09-10 10:25:04 -07:00
Martin Michelsen debc920997 update Dolphin connection instructions 2023-09-10 09:48:46 -07:00
Martin Michelsen 80f79aa13c fix name behavior on BB/GC lobby interactions 2023-09-10 09:27:56 -07:00
Martin Michelsen 7585eaeae5 name some unknown Ep3 enum values 2023-09-10 09:27:28 -07:00
Martin Michelsen 52ed062ed9 add comment on AssistFlag enum 2023-09-09 20:06:01 -07:00
Martin Michelsen 753b89c78d give names to assist_flags 2023-09-09 19:21:33 -07:00
Martin Michelsen fa48b58773 fix invalid array access 2023-09-09 17:55:28 -07:00
Martin Michelsen aa48dd5e15 delete hard_reset_flag 2023-09-09 17:55:06 -07:00
Martin Michelsen 0863c4f27c fix CPU replacement on player disconnect 2023-09-09 12:50:41 -07:00
Martin Michelsen f12fdaf165 bounds-check input client IDs 2023-09-09 12:48:12 -07:00
Martin Michelsen e890bfad63 fix multiple array index bugs 2023-09-09 12:48:08 -07:00
Martin Michelsen f8198580dd merge Ep3 ServerBase and Server into one class 2023-09-09 10:13:51 -07:00
Martin Michelsen a40d1ad851 add reload config shell command 2023-09-09 00:06:30 -07:00
Martin Michelsen 901b2b78d2 add missing include 2023-09-08 23:58:10 -07:00
Martin Michelsen 24439a9dc3 re-record Episode 3 battle test 2023-09-08 23:54:02 -07:00
Martin Michelsen 4498fe1232 rename ep3 game command handlers 2023-09-08 23:35:16 -07:00
Martin Michelsen b9fc225786 add Ep3 $inftime command 2023-09-08 23:32:47 -07:00
Martin Michelsen c430340c9d hide Ep3 maps that don't have enough player slots for the game 2023-09-08 23:32:47 -07:00
Martin Michelsen 9c3f764cd9 fix all-players range gathering bug 2023-09-08 23:32:47 -07:00
Martin Michelsen 9dcdece1f9 fix UNKNOWN_07 and NOT_SC condition codes 2023-09-08 20:09:49 -07:00
Martin Michelsen d663472aae delete some TODO items which are now done 2023-09-08 11:30:21 -07:00
Martin Michelsen 245ebd92c6 don't send Ep3 lobby banners again after ending a proxy session 2023-09-08 10:50:16 -07:00
Martin Michelsen c1ed1afa5b add more info about Ep3 lobby banners 2023-09-08 09:38:00 -07:00
Martin Michelsen 39e491eb1e split field in 6x70 command 2023-09-07 23:54:15 -07:00
Martin Michelsen 15b9c05004 add some more AR codes 2023-09-07 22:34:18 -07:00
Martin Michelsen cfa4e3b8b0 implement Episode 3 lobby banners 2023-09-07 22:34:07 -07:00
Martin Michelsen bd6102a894 add another loading screen AR code 2023-09-06 23:55:26 -07:00
Martin Michelsen c45b4cced7 fix rules not serializing properly in tournament state 2023-09-06 23:55:06 -07:00
Martin Michelsen 548aca8cc0 fix Ep3 card auction 2023-09-06 16:39:32 -07:00
Martin Michelsen 75fab887e1 make tournament state parsing more robust 2023-09-06 09:46:33 -07:00
Martin Michelsen d2a589d968 update MapDefinition comments 2023-09-06 09:46:16 -07:00
Martin Michelsen 71d3d4e27c add offline maps and quests 2023-09-05 23:21:36 -07:00
Martin Michelsen 74ff094012 Revert "increase read timeout during log replay"
This reverts commit bbab6968d1.
2023-09-05 23:18:03 -07:00
Martin Michelsen bbab6968d1 increase read timeout during log replay 2023-09-05 23:07:18 -07:00
Martin Michelsen af781dbc09 re-record Episode 3 battle test 2023-09-05 23:00:30 -07:00
Martin Michelsen f771643880 fix rounding in division expressions 2023-09-05 23:00:30 -07:00
Martin Michelsen 2b2d8dfb3d make Episode 3 EX results configurable 2023-09-05 23:00:30 -07:00
Martin Michelsen 66f584d475 fix condition apply using incorrect criterion for non-item checks 2023-09-05 23:00:30 -07:00
Martin Michelsen 3b69d3484d bring back the $ln command 2023-09-05 23:00:30 -07:00
Matt 013a19885f Update Tournament Explainer
Explains which 4-player battle table to use more clearly
2023-09-04 17:58:52 -07:00
Matt 3a7277bc5d Update README for tournament table location
To make it less ambiguous
2023-09-04 17:58:52 -07:00
Martin Michelsen 9f943cf5d8 add $surrender command 2023-09-03 22:44:36 -07:00
Martin Michelsen c3edb93248 fix tests after name marker update 2023-09-03 22:08:07 -07:00
Martin Michelsen 5712ff3e3e fix long name truncation on non-BB versions 2023-09-03 21:33:00 -07:00
Martin Michelsen 2cb2dd3b24 fix creature summon are computation on left/right-oriented maps 2023-09-03 21:25:33 -07:00
Martin Michelsen da431cc174 add details about Ep3 rank text 2023-09-02 10:10:14 -07:00
Martin Michelsen 7c6a1e730e fix fields in Ep3 card definitions footer struct 2023-09-02 08:46:19 -07:00
Martin Michelsen 85dbea215b document Ep3 assist AI parameters 2023-09-01 20:37:54 -07:00
Martin Michelsen 8449a6d21a describe how ep3 card drop rates actually work 2023-09-01 11:08:23 -07:00
Martin Michelsen 2eda283f8f revert accidentally-committed card defs file 2023-08-31 14:03:05 -07:00
Martin Michelsen ba7951a9f4 make CardAuctionPool name matching more lenient 2023-08-31 14:00:02 -07:00
Martin Michelsen e566a247e4 fix card names in example config auction pool 2023-08-31 13:36:05 -07:00
Martin Michelsen 5b038364a1 be more aggressive when reducing size of card defs file 2023-08-31 13:35:02 -07:00
Martin Michelsen ee7c574fdf fix meseta transaction command 2023-08-31 09:37:12 -07:00
1930 changed files with 133093 additions and 53081 deletions
+2 -2
View File
@@ -15,10 +15,10 @@ Testing
# Files modified by the user and/or server that don't have defaults
system/config.json
system/ep3/battle-records/*.mzrd
system/ep3/tournament-state.json
system/ep3/maps-free/*.bind
system/ep3/maps-quest/*.bind
system/licenses.nsi
system/licenses/*.json
system/players/player_*
system/players/account_*
system/players/bank_*
+17 -22
View File
@@ -40,6 +40,7 @@ find_package(resource_file QUIET)
# Executable definition
add_executable(newserv
src/AFSArchive.cc
src/BattleParamsIndex.cc
src/BMLArchive.cc
src/CatSession.cc
@@ -48,6 +49,7 @@ add_executable(newserv
src/Client.cc
src/CommonItemSet.cc
src/Compression.cc
src/DCSerialNumbers.cc
src/DNSServer.cc
src/EnemyType.cc
src/Episode3/AssistServer.cc
@@ -65,6 +67,7 @@ add_executable(newserv
src/FileContentsCache.cc
src/FunctionCompiler.cc
src/GSLArchive.cc
src/GVMEncoder.cc
src/IPFrameInfo.cc
src/IPStackSimulator.cc
src/ItemCreator.cc
@@ -81,7 +84,7 @@ add_executable(newserv
src/NetworkAddresses.cc
src/PatchFileIndex.cc
src/Player.cc
src/Product.cc
src/PlayerSubordinates.cc
src/ProxyCommands.cc
src/ProxyServer.cc
src/PSOEncryption.cc
@@ -101,7 +104,9 @@ add_executable(newserv
src/Shell.cc
src/StaticGameData.cc
src/Text.cc
src/TextArchive.cc
src/Version.cc
src/WordSelectTable.cc
)
target_include_directories(newserv PUBLIC ${LIBEVENT_INCLUDE_DIR})
target_link_libraries(newserv phosg ${LIBEVENT_LIBRARIES} pthread)
@@ -120,33 +125,23 @@ endif()
enable_testing()
file(GLOB TestCases ${CMAKE_SOURCE_DIR}/tests/*.test.txt)
file(GLOB LogTestCases ${CMAKE_SOURCE_DIR}/tests/*.test.txt)
foreach(TestCase IN ITEMS ${TestCases})
foreach(LogTestCase IN ITEMS ${LogTestCases})
add_test(
NAME ${TestCase}
NAME ${LogTestCase}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_BINARY_DIR}/newserv replay-log ${TestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json --require-password=11111111 --require-access-key=111111111111)
COMMAND ${CMAKE_BINARY_DIR}/newserv replay-log ${LogTestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json --require-basic-credentials)
endforeach()
add_test(
NAME "compression-prs"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_SOURCE_DIR}/tests/test-compression.sh prs ${CMAKE_BINARY_DIR}/newserv)
add_test(
NAME "compression-bc0"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_SOURCE_DIR}/tests/test-compression.sh bc0 ${CMAKE_BINARY_DIR}/newserv)
file(GLOB ScriptTestCases ${CMAKE_SOURCE_DIR}/tests/*.test.sh)
add_test(
NAME "decode-vms"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_SOURCE_DIR}/tests/test-decode-vms.sh ${CMAKE_BINARY_DIR}/newserv)
add_test(
NAME "decode-gci"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_SOURCE_DIR}/tests/test-decode-gci.sh ${CMAKE_BINARY_DIR}/newserv)
foreach(ScriptTestCase IN ITEMS ${ScriptTestCases})
add_test(
NAME ${ScriptTestCase}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${ScriptTestCase} ${CMAKE_BINARY_DIR}/newserv)
endforeach()
# Installation configuration
+72 -73
View File
@@ -1,6 +1,6 @@
# newserv <img align="right" src="s-newserv.png" />
newserv is a game server and proxy for Phantasy Star Online (PSO).
newserv is a game server, proxy, and reverse-engineering tool 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, which was originally created by Sega.
@@ -19,6 +19,7 @@ This project includes code that was reverse-engineered by the community in ages
* How to connect
* Connecting local clients
* [PSO DC](#pso-dc)
* [PSO DC on Flycast](#pso-dc-on-flycast)
* [PSO PC](#pso-pc)
* [PSO GC on a real GameCube](#pso-gc-on-a-real-gamecube)
* [PSO GC on Dolphin](#pso-gc-on-dolphin)
@@ -49,58 +50,32 @@ newserv is many things - a server, a proxy, an encryption and decryption tool, a
With that said, I offer no guarantees on how or when this project will advance. 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. If you feel like contributing to newserv yourself, pull requests are welcome as well.
Current known issues / missing features / things to do:
- Implement the rest of PSOBB. Major areas of work:
- Find any remaining mismatches in enemy IDs / experience
- Sale prices for non-rare weapons with specials are computed incorrectly when buying/selling at shops
- Replace enemy list, game episode, etc. with quest data when loading a quest
- Implement trade window
- Fix some edge cases on the BB proxy server (e.g. make sure Change Ship does the right thing, which is not the same as what it should do on other versions).
- There is a function that encodes QST files, but there's no corresponding CLI option.
- Figure out what controls BML file data segment alignment.
- Extension data in inventories is not handled properly.
- PSOX is not tested at all.
- 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 and overflow lobbies.
- Enforce client-side size limits (e.g. for 60/62 commands) on the server side as well. (For 60/62 specifically, perhaps transform them to 6C/6D if needed.)
- Encapsulate BB server-side random state and make replays deterministic.
- Implement character and inventory replacement for battle and challenge modes.
- Implement the C5 (battle/challenge records) command.
- Implement choice search.
- Episode 3 bugs
- Fix behavior when joining a spectator team after the beginning of a battle.
- Disconnecting during a match turns you into a COM if there are other humans in the match, even if the match is part of a tournament. This may be incorrect behavior for tournaments.
- Disconnecting during a tournament when there are no other humans in the match simply cancels the match (so it can be replayed) instead of forfeiting, which is almost certainly incorrect behavior. (Then again, no one likes losing tournaments to COMs...)
- Tournament deck restrictions aren't enforced when populating COMs at tournament start time. This can cause weird behavior if, for example, a COM deck contains assist cards and the tournament rules forbid them.
- There is a rare failure mode during battles that causes one of the clients to be disconnected.
- Code style
- Add default values in all command structures (like we use for Episode 3 battle commands).
See TODO.md for a list of known issues and future work.
## Compatibility
newserv supports several versions of PSO. Specifically:
| Version | Login | Lobbies | Games | Proxy |
|----------------|--------------|--------------|--------------|--------------|
| DC Trial | Yes (4) | Yes (4) | Yes (4) | No |
| DC Prototype | Yes (4) | Yes (4) | Yes (4) | No |
| DC V1 | Yes (1) | Yes | Yes | Yes |
| DC V2 | Yes (1) | Yes | Yes | Yes |
| DC Trial | Yes (3) | Yes (3) | Yes (3) | No |
| DC Prototype | Yes (3) | Yes (3) | Yes (3) | No |
| DC V1 | Yes | Yes | Yes | Yes |
| DC V2 | Yes | Yes | Yes | Yes |
| PC | Yes | Yes | Yes | Yes |
| GC Ep1&2 Trial | Untested (2) | Untested (2) | Untested (2) | Untested (2) |
| GC Ep1&2 Trial | Untested (1) | Untested (1) | Untested (1) | Untested (1) |
| GC Ep1&2 | Yes | Yes | Yes | Yes |
| GC Ep1&2 Plus | Yes | Yes | Yes | Yes |
| GC Ep3 Trial | Yes | Yes | Partial (5) | Yes |
| GC Ep3 Trial | Yes | Yes | Partial (4) | Yes |
| GC Ep3 | Yes | Yes | Yes | Yes |
| XBOX Ep1&2 | Untested (2) | Untested (2) | Untested (2) | Untested (2) |
| BB (vanilla) | Yes | Yes | Yes (3) | Yes |
| BB (Tethealla) | Yes | Yes | Yes (3) | Yes |
| XBOX Ep1&2 | Untested (1) | Untested (1) | Untested (1) | Untested (1) |
| BB (vanilla) | Yes | Yes | Yes (2) | Yes |
| BB (Tethealla) | Yes | Yes | Yes (2) | Yes |
*Notes:*
1. *DC support has only been tested with the US versions of PSO DC. Other versions probably don't work, but will be easy to add support for. Please submit a GitHub issue if you have a non-US DC version, and can provide a log from a connection attempt.*
2. *newserv's implementations of these versions are based on disassembly of the client executables and have never been tested.*
3. *BB games are mostly playable, but there are still some unimplemented features (for example, some quests that use rare commands may not work). Please submit a GitHub issue if you find something that doesn't work.*
4. *Support for PSO Dreamcast Trial Edition and the December 2000 prototype is somewhat incomplete and probably never will be complete. These versions are rather unstable and seem to crash often, but it's not obvious whether it's because they're prototypes or because newserv sends data they can't handle.*
5. *Creating a game works and battle setup behaves mostly normally, but starting a battle doesn't work.*
1. *newserv's implementations of these versions are based on disassembly of the client executables and have never been tested.*
2. *BB games are mostly playable, but there are still some unimplemented features (for example, some quests that use rare commands may not work). Please submit a GitHub issue if you find something that doesn't work.*
3. *Support for PSO Dreamcast Trial Edition and the December 2000 prototype is somewhat incomplete and probably never will be complete. These versions are rather unstable and seem to crash often, but it's not obvious whether it's because they're prototypes or because newserv sends data they can't handle.*
4. *Creating a game works and battle setup behaves mostly normally, but starting a battle doesn't work.*
## Setup
@@ -132,13 +107,16 @@ To use newserv in other ways (e.g. for translating data), see the end of this do
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 "save files" option, just put them in that directory and name them appropriately.
Standard quest files should be named like `q###-CATEGORY-VERSION.EXT`, battle quests should be named like `b###-VERSION.EXT`, challenge quests should be named like `c###-VERSION.EXT` for Episode 1 or `d###-VERSION.EXT` for Episode 2, and Episode 3 download quests should be named like `e###-gc3.EXT`. The fields in each filename are:
Standard quest files should be named like `q###-CATEGORY-VERSION-LANGUAGE.EXT`, battle quests should be named like `b###-VERSION-LANGUAGE.EXT`, challenge quests should be named like `c###-VERSION-LANGUAGE.EXT` for Episode 1 or `d###-VERSION-LANGUAGE.EXT` for Episode 2. The fields in each filename are:
- `###`: quest number (this doesn't really matter; it should just be unique across the PSO version)
- `CATEGORY`: ret = Retrieval, ext = Extermination, evt = Events, shp = Shops, vr = VR, twr = Tower, gv1/gv2/gv4 = Government (BB only), dl = Download (these don't appear during online play), 1p = Solo (BB only)
- `VERSION`: dn = Dreamcast NTE, d1 = Dreamcast v1, dc = Dreamcast v2, pc = PC, gcn = GameCube Trial Edition, gc = GameCube Episodes 1 & 2, gc3 = Episode 3, xb = Xbox, bb = Blue Burst
- `VERSION`: dn = Dreamcast NTE, d1 = Dreamcast v1, dc = Dreamcast v2, pc = PC, gcn = GameCube Trial Edition, gc = GameCube Episodes 1 & 2, gc3 = Episode 3 (see below), xb = Xbox, bb = Blue Burst
- `LANGUAGE`: j = Japanese, e = English, g = German, f = French, s = Spanish
- `EXT`: file extension (see table below)
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-ret-gc.bin` and `q058-ret-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, and it puts them in the Retrieval category because the filenames contain `-ret`.
On .dat files, the `LANGUAGE` token may be omitted. If it's present, then that .dat file will only be used for that version of the quest; if omitted, then that .dat file will be used for all versions of the quest.
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-ret-gc-e.bin` and `q058-ret-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the filenames contain `-ret`.
The type identifiers (`b`, `c`, `d`, `e`, or `q`) and categories are configurable. See QuestCategories in config.example.json for more information on how to make new categories or edit the existing categories.
@@ -156,6 +134,7 @@ There are multiple PSO quest formats out there; newserv supports all of them. It
| GCI (with key) | .bin.gci and .dat.gci | Yes | decode-gci |
| GCI (no key) | .bin.gci and .dat.gci | Decode (3) | decode-gci (3) |
| GCI (Ep3) | .bin.gci or .mnm.gci | Yes | decode-gci |
| GCI (Ep3 Trial) | .bin.gci or .mnm.gci | Decode (3) | decode-gci (3) |
| DLQ | .bin.dlq and .dat.dlq | Yes | decode-dlq |
| DLQ (Ep3) | .bin.dlq or .mnm.dlq | Yes | decode-dlq |
| QST (online) | .qst | Yes | decode-qst |
@@ -165,9 +144,9 @@ There are multiple PSO quest formats out there; newserv supports all of them. It
1. *This is the default format. You can convert these to uncompressed format by running `newserv decompress-prs FILENAME.bin FILENAME.bind` (and similarly for .dat -> .datd)*
2. *Similar to (1), to compress an uncompressed quest file: `newserv compress-prs FILENAME.bind FILENAME.bin` (and likewise for .datd -> .dat)*
3. *Use the decode action to convert these quests to .bin/.dat format before putting them into the server's quests directory. If you know the encryption seed (serial number), pass it in as a hex string with the `--seed=` option. If you don't know the encryption seed, newserv will find it for you, which will likely take a long time.*
4. *Episode 3 online quests don't go in the system/quests directory; they instead go in the system/ep3/maps-free or system/ep3/maps-quest directories. If you want an Episode 3 quest to be available for both online play and for downloading, the file must exist in both system/quests and in one of the map directories in system/ep3.*
4. *Episode 3 quests don't go in the system/quests directory. See the Episode 3 section below.*
Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/). These files can be encoded in any of the formats described above, except .qst. There are no encrypted Episode 3 GCI formats because the game doesn't encrypt quests saved to the memory card, unlike Episodes 1&2.
Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/). These files can be encoded in any of the formats described above, except .qst.
When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats.
@@ -177,29 +156,37 @@ All quests, including those originally in GCI or DLQ format, are treated as onli
### Episode 3 features
The following Episode 3 features work well:
newserv supports many features unique to Episode 3:
* CARD battles. Not every combination of abilities has been tested yet, so if you find a feature or card ability that doesn't work like it's supposed to, please make a GitHub issue and describe the situation (the attacking card(s), defending card(s), and ability or condition that didn't work).
* Tournaments. (But they don't work like Sega's tournaments did - see below)
* Spectator teams.
* Tournaments. (But they work differently than Sega's tournaments did - see below)
* Downloading quests.
* Trading cards.
* Participating in card auctions. (The auction contents must be configured in config.json.)
* Decorations in lobbies. Currently only images are supported; the game also supports loading custom 3D models in lobbies, but newserv does not implement this (yet).
The following Episode 3 features are implemented, but are only partially tested:
* Spectator teams. There is a known issue that prevents viewing battles unless you're in the spectator team when the battle begins, and spectating clients sometimes crash for an unknown reason.
* Battle replays also sometimes cause the client to crash during the replay. Using the $playrec command is therefore not recommended.
#### Battle records
Tournaments work differently than they did on Sega's servers. Tournaments can be created with the `create-tournament` shell command, which enables players to register for them. (Use `help` to see all the arguments - there are many!) The `start-tournament` shell command starts the tournament (and prevents further registrations), but this doesn't schedule any matches. Instead, players who are ready to play their next match can all stand at the rightmost 4-player battle table in the same CARD lobby, and the tournament match will start automatically.
After playing a battle, you can save the record of the battle with the $saverec command. You can then replay the battle later by using the $playrec command in a lobby - this will create a spectator team and play the recording of the battle as if it were happening in realtime. Note that there is a bug in older versions of Dolphin that seems to be frequently triggered when playing battle records, which causes the emulator to crash with the message `QObject::~QObject: Timers cannot be stopped from another thread`. To avoid this, use the latest version of Dolphin.
#### Tournaments
Tournaments work differently than they did on Sega's servers. Tournaments can be created with the `create-tournament` shell command, which enables players to register for them. (Use `help` to see all the arguments - there are many!) The `start-tournament` shell command starts the tournament (and prevents further registrations), but this doesn't schedule any matches. Instead, players who are ready to play their next match can all stand at the 4-player battle table near the lobby warp in the same CARD lobby, and the tournament match will start automatically.
These tournament semantics mean that there can be multiple matches in the same tournament in play simultaneously, and not all matches in a round must be complete before the next round can begin - only the matches preceding each individual match must be complete for that match to be playable.
Because newserv gives all players 1000000 meseta, there is no reward for winning a tournament. This may change in the future.
The Meseta rewards for winning tournament matches can be configured in config.json.
#### Episode 3 files
Episode 3 state and game data is stored in the system/ep3 directory. The files in there are:
* card-definitions.mnr: Compressed card definition list, sent to Episode 3 clients at connect time. Card stats and abilities can be changed by editing this file.
* card-definitions.mnrd: Decompressed version of the above. If present, newserv will use this instead of the compressed version, since this is easier to edit.
* card-text.mnr: Compressed card text archive. Generally only used for debugging.
* card-text.mnrd: Decompressed card text archive; same format as TextCardE.bin. Generally only used for debugging.
* com-decks.json: COM decks used in tournaments. The default decks in this file come from logs from Sega's servers, so the file doesn't include every COM deck Sega ever made - the rest are probably lost to time.
* maps-free/ and maps-quest/: Online free battle and quest maps (.mnm/.bin/.mnmd/.bind files). Free battle and quest files have exactly the same format; the only difference between the files in these directories is which section of the menu they will appear in on the client.
* maps/: Online free battle and quest maps (.mnm/.bin/.mnmd/.bind files). newserv comes with all the original online and offline maps, including Story Mode quests. If you don't want the offline maps and quests to be playable online, delete the .bind files system/ep3/maps.
* maps-download/: Download maps and quests (.mnm/.bin/.mnmd/.bind files). Files in this directory have the same format as those in the maps/ directory, but should be named like `e###-gc3-LANGUAGE.EXT` (similar to how non-Episode 3 quests are named in the system/quests/ directory). If you want a map to be available for online play and for downloading, the file must exist in both maps/ and maps-download/ (a symbolic link is acceptable).
* tournament-state.json: State of all active tournaments. This file is automatically written when any tournament changes state for any reason (e.g. a tournament is created/started/deleted or a match is resolved).
There is no public editor for Episode 3 maps and quests, but the format is described fairly thoroughly in src/Episode3/DataIndexes.hh (see the MapDefinition structure). You'll need to use `newserv decompress-prs ...` to decompress .bin or .mnm files before editing them, but you don't need to compress the files again to use them - just put the .bind or .mnmd file in the maps directory and newserv will make it available.
@@ -276,18 +263,18 @@ Some chat commands (see below) have the same basic function on the proxy server
### 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.)
newserv supports a variety of commands players can use by chatting in-game. 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 instead (remote Guild Card number, client ID, etc.).
* `$what` (game server only): Shows the type, name, and stats of the nearest item on the ground.
* `$matcount` (game server only): Shows how many of each type of material you've used.
* Debugging commands
* `$debug` (game server only): Enable or disable debug. You need the DEBUG permission in your user license to use this command. When debug is enabled, you'll see in-game messages from the server when you take certain actions. You'll also be placed into the highest available slot in lobbies and games instead of the lowest, which is useful for finding commands for which newserv doesn't handle client IDs properly. This setting also disables certain safeguards and allows you to do some things that might crash your client.
* `$call <function-id>`: Call a quest function on your client.
* `$gc` (game server only): Send your own Guild Card to yourself.
* `$persist` (game server only): Enable or disable persistence for the current lobby or game. This determines whether the lobby/game is deleted when the last player leaves. You need the DEBUG permission in your user license to use this command because there are no game state checks when you do this. For example, if you make a game persistent, start a quest, then leave the game, the game can't be joined by anyone but also can't be deleted.
* `$sc <data>`: Send a command to yourself.
@@ -297,6 +284,7 @@ Some commands only work on the game server and not on the proxy server. The chat
* `$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. On the proxy server, this will not work if the remote server controls item drops (e.g. on BB, or on Schtserv with server drops enabled).
* `$rand <seed>`: Sets your override random seed (specified as a 32-bit hex value). This will make any games you create use the given seed for rare enemies. This also makes item drops deterministic in Blue Burst games hosted by newserv. On the proxy server, this command can cause desyncs with other players in the same game, since they will not see the overridden random seed. To remove the override, run `$rand` with no arguments.
* `$ln [name-or-type]`: Sets the lobby number. Visible only to you. This command exists because some non-lobby maps can be loaded as lobbies with invalid lobby numbers. See the "GC lobby types" and "Ep3 lobby types" entries in the information menu for acceptable values here. Note that non-lobby maps do not have a lobby counter, so there's no way to exit the lobby without using either `$ln` again or `$exit`. On the game server, `$ln` reloads the lobby immediately; on the proxy server, it doesn't take effect until you load another lobby yourself (which means you'll like have to use `$exit` to escape). Run this command with no argument to return to the default lobby.
* `$exit`: If you're in a lobby, sends you to the main menu (which ends your proxy session, if you're in one). If you're in a game or spectator team, sends you to the lobby (but does not end your proxy session if you're in one). Does nothing if you're in a non-Episode 3 game and no quest is in progress.
* `$patch <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
@@ -308,9 +296,16 @@ Some commands only work on the game server and not on the proxy server. The chat
* `$maxlevel <level>`: Sets the maximum level for players to join the current game. (This only applies when joining; if a player joins and then levels up past this level during the game, they are not kicked out, but won't be able to rejoin if they leave.)
* `$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.
* `$spec`: Toggles the allow spectators flag. If any players are spectating when this flag is disabled, they will be sent back to the lobby.
* `$saverec <name>`: Save the recording of the last Episode 3 battle.
* `$playrec <name>`: Play a battle recording. This command creates a spectator team and replays the specified battle log within it. There is a known issue which causes spectators to crash in some cases, so use of this command is currently not recommended.
* `$raretable`: Switches between using the client's or the server's drop table. No effect on BB (the server's drop table is always used). The server's rare tables are defined in JSON files in the system/rare-tables directory.
* Episode 3 commands (game server only)
* `$spec`: Toggles the allow spectators flag for Episode 3 games. If any players are spectating when this flag is disabled, they will be sent back to the lobby.
* `$inftime`: Toggles infinite-time mode. Must be used before starting a battle. If infinite-time mode is enabled, the overall and per-phase time limits will be disabled regardless of the values chosen during battle setup. After completing a battle, infinite-time mode is reset to the server's default value (which can be set in Episode3BehaviorFlags in config.json).
* `$defrange <min>-<max>`: Sets the DEF dice range for the next battle. If this is used, the dice range set during battle rules setup will apply only to ATK dice; DEF dice will use this range instead. Assist cards and other dice effects will still apply. Dice exchange also still applies if it is enabled.
* `$stat <what>`: Shows a statistic about your player or team in the current battle. `<what>` can be `duration`, `fcs-destroyed`, `cards-destroyed`, `damage-given`, `damage-taken`, `opp-cards-destroyed`, `own-cards-destroyed`, `move-distance`, `cards-set`, `fcs-set`, `attack-actions-set`, `techs-set`, `assists-set`, `defenses-self`, `defenses-ally`, `cards-drawn`, `max-attack-damage`, `max-combo`, `attacks-given`, `attacks-taken`, `sc-damage`, `damage-defended`, or `rank`.
* `$surrender`: Causes your team to immediately lose the current battle.
* `$saverec <name>`: Saves the recording of the last battle.
* `$playrec <name>`: Plays a battle recording. This command creates a spectator team and replays the specified battle log within it. There is a bug in Dolphin that makes use of this command unstable in emulation (see the "Battle records" section above).
* 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. By default, cheat mode is off in new games but can be enabled; there is an option in config.json that allows you to disable cheat mode entirely, or set it to on by default in new games.
@@ -320,6 +315,7 @@ Some commands only work on the game server and not on the proxy server. The chat
* `$next`: 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.
* `$item <desc>` (or `$i <desc>`): Create an item. `desc` may be a description of the item (e.g. "Hell Saber +5 0/10/25/0/10") or a string of hex data specifying the item code. Item codes are 16 hex bytes; at least 2 bytes must be specified, and all unspecified bytes are zeroes. If you are on the proxy server, you must not be using Blue Burst for this command to work. On the game server, this command works for all versions.
* `$unset <index>`: In an Episode 3 battle, removes one of your set cards from the field. `<index>` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed.
* 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, this applies to all lobbies and games you join, but only you will see the new event - other players will not.
@@ -341,18 +337,23 @@ Some versions of PSO DC will connect to a private server if you just set their D
If you're emulating PSO DC or have a disc image, you can patch the appropriate files within the disc image to make it connect to any address you want. Creating such a patch is also beyond the scope of this document.
Finally, if you're emulating PSO DC, you can modify the loaded executable in memory to make it connect anywhere you want. There is a script included with newserv that can do this for Flycast. The script only works on macOS because it uses memwatch, which is specifically for macOS, but a similar technique could be done manually using scanmem on Linux or Cheat Engine on Windows. (The script is fairly short, and what it does should be easy to understand so you can duplicate its effects with scanmem or Cheat Engine.)
#### PSO DC on Flycast
To use the script, do this:
If you're emulating PSO DC, all versions will connect to newserv by setting the following options in Flycast's `emu.cfg` file under `[network]`:
- DNS = Your newserv's server address (newserv's DNS server must be running on port 53)
- EmulateBBA = no (while some versions support the BBA, some do not, and all versions support the modem)
- Enable = yes
Once set up, the EU and US versions will work without any extra set up (other than the HL Check Disable code for USv2), while the JP versions require HL Check Disable codes to be running, and an e-mail account set up. The easiest way to set up an e-mail account is through PlanetWeb's Internet Browser for Dreamcast.
If the server is running on the same machine as Flycast, this might not work, even if you point Flycast's DNS queries at your local IP address (instead of 127.0.0.1). In this case, you can modify the loaded executable in memory to make it connect anywhere you want. There is a script included with newserv that can do this on macOS; a similar technique could be done manually using scanmem on Linux or Cheat Engine on Windows. To use the script, do this:
1. Build and install memwatch (https://github.com/fuzziqersoftware/memwatch).
2. Start Flycast and run PSO. (You must run the script below after PSO is loaded - it won't work if you run it before loading the game.)
2. Start Flycast and run PSO. (You must start PSO before running the script; it won't work if you run the script before loading the game.)
3. Run `sudo patch_flycast_memory.py <original-destination>`. Replace `<original-destination>` with the hostname that PSO wants to connect to (you can find this out by using Wireshark and looking for DNS queries). The script may take up to a minute; you can continue using Flycast while it runs, but don't start an online game until the script is done.
4. Run newserv and start an online game in PSO.
If you use this method, you'll have to run the script every time you start PSO in Flycast, but you won't have to run it again if you start another online game without restarting emulation.
Finally, the script takes an optional second argument that allows you to redirect the connection elsewhere (instead of the local machine). This allows you to connect directly to remote servers if desired.
#### PSO PC
The version of PSO PC I have has the server addresses starting at offset 0x29CB34 in pso.exe. Using a hex editor, change those to "localhost" (without quotes) if you just want to connect to a locally-running newserv instance. Alternatively, you can add an entry to the Windows hosts file (C:\Windows\System32\drivers\etc\hosts) to redirect the connection to 127.0.0.1 (localhost) or any other IP address.
@@ -365,17 +366,14 @@ If you have PSO Plus or Episode III, it won't want to connect to a server on the
#### PSO GC on Dolphin
If you have BBA support via a tap interface or via the HLE/built-in interface, you may be able to just set the DNS server address (as you would on a real GameCube, above) and it may work.
If you're using the HLE BBA type, set the BBA's DNS server address to newserv's IP address and it should work. (If newserv is on the same machine as Dolphin, try your local IP address or 127.0.0.1.) In PSO, use the example values below in PSO's network configuration.
If you're using a version of Dolphin with tapserver support, you can make it connect to a newserv instance running on the same machine via the tapserver interface. You do not need to install or run tapserver, and this works for all PSO versions without any of the dual-interface trickery described above. To do this:
If you're using the TAP BBA type, you'll have to set PSO's network settings appropriately for your tap interface. Set the DNS server address in PSO's network settings to newserv's IP address.
If you're using a version of Dolphin with tapserver support, you can make it connect to a newserv instance running on the same machine via the tapserver interface. You do not need to install or run tapserver. To do this:
1. Set Dolphin's BBA type to tapserver (Config -> GameCube -> SP1).
2. Enable newserv's IP stack simulator according to the comments in config.json and start newserv.
3. 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`
- Default gateway: `10.0.1.1`
- DNS server address 1: `10.0.1.1`
- Leave everything else blank
3. In PSO's network settings, enable DHCP ("Automatically obtain an IP address"), set DNS server address to "Automatic", and leave DHCP Hostname as "Not set". Leave the proxy server settings blank.
4. Start an online game.
### Connecting external clients
@@ -400,6 +398,7 @@ newserv has many CLI options, which can be used to access functionality other th
* Decode Shift-JIS text to UTF-16 (`decode-sjis`)
* Convert quests in .gci, .vms, .dlq, or .qst format to .bin/.dat format (`decode-gci`, `decode-vms`, `decode-dlq`, `decode-qst`)
* Convert quests in .bin/.dat to .qst format (`encode-qst`)
* Convert text archives (e.g. TextEnglish.pr2) to JSON and vice versa (`decode-text-archive`, `encode-text-archive`)
* Disassemble quest scripts (`disassemble-quest-script`)
* Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`)
* Convert item data to a human-readable description, or vice versa (`describe-item`, `encode-item`)
+46
View File
@@ -0,0 +1,46 @@
## General
- Test PSOX (blocked on Insignia private server support)
- Implement server-side drops on non-BB game versions
- Find a way to silence audio in RunDOL.s
- Encapsulate BB server-side random state and make replays deterministic
- Implement choice search
- Write a simple status API
- Implement per-game logging
- Add default values in all command structures (like we use for Episode 3 battle commands)
- Check for RCE potential in 6x6B-6x6E commands
- Build an exception-handling abstraction in ChatCommands that shows formatted error messages in all cases
- Make reloading happen on separate threads so compression doesn't block active clients
- Implement decrypt/encrypt actions for VMS files
- Make UI strings localizable (e.g. entries in menus, welcome message, etc.)
- Figure out what causes the corruption message on PC proxy sessions and fix it
- Enable item tracking in battle/challenge games (everything should already be set up for it to work)
- Rewrite REL-based parsers so they don't assume any fixed offsets
## Episode 3
- Make disconnecting during a tournament match cause you to forfeit the match
- Enforce tournament deck restrictions (e.g. rank checks, No Assist option) when populating COMs at tournament start time
- It may be possible to send spectators back to the waiting room after a non-tournament battle by sending 6xB4x05 with environment 0x19, then 6xB4x3B again; try this
- Add support for recording battles on the proxy server (both in primary and spectator teams)
- When `reload ep3` happens and the defs file is changed, send the new defs file to all connected players who aren't in a game (if this even works - when exactly does the client decompress the defs file from the server?)
- Make `reload licenses` not vulnerable to online players' licenses overwriting licenses on disk somehow
- Implement ranks (based on total Meseta earned)
## PSOBB
- Find any remaining mismatches in enemy IDs / experience
- Support EXP multipliers
- Sale prices for non-rare weapons with specials are computed incorrectly when buying/selling at shops
- Replace enemy list, game episode, etc. with quest data when loading a quest
- Implement trade window
- Fix some edge cases on the BB proxy server (e.g. Change Ship)
- Implement less-common subcommands
- 6xAC: Sort inventory
- 6xC1, 6xC2, 6xCD, 6xCE
- 6xCC: Exchange item for team points
- 6xD8: Add S-rank weapon special
- 6xDE: Good Luck quest
- 6xE0
- 6xE1: Gallon's Plan quest
- Implement team commands
+16
View File
@@ -0,0 +1,16 @@
struct AITalkBin {
be_uint32_t num_scs;
be_uint32_t sc_offsets[num_scs];
struct SCDialogueEntry {
be_uint32_t num_entries;
be_uint32_t unknown_a1;
be_uint32_t size; // in bytes
struct WhenEntry {
be_uint32_t when;
be_uint32_t percent_chance; // 0-100
be_uint32_t count;
be_uint32_t string_ids[count];
} __attribute__((packed));
} __attribute__((packed));
} __attribute__((packed));
+91 -1
View File
@@ -25,6 +25,18 @@
(Ep3 USA) Auto-press A as fast as possible during loading screens
042F9AC0 60000000
(Ep3 USA) Replace loading screen A button sounds with random sounds
042F9B18 4804BB19
042F9B1C 5463063E
042F9B20 60631400
042F9B24 64630005
042F9B28 38800000
(Ep3 USA) Change color of loading screens
(Replace AA, RR, GG, BB appropriately)
042FA704 3CC0AARR
042FA708 60C6GGBB
(Ep3 USA) Use 16:9 aspect ratio
04383DC8 4BC87F99
0400BD60 C042DED0
@@ -49,7 +61,7 @@
(Ep3 USA) Disable lobby event music (but keep the visuals)
040B705C 38000000
(Ep3 USA) Enable unused fourth Pinz's Shop choice
(Ep3 USA) Enable Pinz's Shop Super Card Capsule Machine as a fourth option
043101C0 38800004
04310238 2C1D0004
04487E8C 000000C8
@@ -172,5 +184,83 @@ TODO: Figure out more debug message conditionals (vars/functions) and add them h
(Ep3 USA) Dressing room always accessible
041A16FC 38600001
(Ep3 USA) Full dressing room v1
Can't change your class, but you start with your existing appearance
Go online with this code on after using the dressing room to fully save changes
0418EB5C 60000000
042A0184 389D0370
042A0188 387E2120
(Ep3 USA) Full dressing room v2
Can change your class, but you start with the default appearance
Go online with this code on after using the dressing room to fully save changes
04186ECC 4BFFFFD8
042A0184 389D0370
042A0188 387E2120
(Ep3 USA) Replace Options menu with debug menu
04149E70 38600019
(Ep3 USA) Jukebox is free
0430D1DC 48000024
(Ep3 USA) Use own character in battle (online only)
041FFAB0 4800001C
042A54D8 4BD5B0F9
04200A34 4BDFFB9D
041FFA9C 4BE00B35
040005D0 38600000
040005D4 3CA08049
040005D8 80A54160
040005DC 2805000F
040005E0 41820008
040005E4 481E8E24
040005E8 80ADA448
040005EC 7C042800
040005F0 41820008
040005F4 481E8E14
040005F8 38600001
040005FC 4E800020
(Ep3 USA) Disable chat smut filter
0412F8B8 7D0802A6
0412F8BC 7C661B78
0412F8C0 7C872378
0412F8C4 48217285
0412F8C8 38A30001
0412F8CC 7CE33B78
0412F8D0 7CC43378
0412F8D4 7D0803A6
0412F8D8 4BEDEBF4
(Ep3 USA) Metal tiles don't appear in Simulator map
04296904 4E800020
(Ep3 USA) Enable Boooo and Laughter soundchat sounds
Note: Without a TextEnglish.pr2/pr3 patch, the menu items for these sounds will be blank (but they will still work)
0430B734 38800029
0430B770 2C1F0029
0430B59C 2C030029
0430B5A8 5460083C
0430B5B4 7C63022E
0442B690 80258026
0442B694 8227852D
0442B698 80308031
0442B69C 8A3F8532
0442B6A0 8A408533
0442B6A4 8A418A28
0442B6A8 8A388A29
0442B6AC 8A39852E
0442B6B0 802F853D
0442B6B4 85348535
0442B6B8 853B8536
0442B6BC 8537852B
0442B6C0 853A853C
0442B6C4 853E8044
0442B6C8 80458046
0442B6CC 80478048
0442B6D0 8049804A
0442B6D4 804B804C
0442B6D8 804D804E
0442B6DC 804F802A
0442B6E0 802C0000
-4
View File
@@ -1,4 +0,0 @@
./newserv decrypt-gci-save --sys=8P-GPSE-PSO3_SYSTEM.gci 8P-GPSE-PSO3_CHARACTER.gci
./newserv decrypt-gci-save --sys=8P-GPSE-PSO3_SYSTEM.gci 8P-GPSE-PSO3_GUILDCARD.gci
./newserv decrypt-gci-save --sys=8P-GPOE-PSO_SYSTEM.gci 8P-GPOE-PSO_CHARACTER.gci
./newserv decrypt-gci-save --sys=8P-GPOE-PSO_SYSTEM.gci 8P-GPOE-PSO_GUILDCARD.gci
-29
View File
@@ -1,29 +0,0 @@
N1, N2, N3, N4 => use 8041F800 table
R1, R2, R3, R4 => use 8041F8A0 table
(Episode 3 USA) Able to find VIP cards offline (but still very rare)
042C0B20 4800000C
P(activate) is the probability that any transformation is attempted at all
P(f/success) defines the probability range: so the actual probability is a
uniform random number between P(activate) and P(activate) * P(f/success)
count P(activate) P(f/success) P(vip)
0-4 0.0 0.0 0.0
5-10 0.01923077 0.55 0.005
11-16 0.021276595 0.6 0.0045454544
17-24 0.023809524 0.7 0.004347826
25-32 0.027027028 0.7 0.004
33-40 0.03125 0.8 0.0038461538
41-52 0.037037037 0.8 0.0035714286
53-99 0.05 0.9 0.0033333334
0-4 0.0 0.0 0.0
5-10 0.020408163 0.55 0.005
11-16 0.022727273 0.6 0.004761905
17-24 0.025641026 0.7 0.0045454544
25-32 0.029411765 0.7 0.005
33-40 0.03448276 0.7 0.005
41-52 0.041666668 0.8 0.0045454544
53-99 0.05263158 0.9 0.004347826
+129 -129
View File
@@ -714,143 +714,143 @@ F80D ------------ -------- ------------ -------- 8C16C704-B 8C1701C4 00590D
F80E ------------ -------- ------------ -------- 8C16C7C8-L 8C17020C 00590EA0-L 00594CC0 80242DA8-L 8023DCB4 801F29D0-... 801EDC84 80109C68-... 80104D18 00219070-... 0021D030 006B1028-... 006B4964
F80F ------------ -------- ------------ -------- 8C16C7C8-L 8C170230 00590EA0-L 00594CE0 80242DA8-L 8023DC88 801F29D0-... 801EDC4C 80109C68-... 80104CE0 00219070-... 0021D060 006B1028-... 006B497C
DC-NTE--------------- DCv1----------------- DCv2----------------- PC------------------- GC1&2NTE------------- GC1&2v11------------- GCEp3USA------------- XBOX-EXE------------- BB-------------------
F810 ------------ -------- ------------ -------- 8C16C7C8-L 8C170254 00590EA0-L 00594D00 80242DA8-L 8023DC08 801F29D0-... 801EDBD8 80109C68-... 80104C6C 00219070-... 0021D090 006B1028-... 006B4994
F811 ------------ -------- ------------ -------- 8C16C6F0-() 8C17029C 00590DD0-() 00594DE0 80242F44-() 8023DBD8 801F2A10-() 801EDBA8 80109CA8-() 80104C3C 00219060-() 0021D170 006B101C-() 006B98E4
F812 ------------ -------- ------------ -------- 8C16C7C8-L 8C1702B0 00590EA0-L 00594E00 80242DA8-L 8023DB9C 801F29D0-... 801EDB64 80109C68-... 80104BF8 00219070-... 0021D190 006B1028-... 006B4A70
F813 ------------ -------- ------------ -------- 8C16C7C8-L 8C1702D4 00590EA0-L 00594E20 80242DA8-L 8023DB5C 801F29D0-... 801EDB1C 80109C68-... 80104BB0 00219070-... 0021D1B0 006B1028-... 006B4A94
F814 ------------ -------- ------------ -------- 8C16C7C8-L 8C1702F8 00590EA0-L 00594E40 80242DA8-L 8023DB24 801F29D0-... 801EDAD4 80109C68-... 80104B68 00219070-... 0021D1D0 006B1028-... 006B4AB8
F815 ------------ -------- ------------ -------- 8C16C7C8-L 8C17031C 00590EA0-L 00594E60 80242DA8-L 8023DAE4 801F29D0-... 801EDA8C 80109C68-... 80104B20 00219070-... 0021D1F0 006B1028-... 006B4ADC
F816 ------------ -------- ------------ -------- 8C16C7C8-L 8C170340 00590EA0-L 00594E80 80242DA8-L 8023DAAC 801F29D0-... 801EDA44 80109C68-... 80104AD8 00219070-... 0021D210 006B1028-... 006B98C0
F817 ------------ -------- ------------ -------- 8C16C7C8-L 8C170364 00590EA0-L 00594EA0 80242DA8-L 8023DA74 801F29D0-... 801ED9FC 80109C68-... 80104A90 00219070-... 0021D230 006B1028-... 006B4B00
F818 ------------ -------- ------------ -------- 8C16C7C8-L 8C170388 00590EA0-L 00594EC0 80242DA8-L 8023DA30 801F29D0-... 801ED9B0 80109C68-... 80104A44 00219070-... 0021D250 006B1028-... 006B4B24
F819 ------------ -------- ------------ -------- 8C16C7C8-L 8C1703AC 00590EA0-L 00594EE0 80242DA8-L 8023D9F4 801F29D0-... 801ED964 80109C68-... 801049F8 00219070-... 0021D270 006B1028-... 006B4B48
F81A ------------ -------- ------------ -------- 8C16C7C8-L 8C1703D0 00590EA0-L 00594F00 80242DA8-L 8023D9BC 801F29D0-... 801ED91C 80109C68-... 801049B0 00219070-... 0021D290 006B1028-... 006B4B6C
F81B ------------ -------- ------------ -------- 8C16C7C8-L 8C1703F4 00590EA0-L 00594F20 80242DA8-L 8023D984 801F29D0-... 801ED8D4 80109C68-... 80104968 00219070-... 0021D2B0 006B1028-... 006B4B90
F81C ------------ -------- ------------ -------- 8C16CC20-S 8C170418 00591250-S 00594F40 80242648-S 8023D92C 801F29D0-... 801ED878 80109C68-... 80104964 00219070-... 0021D2D0 006B1028-... 006B4BB4
F81D ------------ -------- ------------ -------- 8C16C7C8-L 8C170C48 00590EA0-L 00595870 80242DA8-L 8023C994 801F29D0-... 801EC87C 80109C68-... 80104218 00219070-... 0021DC80 006B1028-... 006B52EC
F81E ------------ -------- ------------ -------- 8C16C7C8-L 8C170C54 00590EA0-L 00595880 80242DA8-L 8023C96C 801F29D0-... 801EC84C 80109C68-... 801041E8 00219070-... 0021DC90 006B1028-... 006B52F8
F810 ------------ -------- ------------ -------- 8C16C7C8-L 8C170254 00590EA0-L 00594D00 80242DA8-L 8023DC08 801F29D0-... 801EDBD8 80109C68-... 80104C6C 00219070-... 0021D090 006B1028-... 006B4994 ba_initial_floor
F811 ------------ -------- ------------ -------- 8C16C6F0-() 8C17029C 00590DD0-() 00594DE0 80242F44-() 8023DBD8 801F2A10-() 801EDBA8 80109CA8-() 80104C3C 00219060-() 0021D170 006B101C-() 006B98E4 set_ba_rules
F812 ------------ -------- ------------ -------- 8C16C7C8-L 8C1702B0 00590EA0-L 00594E00 80242DA8-L 8023DB9C 801F29D0-... 801EDB64 80109C68-... 80104BF8 00219070-... 0021D190 006B1028-... 006B4A70 ba_set_tech_disk_mode
F813 ------------ -------- ------------ -------- 8C16C7C8-L 8C1702D4 00590EA0-L 00594E20 80242DA8-L 8023DB5C 801F29D0-... 801EDB1C 80109C68-... 80104BB0 00219070-... 0021D1B0 006B1028-... 006B4A94 ba_set_weapon_and_armor_mode
F814 ------------ -------- ------------ -------- 8C16C7C8-L 8C1702F8 00590EA0-L 00594E40 80242DA8-L 8023DB24 801F29D0-... 801EDAD4 80109C68-... 80104B68 00219070-... 0021D1D0 006B1028-... 006B4AB8 ba_set_forbid_mags
F815 ------------ -------- ------------ -------- 8C16C7C8-L 8C17031C 00590EA0-L 00594E60 80242DA8-L 8023DAE4 801F29D0-... 801EDA8C 80109C68-... 80104B20 00219070-... 0021D1F0 006B1028-... 006B4ADC ba_set_tool_mode
F816 ------------ -------- ------------ -------- 8C16C7C8-L 8C170340 00590EA0-L 00594E80 80242DA8-L 8023DAAC 801F29D0-... 801EDA44 80109C68-... 80104AD8 00219070-... 0021D210 006B1028-... 006B98C0 ba_set_trap_mode
F817 ------------ -------- ------------ -------- 8C16C7C8-L 8C170364 00590EA0-L 00594EA0 80242DA8-L 8023DA74 801F29D0-... 801ED9FC 80109C68-... 80104A90 00219070-... 0021D230 006B1028-... 006B4B00 ba_set_unused_F817
F818 ------------ -------- ------------ -------- 8C16C7C8-L 8C170388 00590EA0-L 00594EC0 80242DA8-L 8023DA30 801F29D0-... 801ED9B0 80109C68-... 80104A44 00219070-... 0021D250 006B1028-... 006B4B24 ba_set_respawn
F819 ------------ -------- ------------ -------- 8C16C7C8-L 8C1703AC 00590EA0-L 00594EE0 80242DA8-L 8023D9F4 801F29D0-... 801ED964 80109C68-... 801049F8 00219070-... 0021D270 006B1028-... 006B4B48 ba_set_replace_char
F81A ------------ -------- ------------ -------- 8C16C7C8-L 8C1703D0 00590EA0-L 00594F00 80242DA8-L 8023D9BC 801F29D0-... 801ED91C 80109C68-... 801049B0 00219070-... 0021D290 006B1028-... 006B4B6C ba_dropwep
F81B ------------ -------- ------------ -------- 8C16C7C8-L 8C1703F4 00590EA0-L 00594F20 80242DA8-L 8023D984 801F29D0-... 801ED8D4 80109C68-... 80104968 00219070-... 0021D2B0 006B1028-... 006B4B90 ba_teams
F81C ------------ -------- ------------ -------- 8C16CC20-S 8C170418 00591250-S 00594F40 80242648-S 8023D92C 801F29D0-... 801ED878 80109C68-... 80104964 00219070-... 0021D2D0 006B1028-... 006B4BB4 ba_start
F81D ------------ -------- ------------ -------- 8C16C7C8-L 8C170C48 00590EA0-L 00595870 80242DA8-L 8023C994 801F29D0-... 801EC87C 80109C68-... 80104218 00219070-... 0021DC80 006B1028-... 006B52EC death_lvl_up
F81E ------------ -------- ------------ -------- 8C16C7C8-L 8C170C54 00590EA0-L 00595880 80242DA8-L 8023C96C 801F29D0-... 801EC84C 80109C68-... 801041E8 00219070-... 0021DC90 006B1028-... 006B52F8 ba_set_meseta_drop_mode
DC-NTE--------------- DCv1----------------- DCv2----------------- PC------------------- GC1&2NTE------------- GC1&2v11------------- GCEp3USA------------- XBOX-EXE------------- BB-------------------
F820 ------------ -------- ------------ -------- 8C16C7C8-L 8C17042C 00590EA0-L 00594F60 80242DA8-L 8023D8C4 801F29D0-... 801ED7F8 80109C68-... 80104960 00219070-... 0021D310 006B1028-... 006B4BC8
F821 ------------ -------- ------------ -------- 8C16C704-B 8C170450 00590DE0-B 00594F80 80242EF8-B 8023D824 801F2988-B 801ED6FC 80109C20-B 8010495C 00219090-B 0021D370 006B1040-B 006B4BF4
F822 ------------ -------- ------------ -------- 8C16C704-B 8C1704C8 00590DE0-B 00595030 80242EF8-B 8023D820 801F2988-B 801ED6A4 80109C20-B 80104958 00219090-B 00028D40 006B1040-B 0061CDB0
F823 ------------ -------- ------------ -------- 8C16C7C8-L 8C1704CC 00590EA0-L 00595040 80242DA8-L 8023D7E0 801F29D0-... 801ED658 80109C68-... 80104954 00219070-... 0021D3B0 006B1028-... 006B4C70
F824 ------------ -------- ------------ -------- 8C16C7C8-L 8C1704D8 00590EA0-L 00595050 80242DA8-L 8023D7A0 801F29D0-... 801ED60C 80109C68-... 80104950 00219070-... 0021D3E0 006B1028-... 006B4C80
F825 ------------ -------- ------------ -------- 8C16C704-B 8C1704E4 00590DE0-B 00595060 80242EF8-B 8023D6F0 801F2988-B 801ED54C 80109C20-B 8010494C 00219090-B 0021D430 006B1040-B 006B4C90
F826 ------------ -------- ------------ -------- 8C16C704-B 8C17051C 00590DE0-B 005950B0 80242EF8-B 8023D68C 801F2988-B 801ED4F4 80109C20-B 80104948 00219090-B 0021D480 006B1040-B 006B4CC0
F827 ------------ -------- ------------ -------- 8C16C704-B 8C170544 00590DE0-B 005950D0 80242EF8-B 8023D628 801F2988-B 801ED49C 80109C20-B 80104944 00219090-B 0021D4C0 006B1040-B 006B4CD4
F828 ------------ -------- ------------ -------- 8C16C730-BB 8C170590 00590E10-BB 00595120 80242EA0-BB 8023D540 801F2930-BB 801ED3A8 80109BC8-BB 80104898 002190C0-BB 0021D540 006B1058-BB 006B4D08
F829 ------------ -------- ------------ -------- 8C16C730-BB 8C170620 00590E10-BB 005951A0 80242EA0-BB 8023D4DC 801F2930-BB 801ED340 80109BC8-BB 80104830 002190C0-BB 0021D5C0 006B1058-BB 006B4D80
F82A ------------ -------- ------------ -------- 8C16C704-B 8C17066C 00590DE0-B 00595200 80242EF8-B 8023D4A0 801F2988-B 801ED304 80109C20-B 801047EC 00219090-B 0021D610 006B1040-B 006B4DC0
F82B ------------ -------- ------------ -------- 8C16C810-LL 8C17069C 00590ED0-LL 00595230 80242D00-LL 8023D414 801F29D0-... 801ED2A0 80109C68-... 80104788 00219070-... 0021D650 006B1028-... 006B4DE4
F82C ------------ -------- ------------ -------- 8C16C810-LL 8C1706E0 00590ED0-LL 00595280 80242D00-LL 8023D38C 801F29D0-... 801ED240 80109C68-... 80104728 00219070-... 0021D6D0 006B1028-... 006B4E30
F82D ------------ -------- ------------ -------- 8C16C704-B 8C170724 00590DE0-B 005952D0 80242EF8-B 8023D33C 801F2988-B 801ED1F0 80109C20-B 801046D8 00219090-B 0021D750 006B1040-B 006B4E7C
F82E ------------ -------- ------------ -------- 8C16C704-B 8C170758 00590DE0-B 00595300 80242EF8-B 8023D2E8 801F2988-B 801ED19C 80109C20-B 80104684 00219090-B 0021D780 006B1040-B 006B4EA4
F820 ------------ -------- ------------ -------- 8C16C7C8-L 8C17042C 00590EA0-L 00594F60 80242DA8-L 8023D8C4 801F29D0-... 801ED7F8 80109C68-... 80104960 00219070-... 0021D310 006B1028-... 006B4BC8 cmode_stage
F821 ------------ -------- ------------ -------- 8C16C704-B 8C170450 00590DE0-B 00594F80 80242EF8-B 8023D824 801F2988-B 801ED6FC 80109C20-B 8010495C 00219090-B 0021D370 006B1040-B 006B4BF4 nop_F821
F822 ------------ -------- ------------ -------- 8C16C704-B 8C1704C8 00590DE0-B 00595030 80242EF8-B 8023D820 801F2988-B 801ED6A4 80109C20-B 80104958 00219090-B 00028D40 006B1040-B 0061CDB0 nop_F822
F823 ------------ -------- ------------ -------- 8C16C7C8-L 8C1704CC 00590EA0-L 00595040 80242DA8-L 8023D7E0 801F29D0-... 801ED658 80109C68-... 80104954 00219070-... 0021D3B0 006B1028-... 006B4C70 set_cmode_char_template
F824 ------------ -------- ------------ -------- 8C16C7C8-L 8C1704D8 00590EA0-L 00595050 80242DA8-L 8023D7A0 801F29D0-... 801ED60C 80109C68-... 80104950 00219070-... 0021D3E0 006B1028-... 006B4C80 set_cmode_diff
F825 ------------ -------- ------------ -------- 8C16C704-B 8C1704E4 00590DE0-B 00595060 80242EF8-B 8023D6F0 801F2988-B 801ED54C 80109C20-B 8010494C 00219090-B 0021D430 006B1040-B 006B4C90 exp_multiplication
F826 ------------ -------- ------------ -------- 8C16C704-B 8C17051C 00590DE0-B 005950B0 80242EF8-B 8023D68C 801F2988-B 801ED4F4 80109C20-B 80104948 00219090-B 0021D480 006B1040-B 006B4CC0 if_player_alive_cm
F827 ------------ -------- ------------ -------- 8C16C704-B 8C170544 00590DE0-B 005950D0 80242EF8-B 8023D628 801F2988-B 801ED49C 80109C20-B 80104944 00219090-B 0021D4C0 006B1040-B 006B4CD4 get_user_is_dead
F828 ------------ -------- ------------ -------- 8C16C730-BB 8C170590 00590E10-BB 00595120 80242EA0-BB 8023D540 801F2930-BB 801ED3A8 80109BC8-BB 80104898 002190C0-BB 0021D540 006B1058-BB 006B4D08 go_floor
F829 ------------ -------- ------------ -------- 8C16C730-BB 8C170620 00590E10-BB 005951A0 80242EA0-BB 8023D4DC 801F2930-BB 801ED340 80109BC8-BB 80104830 002190C0-BB 0021D5C0 006B1058-BB 006B4D80 get_num_kills
F82A ------------ -------- ------------ -------- 8C16C704-B 8C17066C 00590DE0-B 00595200 80242EF8-B 8023D4A0 801F2988-B 801ED304 80109C20-B 801047EC 00219090-B 0021D610 006B1040-B 006B4DC0 reset_kills
F82B ------------ -------- ------------ -------- 8C16C810-LL 8C17069C 00590ED0-LL 00595230 80242D00-LL 8023D414 801F29D0-... 801ED2A0 80109C68-... 80104788 00219070-... 0021D650 006B1028-... 006B4DE4 unlock_door2
F82C ------------ -------- ------------ -------- 8C16C810-LL 8C1706E0 00590ED0-LL 00595280 80242D00-LL 8023D38C 801F29D0-... 801ED240 80109C68-... 80104728 00219070-... 0021D6D0 006B1028-... 006B4E30 lock_door2
F82D ------------ -------- ------------ -------- 8C16C704-B 8C170724 00590DE0-B 005952D0 80242EF8-B 8023D33C 801F2988-B 801ED1F0 80109C20-B 801046D8 00219090-B 0021D750 006B1040-B 006B4E7C if_switch_not_pressed
F82E ------------ -------- ------------ -------- 8C16C704-B 8C170758 00590DE0-B 00595300 80242EF8-B 8023D2E8 801F2988-B 801ED19C 80109C20-B 80104684 00219090-B 0021D780 006B1040-B 006B4EA4 if_switch_pressed
DC-NTE--------------- DCv1----------------- DCv2----------------- PC------------------- GC1&2NTE------------- GC1&2v11------------- GCEp3USA------------- XBOX-EXE------------- BB-------------------
F830 ------------ -------- ------------ -------- 8C16C704-B 8C17056C 00590DE0-B 005950F0 80242EF8-B 8023D5F8 801F2988-B 801ED46C 80109C20-B 80104940 00219090-B 0021D500 006B1040-B 006B4CE8
F831 ------------ -------- ------------ -------- 8C16C6F0-() 8C170584 00590DD0-() 00595110 80242F44-() 8023D5D8 801F2A10-() 801ED44C 80109CA8-() 8010493C 00219060-() 0021D530 006B101C-() 006B4D00
F838 ------------ -------- ------------ -------- 8C16C704-B 8C17078C 00590DE0-B 00595340 80242EF8-B 8023D2A0 801F2988-B 801ED14C 80109C20-B 80104634 00219090-B 0021D7B0 006B1040-B 006B4ED4
F839 ------------ -------- ------------ -------- 8C16C704-B 8C1707D8 00590DE0-B 00595380 80242EF8-B 8023D258 801F2988-B 801ED0FC 80109C20-B 801045E4 00219090-B 0021D800 006B1040-B 006B4F04
F83A ------------ -------- ------------ -------- 8C16C704-B 8C170824 00590DE0-B 005953C0 80242EF8-B 8023D180 801F2988-B 801ED00C 80109C20-B 801044F4 00219090-B 0021D840 006B1040-B 006B4F34
F83B ------------ -------- ------------ -------- 8C16C704-B 8C1708B8 00590DE0-B 00595460 80242EF8-B 8023D0A8 801F2988-B 801ECF1C 80109C20-B 801043E4 00219090-B 0021D8D0 006B1040-B 006B4FA4
F83C ------------ -------- ------------ -------- 8C16C704-B 8C170990 00590DE0-B 00595540 80242EF8-B 8023CF34 801F2988-B 801ECE6C 80109C20-B 80104388 00219090-B 0021D9A0 006B1040-B 006B50D8
F83D ------------ -------- ------------ -------- 8C16C7C8-L 8C170C70 00590EA0-L 005958A0 80242DA8-L 8023C964 801F29D0-... 801EC820 80109C68-... 801041B4 00219070-... 0021DCB0 006B1028-... 006B5314
F83E ------------ -------- ------------ -------- 8C16C7C8-L 8C170C7C 00590EA0-L 005958B0 80242DA8-L 8023C95C 801F29D0-... 801EC7F4 80109C68-... 80104180 00219070-... 0021DCC0 006B1028-... 006B5320
F830 ------------ -------- ------------ -------- 8C16C704-B 8C17056C 00590DE0-B 005950F0 80242EF8-B 8023D5F8 801F2988-B 801ED46C 80109C20-B 80104940 00219090-B 0021D500 006B1040-B 006B4CE8 control_dragon
F831 ------------ -------- ------------ -------- 8C16C6F0-() 8C170584 00590DD0-() 00595110 80242F44-() 8023D5D8 801F2A10-() 801ED44C 80109CA8-() 8010493C 00219060-() 0021D530 006B101C-() 006B4D00 release_dragon
F838 ------------ -------- ------------ -------- 8C16C704-B 8C17078C 00590DE0-B 00595340 80242EF8-B 8023D2A0 801F2988-B 801ED14C 80109C20-B 80104634 00219090-B 0021D7B0 006B1040-B 006B4ED4 shrink
F839 ------------ -------- ------------ -------- 8C16C704-B 8C1707D8 00590DE0-B 00595380 80242EF8-B 8023D258 801F2988-B 801ED0FC 80109C20-B 801045E4 00219090-B 0021D800 006B1040-B 006B4F04 unshrink
F83A ------------ -------- ------------ -------- 8C16C704-B 8C170824 00590DE0-B 005953C0 80242EF8-B 8023D180 801F2988-B 801ED00C 80109C20-B 801044F4 00219090-B 0021D840 006B1040-B 006B4F34 set_shrink_cam1
F83B ------------ -------- ------------ -------- 8C16C704-B 8C1708B8 00590DE0-B 00595460 80242EF8-B 8023D0A8 801F2988-B 801ECF1C 80109C20-B 801043E4 00219090-B 0021D8D0 006B1040-B 006B4FA4 set_shrink_cam2
F83C ------------ -------- ------------ -------- 8C16C704-B 8C170990 00590DE0-B 00595540 80242EF8-B 8023CF34 801F2988-B 801ECE6C 80109C20-B 80104388 00219090-B 0021D9A0 006B1040-B 006B50D8 display_clock2
F83D ------------ -------- ------------ -------- 8C16C7C8-L 8C170C70 00590EA0-L 005958A0 80242DA8-L 8023C964 801F29D0-... 801EC820 80109C68-... 801041B4 00219070-... 0021DCB0 006B1028-... 006B5314 set_area_total
F83E ------------ -------- ------------ -------- 8C16C7C8-L 8C170C7C 00590EA0-L 005958B0 80242DA8-L 8023C95C 801F29D0-... 801EC7F4 80109C68-... 80104180 00219070-... 0021DCC0 006B1028-... 006B5320 delete_area_title
DC-NTE--------------- DCv1----------------- DCv2----------------- PC------------------- GC1&2NTE------------- GC1&2v11------------- GCEp3USA------------- XBOX-EXE------------- BB-------------------
F840 ------------ -------- ------------ -------- 8C16C6F0-() 8C17094C 00590DD0-() 00595500 80242F44-() 8023D088 801F2A10-() 801ECEFC 80109CA8-() 801043C4 00219060-() 0021D960 006B101C-() 006B98B8
F841 ------------ -------- ------------ -------- 8C16C97C-W 8C170958 00591040-W 00595510 80242A98-W 8023CF8C 801F2848-W 801ECEC4 80109AE0-W 8010438C 00219370-W 0021D970 006B10E0-W 006B5014
F848 ------------ -------- ------------ -------- 8C16C704-B 8C1709A8 00590DE0-B 00595560 80242EF8-B 8023CEB4 801F2988-B 801ECDE8 80109C20-B 80104384 00219090-B 0021DA10 006B1040-B 006B50EC
F849 ------------ -------- ------------ -------- 8C16C704-B 8C1709E0 00590DE0-B 005955A0 80242EF8-B 8023CE34 801F2988-B 801ECD64 80109C20-B 80104380 00219090-B 0021DA40 006B1040-B 006B5118
F84A ------------ -------- ------------ -------- 8C16C704-B 8C170A18 00590DE0-B 005955E0 80242EF8-B 8023CDB4 801F2988-B 801ECCE0 80109C20-B 8010437C 00219090-B 0021DA70 006B1040-B 006B5144
F84B ------------ -------- ------------ -------- 8C16C704-B 8C170A50 00590DE0-B 00595620 80242EF8-B 8023CD34 801F2988-B 801ECC5C 80109C20-B 80104378 00219090-B 0021DAA0 006B1040-B 006B5170
F84C ------------ -------- ------------ -------- 8C16C704-B 8C170A88 00590DE0-B 00595660 80242EF8-B 8023CCB4 801F2988-B 801ECBD8 80109C20-B 80104374 00219090-B 0021DAD0 006B1040-B 006B519C
F84D ------------ -------- ------------ -------- 8C16C704-B 8C170AC0 00590DE0-B 005956A0 80242EF8-B 8023CC34 801F2988-B 801ECB54 80109C20-B 80104370 00219090-B 0021DB00 006B1040-B 006B51C8
F84E ------------ -------- ------------ -------- 8C16C704-B 8C170AF8 00590DE0-B 005956E0 80242EF8-B 8023CBB4 801F2988-B 801ECAD0 80109C20-B 8010436C 00219090-B 0021DB30 006B1040-B 006B51F4
F84F ------------ -------- ------------ -------- 8C16C704-B 8C170B30 00590DE0-B 00595720 80242EF8-B 8023CB34 801F2988-B 801ECA4C 80109C20-B 80104368 00219090-B 0021DB60 006B1040-B 006B5220
F840 ------------ -------- ------------ -------- 8C16C6F0-() 8C17094C 00590DD0-() 00595500 80242F44-() 8023D088 801F2A10-() 801ECEFC 80109CA8-() 801043C4 00219060-() 0021D960 006B101C-() 006B98B8 load_npc_data
F841 ------------ -------- ------------ -------- 8C16C97C-W 8C170958 00591040-W 00595510 80242A98-W 8023CF8C 801F2848-W 801ECEC4 80109AE0-W 8010438C 00219370-W 0021D970 006B10E0-W 006B5014 get_npc_data
F848 ------------ -------- ------------ -------- 8C16C704-B 8C1709A8 00590DE0-B 00595560 80242EF8-B 8023CEB4 801F2988-B 801ECDE8 80109C20-B 80104384 00219090-B 0021DA10 006B1040-B 006B50EC give_damage_score
F849 ------------ -------- ------------ -------- 8C16C704-B 8C1709E0 00590DE0-B 005955A0 80242EF8-B 8023CE34 801F2988-B 801ECD64 80109C20-B 80104380 00219090-B 0021DA40 006B1040-B 006B5118 take_damage_score
F84A ------------ -------- ------------ -------- 8C16C704-B 8C170A18 00590DE0-B 005955E0 80242EF8-B 8023CDB4 801F2988-B 801ECCE0 80109C20-B 8010437C 00219090-B 0021DA70 006B1040-B 006B5144 enemy_give_score
F84B ------------ -------- ------------ -------- 8C16C704-B 8C170A50 00590DE0-B 00595620 80242EF8-B 8023CD34 801F2988-B 801ECC5C 80109C20-B 80104378 00219090-B 0021DAA0 006B1040-B 006B5170 enemy_take_score
F84C ------------ -------- ------------ -------- 8C16C704-B 8C170A88 00590DE0-B 00595660 80242EF8-B 8023CCB4 801F2988-B 801ECBD8 80109C20-B 80104374 00219090-B 0021DAD0 006B1040-B 006B519C kill_score
F84D ------------ -------- ------------ -------- 8C16C704-B 8C170AC0 00590DE0-B 005956A0 80242EF8-B 8023CC34 801F2988-B 801ECB54 80109C20-B 80104370 00219090-B 0021DB00 006B1040-B 006B51C8 death_score
F84E ------------ -------- ------------ -------- 8C16C704-B 8C170AF8 00590DE0-B 005956E0 80242EF8-B 8023CBB4 801F2988-B 801ECAD0 80109C20-B 8010436C 00219090-B 0021DB30 006B1040-B 006B51F4 enemy_kill_score
F84F ------------ -------- ------------ -------- 8C16C704-B 8C170B30 00590DE0-B 00595720 80242EF8-B 8023CB34 801F2988-B 801ECA4C 80109C20-B 80104368 00219090-B 0021DB60 006B1040-B 006B5220 enemy_death_score
DC-NTE--------------- DCv1----------------- DCv2----------------- PC------------------- GC1&2NTE------------- GC1&2v11------------- GCEp3USA------------- XBOX-EXE------------- BB-------------------
F850 ------------ -------- ------------ -------- 8C16C704-B 8C170B68 00590DE0-B 00595760 80242EF8-B 8023CAB4 801F2988-B 801EC9C8 80109C20-B 80104364 00219090-B 0021DB90 006B1040-B 006B524C
F851 ------------ -------- ------------ -------- 8C16C704-B 8C170BA0 00590DE0-B 005957A0 80242EF8-B 8023CA68 801F2988-B 801EC97C 80109C20-B 80104318 00219090-B 0021DBC0 006B1040-B 006B9888
F852 ------------ -------- ------------ -------- 8C16C7C8-L 8C170BD8 00590EA0-L 005957E0 80242DA8-L 8023CA50 801F29D0-... 801EC95C 80109C68-... 801042F8 00219070-... 0021DBF0 006B1028-... 006B5278
F853 ------------ -------- ------------ -------- 8C16C6F0-() 8C170C88 00590DD0-() 005958C0 80242F44-() 8023C950 801F2A10-() 801EC7D4 80109CA8-() 80104154 00219060-() 0021DCD0 006B101C-() 006B532C
F854 ------------ -------- ------------ -------- 8C16C6F0-() 8C170C94 00590DD0-() 005958D0 80242F44-() 8023C944 801F2A10-() 801EC7B4 80109CA8-() 80104134 00219060-() 0021DCE0 006B101C-() 006B5338
F855 ------------ -------- ------------ -------- 8C16C6F0-() 8C170CA0 00590DD0-() 005958E0 80242F44-() 8023C938 801F2A10-() 801EC794 80109CA8-() 80104108 00219060-() 0021DCF0 006B101C-() 006B5344
F856 ------------ -------- ------------ -------- 8C16C6F0-() 8C170CAC 00590DD0-() 005958F0 80242F44-() 8023C92C 801F2A10-() 801EC774 80109CA8-() 801040E8 00219060-() 0021DD00 006B101C-() 006B5350
F857 ------------ -------- ------------ -------- 8C16CC20-S 8C170CB8 00591250-S 00595900 80242648-S 8023C8EC 801F29D0-... 801EC728 80109C68-... 801040E4 00219070-... 0021DD10 006B1028-... 006B535C
F858 ------------ -------- ------------ -------- 8C16C6F0-() 8C170CC4 00590DD0-() 00595910 80242F44-() 8023C8E0 801F2A10-() 801EC704 80109CA8-() 801040C0 00219060-() 0021DD40 006B101C-() 006B987C
F859 ------------ -------- ------------ -------- 8C16C6F0-() 8C170CD0 00590DD0-() 00595920 80242F44-() 8023C8D4 801F2A10-() 801EC6E0 80109CA8-() 8010409C 00219060-() 0021DD50 006B101C-() 006B9870
F85A ------------ -------- ------------ -------- 8C16C7C8-L 8C170CDC 00590EA0-L 00595930 80242DA8-L 8023C854 801F2988-B 801EC660 80109C20-B 8010401C 00219090-B 0021DD60 006B1040-B 006B536C
F85B ------------ -------- ------------ -------- 8C16C810-LL 8C170D3C 00590ED0-LL 00595980 80242D00-LL 8023C824 801F29D0-... 801EC630 80109C68-... 80103FEC 00219070-... 0021DDC0 006B1028-... 006B53C8
F85C ------------ -------- ------------ -------- 8C16C6F0-() 8C170D54 00590DD0-() 005959A0 80242F44-() 8023C7F4 801F2A10-() 801EC600 80109CA8-() 80103FBC 00219060-() 0021DDE0 006B101C-() 006B53E0
F85D ------------ -------- ------------ -------- 8C16C7C8-L 8C170D80 00590EA0-L 005959C0 80242DA8-L 8023C7D4 801F29D0-... 801EC5D4 80109C68-... 80103F90 00219070-... 0021DE10 006B1028-... 006B53F8
F85E ------------ -------- ------------ -------- 8C16C7C8-L 8C170D8C 00590EA0-L 005959D0 80242DA8-L 8023C7B0 801F29D0-... 801EC5A4 80109C68-... 80103F60 00219070-... 0021DE20 006B1028-... 006B5408
F85F ------------ -------- ------------ -------- 8C16C7C8-L 8C170DAC 00590EA0-L 005959F0 80242DA8-L 8023C7A0 801F29D0-... 801EC58C 80109C68-... 80103F48 00219070-... 0021DE40 006B1028-... 006B5424
F850 ------------ -------- ------------ -------- 8C16C704-B 8C170B68 00590DE0-B 00595760 80242EF8-B 8023CAB4 801F2988-B 801EC9C8 80109C20-B 80104364 00219090-B 0021DB90 006B1040-B 006B524C meseta_score
F851 ------------ -------- ------------ -------- 8C16C704-B 8C170BA0 00590DE0-B 005957A0 80242EF8-B 8023CA68 801F2988-B 801EC97C 80109C20-B 80104318 00219090-B 0021DBC0 006B1040-B 006B9888 ba_set_trap_count
F852 ------------ -------- ------------ -------- 8C16C7C8-L 8C170BD8 00590EA0-L 005957E0 80242DA8-L 8023CA50 801F29D0-... 801EC95C 80109C68-... 801042F8 00219070-... 0021DBF0 006B1028-... 006B5278 ba_set_target
F853 ------------ -------- ------------ -------- 8C16C6F0-() 8C170C88 00590DD0-() 005958C0 80242F44-() 8023C950 801F2A10-() 801EC7D4 80109CA8-() 80104154 00219060-() 0021DCD0 006B101C-() 006B532C reverse_warps
F854 ------------ -------- ------------ -------- 8C16C6F0-() 8C170C94 00590DD0-() 005958D0 80242F44-() 8023C944 801F2A10-() 801EC7B4 80109CA8-() 80104134 00219060-() 0021DCE0 006B101C-() 006B5338 unreverse_warps
F855 ------------ -------- ------------ -------- 8C16C6F0-() 8C170CA0 00590DD0-() 005958E0 80242F44-() 8023C938 801F2A10-() 801EC794 80109CA8-() 80104108 00219060-() 0021DCF0 006B101C-() 006B5344 set_ult_map
F856 ------------ -------- ------------ -------- 8C16C6F0-() 8C170CAC 00590DD0-() 005958F0 80242F44-() 8023C92C 801F2A10-() 801EC774 80109CA8-() 801040E8 00219060-() 0021DD00 006B101C-() 006B5350 unset_ult_map
F857 ------------ -------- ------------ -------- 8C16CC20-S 8C170CB8 00591250-S 00595900 80242648-S 8023C8EC 801F29D0-... 801EC728 80109C68-... 801040E4 00219070-... 0021DD10 006B1028-... 006B535C set_area_title
F858 ------------ -------- ------------ -------- 8C16C6F0-() 8C170CC4 00590DD0-() 00595910 80242F44-() 8023C8E0 801F2A10-() 801EC704 80109CA8-() 801040C0 00219060-() 0021DD40 006B101C-() 006B987C ba_show_self_traps
F859 ------------ -------- ------------ -------- 8C16C6F0-() 8C170CD0 00590DD0-() 00595920 80242F44-() 8023C8D4 801F2A10-() 801EC6E0 80109CA8-() 8010409C 00219060-() 0021DD50 006B101C-() 006B9870 ba_hide_self_traps
F85A ------------ -------- ------------ -------- 8C16C7C8-L 8C170CDC 00590EA0-L 00595930 80242DA8-L 8023C854 801F2988-B 801EC660 80109C20-B 8010401C 00219090-B 0021DD60 006B1040-B 006B536C equip_item
F85B ------------ -------- ------------ -------- 8C16C810-LL 8C170D3C 00590ED0-LL 00595980 80242D00-LL 8023C824 801F29D0-... 801EC630 80109C68-... 80103FEC 00219070-... 0021DDC0 006B1028-... 006B53C8 unequip_item
F85C ------------ -------- ------------ -------- 8C16C6F0-() 8C170D54 00590DD0-() 005959A0 80242F44-() 8023C7F4 801F2A10-() 801EC600 80109CA8-() 80103FBC 00219060-() 0021DDE0 006B101C-() 006B53E0 qexit2
F85D ------------ -------- ------------ -------- 8C16C7C8-L 8C170D80 00590EA0-L 005959C0 80242DA8-L 8023C7D4 801F29D0-... 801EC5D4 80109C68-... 80103F90 00219070-... 0021DE10 006B1028-... 006B53F8 set_allow_item_flags
F85E ------------ -------- ------------ -------- 8C16C7C8-L 8C170D8C 00590EA0-L 005959D0 80242DA8-L 8023C7B0 801F29D0-... 801EC5A4 80109C68-... 80103F60 00219070-... 0021DE20 006B1028-... 006B5408 ba_enable_sonar
F85F ------------ -------- ------------ -------- 8C16C7C8-L 8C170DAC 00590EA0-L 005959F0 80242DA8-L 8023C7A0 801F29D0-... 801EC58C 80109C68-... 80103F48 00219070-... 0021DE40 006B1028-... 006B5424 ba_use_sonar
DC-NTE--------------- DCv1----------------- DCv2----------------- PC------------------- GC1&2NTE------------- GC1&2v11------------- GCEp3USA------------- XBOX-EXE------------- BB-------------------
F860 ------------ -------- ------------ -------- 8C16C6F0-() 8C170DB8 00590DD0-() 00595A00 80242F44-() 8023C778 801F2A10-() 801EC564 80109CA8-() 80103F44 00219060-() 0021DE50 006B101C-() 006B5430
F861 ------------ -------- ------------ -------- 8C16C7C8-L 8C170DD8 00590EA0-L 00595A20 80242DA8-L 8023C744 801F29D0-... 801EC524 80109C68-... 80103F40 00219070-... 0021DE70 006B1028-... 006B5464
F862 ------------ -------- ------------ -------- 8C16C878-LLS 8C170E00 00590F10-LLS 00595A50 80242B98-LLS 8023C6A4 801F29D0-... 801EC480 80109C68-... 80103E9C 00219070-... 0021DEA0 006B1028-... 006B54F4
F863 ------------ -------- ------------ -------- 8C16C7C8-L 8C170E88 00590EA0-L 00595AD0 80242DA8-L 8023C5E8 801F2988-B 801EC3D4 80109C20-B 80103DF0 00219090-B 0021DF30 006B1040-B 006B5570
F864 ------------ -------- ------------ -------- 8C16CC88-LS 8C170F50 00591320-LS 00595BA0 80242514-LS 8023C594 801F29D0-... 801EC37C 80109C68-... 80103DEC 00219070-... 0021E010 006B1028-... 006B5634
F865 ------------ -------- ------------ -------- 8C16C6F0-() 8C170F60 00590DD0-() 00595BC0 80242F44-() 8023C564 801F2A10-() 801EC34C 80109CA8-() 80103DBC 00219060-() 0021E030 006B101C-() 006B564C
F866 ------------ -------- ------------ -------- 8C16C6F0-() 8C170F8C 00590DD0-() 00595BE0 80242F44-() 8023C534 801F2A10-() 801EC31C 80109CA8-() 80103D8C 00219060-() 0021E080 006B101C-() 006B5668
F867 ------------ -------- ------------ -------- 8C16C704-B 8C170FB8 00590DE0-B 00595C00 80242EF8-B 8023C470 801F2988-B 801EC25C 80109C20-B 80103D88 00219090-B 0021E0D0 006B1040-B 006B5684
F868 ------------ -------- ------------ -------- 8C16C730-BB 8C17100C 00590E10-BB 00595C60 80242EA0-BB 8023C3A0 801F2930-BB 801EC188 80109BC8-BB 80103D84 002190C0-BB 0021E190 006B1058-BB 006B56D8
F869 ------------ -------- ------------ -------- 8C16C730-BB 8C171104 00590E10-BB 00595D90 80242EA0-BB 8023C1AC 801F2930-BB 801EBF90 80109BC8-BB 80103C94 002190C0-BB 0021E4E0 006B1058-BB 006B57E0
F86A ------------ -------- ------------ -------- 8C16C730-BB 8C1711C0 00590E10-BB 00595E80 80242EA0-BB 8023C084 801F2930-BB 801EBE70 80109BC8-BB 80103C90 002190C0-BB 0021E5C0 006B1058-BB 006B5894
F86B ------------ -------- ------------ -------- 8C16C704-B 8C171264 00590DE0-B 00595F70 80242EF8-B 8023C058 801F2988-B 801EBE44 80109C20-B 80103C64 00219090-B 0021E6A0 006B1040-B 006B5940
F86C ------------ -------- ------------ -------- 8C16C704-B 8C17127C 00590DE0-B 00595F90 80242EF8-B 8023C008 801F2988-B 801EBDF4 80109C20-B 80103C14 00219090-B 0021E6C0 006B1040-B 006B5954
F86D ------------ -------- ------------ -------- 8C16C6F0-() 8C1712C4 00590DD0-() 00595FD0 80242F44-() 8023BFFC 801F2A10-() 801EBDD0 80109CA8-() 80103BF0 00219060-() 0021E720 006B101C-() 006B5988
F86E ------------ -------- ------------ -------- 8C16C6F0-() 8C1712D0 00590DD0-() 00595FE0 80242F44-() 8023BFF0 801F2A10-() 801EBDAC 80109CA8-() 80103BCC 00219060-() 0021E730 006B101C-() 006B5994
F86F ------------ -------- ------------ -------- 8C16C7C8-L 8C170BEC 00590EA0-L 005957F0 80242DA8-L 8023CA34 801F29D0-... 801EC938 80109C68-... 801042D4 00219070-... 0021DC00 006B1028-... 006B528C
F860 ------------ -------- ------------ -------- 8C16C6F0-() 8C170DB8 00590DD0-() 00595A00 80242F44-() 8023C778 801F2A10-() 801EC564 80109CA8-() 80103F44 00219060-() 0021DE50 006B101C-() 006B5430 clear_score_announce
F861 ------------ -------- ------------ -------- 8C16C7C8-L 8C170DD8 00590EA0-L 00595A20 80242DA8-L 8023C744 801F29D0-... 801EC524 80109C68-... 80103F40 00219070-... 0021DE70 006B1028-... 006B5464 set_score_announce
F862 ------------ -------- ------------ -------- 8C16C878-LLS 8C170E00 00590F10-LLS 00595A50 80242B98-LLS 8023C6A4 801F29D0-... 801EC480 80109C68-... 80103E9C 00219070-... 0021DEA0 006B1028-... 006B54F4 give_s_rank_weapon
F863 ------------ -------- ------------ -------- 8C16C7C8-L 8C170E88 00590EA0-L 00595AD0 80242DA8-L 8023C5E8 801F2988-B 801EC3D4 80109C20-B 80103DF0 00219090-B 0021DF30 006B1040-B 006B5570 get_mag_levels
F864 ------------ -------- ------------ -------- 8C16CC88-LS 8C170F50 00591320-LS 00595BA0 80242514-LS 8023C594 801F29D0-... 801EC37C 80109C68-... 80103DEC 00219070-... 0021E010 006B1028-... 006B5634 cmode_rank
F865 ------------ -------- ------------ -------- 8C16C6F0-() 8C170F60 00590DD0-() 00595BC0 80242F44-() 8023C564 801F2A10-() 801EC34C 80109CA8-() 80103DBC 00219060-() 0021E030 006B101C-() 006B564C award_item_name
F866 ------------ -------- ------------ -------- 8C16C6F0-() 8C170F8C 00590DD0-() 00595BE0 80242F44-() 8023C534 801F2A10-() 801EC31C 80109CA8-() 80103D8C 00219060-() 0021E080 006B101C-() 006B5668 award_item_select
F867 ------------ -------- ------------ -------- 8C16C704-B 8C170FB8 00590DE0-B 00595C00 80242EF8-B 8023C470 801F2988-B 801EC25C 80109C20-B 80103D88 00219090-B 0021E0D0 006B1040-B 006B5684 award_item_give_to
F868 ------------ -------- ------------ -------- 8C16C730-BB 8C17100C 00590E10-BB 00595C60 80242EA0-BB 8023C3A0 801F2930-BB 801EC188 80109BC8-BB 80103D84 002190C0-BB 0021E190 006B1058-BB 006B56D8 set_cmode_rank
F869 ------------ -------- ------------ -------- 8C16C730-BB 8C171104 00590E10-BB 00595D90 80242EA0-BB 8023C1AC 801F2930-BB 801EBF90 80109BC8-BB 80103C94 002190C0-BB 0021E4E0 006B1058-BB 006B57E0 check_rank_time
F86A ------------ -------- ------------ -------- 8C16C730-BB 8C1711C0 00590E10-BB 00595E80 80242EA0-BB 8023C084 801F2930-BB 801EBE70 80109BC8-BB 80103C90 002190C0-BB 0021E5C0 006B1058-BB 006B5894 item_create_cmode
F86B ------------ -------- ------------ -------- 8C16C704-B 8C171264 00590DE0-B 00595F70 80242EF8-B 8023C058 801F2988-B 801EBE44 80109C20-B 80103C64 00219090-B 0021E6A0 006B1040-B 006B5940 ba_set_box_drop_area
F86C ------------ -------- ------------ -------- 8C16C704-B 8C17127C 00590DE0-B 00595F90 80242EF8-B 8023C008 801F2988-B 801EBDF4 80109C20-B 80103C14 00219090-B 0021E6C0 006B1040-B 006B5954 award_item_ok
F86D ------------ -------- ------------ -------- 8C16C6F0-() 8C1712C4 00590DD0-() 00595FD0 80242F44-() 8023BFFC 801F2A10-() 801EBDD0 80109CA8-() 80103BF0 00219060-() 0021E720 006B101C-() 006B5988 ba_set_trapself
F86E ------------ -------- ------------ -------- 8C16C6F0-() 8C1712D0 00590DD0-() 00595FE0 80242F44-() 8023BFF0 801F2A10-() 801EBDAC 80109CA8-() 80103BCC 00219060-() 0021E730 006B101C-() 006B5994 ba_clear_trapself
F86F ------------ -------- ------------ -------- 8C16C7C8-L 8C170BEC 00590EA0-L 005957F0 80242DA8-L 8023CA34 801F29D0-... 801EC938 80109C68-... 801042D4 00219070-... 0021DC00 006B1028-... 006B528C ba_set_lives
DC-NTE--------------- DCv1----------------- DCv2----------------- PC------------------- GC1&2NTE------------- GC1&2v11------------- GCEp3USA------------- XBOX-EXE------------- BB-------------------
F870 ------------ -------- ------------ -------- 8C16C7C8-L 8C170C00 00590EA0-L 00595810 80242DA8-L 8023CA0C 801F29D0-... 801EC90C 80109C68-... 801042A8 00219070-... 0021DC20 006B1028-... 006B52A0
F871 ------------ -------- ------------ -------- 8C16C7C8-L 8C170C18 00590EA0-L 00595830 80242DA8-L 8023C9C8 801F29D0-... 801EC8C4 80109C68-... 80104260 00219070-... 0021DC40 006B1028-... 006B52BC
F872 ------------ -------- ------------ -------- 8C16C7C8-L 8C170C30 00590EA0-L 00595850 80242DA8-L 8023C9A4 801F29D0-... 801EC894 80109C68-... 80104230 00219070-... 0021DC60 006B1028-... 006B52D8
F873 ------------ -------- ------------ -------- 8C16C704-B 8C1712DC 00590DE0-B 00595FF0 80242EF8-B 8023BF98 801F2988-B 801EBD54 80109C20-B 80103BC8 00219090-B 0021E740 006B1040-B 006B59A0
F874 ------------ -------- ------------ -------- 8C16CC88-LS 8C171314 00591320-LS 00596030 80242514-LS 8023BEC0 801F29D0-... 801EBC80 80109C68-... 80103BC4 00219070-... 0021E790 006B1028-... 006B59CC
F875 ------------ -------- ------------ -------- 8C16C704-B 8C1713BC 00590DE0-B 005960C0 80242EF8-B 8023BE70 801F2988-B 801EBC30 80109C20-B 80103B74 00219090-B 0021E860 006B1040-B 006B5ADC
F876 ------------ -------- ------------ -------- 8C16C704-B 8C171404 00590DE0-B 00596100 80242EF8-B 8023BE20 801F2988-B 801EBBE0 80109C20-B 80103B24 00219090-B 0021E8B0 006B1040-B 006B5B0C
F877 ------------ -------- ------------ -------- 8C16C704-B 8C17144C 00590DE0-B 00596140 80242EF8-B 8023BDE0 801F2988-B 801EBBA4 80109C20-B 80103AE8 00219090-B 0021E900 006B1040-B 006B5B3C
F878 ------------ -------- ------------ -------- 8C16C704-B 8C171480 00590DE0-B 00596170 80242EF8-B 8023BDA0 801F2988-B 801EBB68 80109C20-B 80103A84 00219090-B 0021E940 006B1040-B 006B5B64
F879 ------------ -------- ------------ -------- 8C16C730-BB 8C1714B8 00590E10-BB 005961A0 80242EA0-BB 8023BD00 801F2930-BB 801EBAC8 80109BC8-BB 801039E4 002190C0-BB 0021E980 006B1058-BB 006B5B8C
F87A ------------ -------- ------------ -------- 8C16C730-BB 8C171530 00590E10-BB 00596230 80242EA0-BB 8023BBC0 801F2930-BB 801EB988 80109BC8-BB 801038A4 002190C0-BB 0021E9F0 006B1058-BB 006B5BF8
F87B ------------ -------- ------------ -------- 8C16C730-BB 8C171624 00590E10-BB 00596340 80242EA0-BB 8023BB20 801F2930-BB 801EB8E8 80109BC8-BB 80103804 002190C0-BB 0021EAB0 006B1058-BB 006B5CB8
F87C ------------ -------- ------------ -------- 8C16C704-B 8C171698 00590DE0-B 005963F0 80242EF8-B 8023BAE8 801F2988-B 801EB88C 80109C20-B 801037A8 00219090-B 0021EB30 006B1040-B 006B5D60
F87D ------------ -------- ------------ -------- 8C16C704-B 8C1716CC 00590DE0-B 00596410 80242EF8-B 8023BAB0 801F2988-B 801EB854 80109C20-B 80103770 00219090-B 0021EB60 006B1040-B 006B5D88
F87E ------------ -------- ------------ -------- 8C16C704-B 8C1716FC 00590DE0-B 00596440 80242EF8-B 8023BA5C 801F2988-B 801EB800 80109C20-B 8010371C 00219090-B 0021EBC0 006B1040-B 006B5DA8
F87F ------------ -------- ------------ -------- 8C16C730-BB 8C171740 00590E10-BB 00596480 80242EA0-BB 8023BA20 801F2930-BB 801EB79C 80109BC8-BB 801036B8 002190C0-BB 0021EC40 006B1058-BB 006B5DD0
F870 ------------ -------- ------------ -------- 8C16C7C8-L 8C170C00 00590EA0-L 00595810 80242DA8-L 8023CA0C 801F29D0-... 801EC90C 80109C68-... 801042A8 00219070-... 0021DC20 006B1028-... 006B52A0 ba_set_max_tech_level
F871 ------------ -------- ------------ -------- 8C16C7C8-L 8C170C18 00590EA0-L 00595830 80242DA8-L 8023C9C8 801F29D0-... 801EC8C4 80109C68-... 80104260 00219070-... 0021DC40 006B1028-... 006B52BC ba_set_char_level
F872 ------------ -------- ------------ -------- 8C16C7C8-L 8C170C30 00590EA0-L 00595850 80242DA8-L 8023C9A4 801F29D0-... 801EC894 80109C68-... 80104230 00219070-... 0021DC60 006B1028-... 006B52D8 ba_set_time_limit
F873 ------------ -------- ------------ -------- 8C16C704-B 8C1712DC 00590DE0-B 00595FF0 80242EF8-B 8023BF98 801F2988-B 801EBD54 80109C20-B 80103BC8 00219090-B 0021E740 006B1040-B 006B59A0 dark_falz_is_dead
F874 ------------ -------- ------------ -------- 8C16CC88-LS 8C171314 00591320-LS 00596030 80242514-LS 8023BEC0 801F29D0-... 801EBC80 80109C68-... 80103BC4 00219070-... 0021E790 006B1028-... 006B59CC set_cmode_rank_override
F875 ------------ -------- ------------ -------- 8C16C704-B 8C1713BC 00590DE0-B 005960C0 80242EF8-B 8023BE70 801F2988-B 801EBC30 80109C20-B 80103B74 00219090-B 0021E860 006B1040-B 006B5ADC enable_stealth_suit_effect
F876 ------------ -------- ------------ -------- 8C16C704-B 8C171404 00590DE0-B 00596100 80242EF8-B 8023BE20 801F2988-B 801EBBE0 80109C20-B 80103B24 00219090-B 0021E8B0 006B1040-B 006B5B0C disable_stealth_suit_effect
F877 ------------ -------- ------------ -------- 8C16C704-B 8C17144C 00590DE0-B 00596140 80242EF8-B 8023BDE0 801F2988-B 801EBBA4 80109C20-B 80103AE8 00219090-B 0021E900 006B1040-B 006B5B3C enable_techs
F878 ------------ -------- ------------ -------- 8C16C704-B 8C171480 00590DE0-B 00596170 80242EF8-B 8023BDA0 801F2988-B 801EBB68 80109C20-B 80103A84 00219090-B 0021E940 006B1040-B 006B5B64 disable_techs
F879 ------------ -------- ------------ -------- 8C16C730-BB 8C1714B8 00590E10-BB 005961A0 80242EA0-BB 8023BD00 801F2930-BB 801EBAC8 80109BC8-BB 801039E4 002190C0-BB 0021E980 006B1058-BB 006B5B8C get_gender
F87A ------------ -------- ------------ -------- 8C16C730-BB 8C171530 00590E10-BB 00596230 80242EA0-BB 8023BBC0 801F2930-BB 801EB988 80109BC8-BB 801038A4 002190C0-BB 0021E9F0 006B1058-BB 006B5BF8 get_chara_class
F87B ------------ -------- ------------ -------- 8C16C730-BB 8C171624 00590E10-BB 00596340 80242EA0-BB 8023BB20 801F2930-BB 801EB8E8 80109BC8-BB 80103804 002190C0-BB 0021EAB0 006B1058-BB 006B5CB8 take_slot_meseta
F87C ------------ -------- ------------ -------- 8C16C704-B 8C171698 00590DE0-B 005963F0 80242EF8-B 8023BAE8 801F2988-B 801EB88C 80109C20-B 801037A8 00219090-B 0021EB30 006B1040-B 006B5D60 get_guild_card_file_creation_time
F87D ------------ -------- ------------ -------- 8C16C704-B 8C1716CC 00590DE0-B 00596410 80242EF8-B 8023BAB0 801F2988-B 801EB854 80109C20-B 80103770 00219090-B 0021EB60 006B1040-B 006B5D88 kill_player
F87E ------------ -------- ------------ -------- 8C16C704-B 8C1716FC 00590DE0-B 00596440 80242EF8-B 8023BA5C 801F2988-B 801EB800 80109C20-B 8010371C 00219090-B 0021EBC0 006B1040-B 006B5DA8 get_serial_number
F87F ------------ -------- ------------ -------- 8C16C730-BB 8C171740 00590E10-BB 00596480 80242EA0-BB 8023BA20 801F2930-BB 801EB79C 80109BC8-BB 801036B8 002190C0-BB 0021EC40 006B1058-BB 006B5DD0 get_eventflag
DC-NTE--------------- DCv1----------------- DCv2----------------- PC------------------- GC1&2NTE------------- GC1&2v11------------- GCEp3USA------------- XBOX-EXE------------- BB-------------------
F880 ------------ -------- ------------ -------- 8C16C704-B 8C171780 00590DE0-B 005964C0 80242EF8-B 8023B9A0 801F2988-B 801EB708 80109C20-B 80103624 00219090-B 0021EC80 006B1040-B 006B5E04
F881 ------------ -------- ------------ -------- 8C16C704-B 8C1717BC 00590DE0-B 00596500 80242EF8-B 8023B914 801F2988-B 801EB67C 80109C20-B 80103598 00219090-B 0021ECB0 006B1040-B 006B5E30
F882 ------------ -------- ------------ -------- 8C16C704-B 8C17181C 00590DE0-B 00596560 80242EF8-B 8023B890 801F2988-B 801EB5F8 80109C20-B 80103514 00219090-B 0021ED20 006B1040-B 006B5E84
F883 ------------ -------- ------------ -------- 8C16C730-BB 8C171060 00590E10-BB 00595CE0 80242EA0-BB 8023C2BC 801F2930-BB 801EC09C 80109BC8-BB 80103C98 002190C0-BB 0021E210 006B1058-BB 006B572C
F884 ------------ -------- ------------ -------- 8C16C928-LB 8C171884 00591000-LB 005965D0 80242B10-LB 8023B83C 801F29D0-... 801EB598 80109C68-... 801034B4 00219070-... 0021EDC0 006B1028-... 006B5EDC
F885 ------------ -------- ------------ -------- 8C16C928-LB 8C1718D0 00591000-LB 00596610 80242B10-LB 8023B7E8 801F29D0-... 801EB448 80109C68-... 80103364 00219070-... 0021EEB0 006B1028-... 006B5F8C
F886 ------------ -------- ------------ -------- 8C16C730-BB 8C17191C 00590E10-BB 00596640 80242EA0-BB 8023B7A4 801F2930-BB 801EB404 80109BC8-BB 80103360 002190C0-BB 0021EEE0 006B1058-BB 006B5FBC
F887 ------------ -------- ------------ -------- 8C16C730-BB 8C171954 00590E10-BB 00596690 80242EA0-BB 8023B764 801F2930-BB 801EB3C4 80109BC8-BB 8010335C 002190C0-BB 0021EF20 006B1058-BB 006B5FDC
F888 ------------ -------- ------------ -------- 8C16C6F0-() 8C171988 00590DD0-() 005966D0 80242F44-() 8023B758 801F2A10-() 801EB3B8 80109CA8-() 80103358 00219060-() 0021EF50 006B101C-() 006B5FFC
F889 ------------ -------- ------------ -------- 8C16C6F0-() 8C171994 00590DD0-() 005966E0 80242F44-() 8023B74C 801F2A10-() 801EB3AC 80109CA8-() 80103354 00219060-() 0021EF60 006B101C-() 006B6008
F88A ------------ -------- ------------ -------- 8C16C730-BB 8C1719A0 00590E10-BB 005966F0 80242EA0-BB 8023B594 801F2930-BB 801EB19C 80109BC8-BB 80102F90 002190C0-BB 0021EF70 006B1058-BB 006B6014
F88B ------------ -------- ------------ -------- 8C16CD18-BS 8C171BB0 00591400-BS 005968B0 80242404-BS 8023B434 801F29D0-... 801EB04C 80109C68-... 80102E40 00219070-... 0021F0B0 006B1028-... 006B6218
F88C ------------ -------- ------------ -------- 8C16C704-B 8C171CC0 00590DE0-B 00596980 80242EF8-B 8023B420 801F2988-B 801EB038 80109C20-B 80102E2C 00219090-B 0021F1C0 006B1040-B 006B635C
F88D ------------ -------- ------------ -------- 8C16C704-B 8C171CD4 00590DE0-B 005969A0 80242EF8-B 8023B3C8 801F2988-B 801EAFE0 80109C20-B 80102E28 00219090-B 0021F1E0 006B1058-BB 006B6370
F88E ------------ -------- ------------ -------- 8C16C704-B 8C171CEC 00590DE0-B 005969C0 80242EF8-B 8023B364 801F2988-B 801EAF88 80109C20-B 80102E24 00219090-B 0021F250 006B1040-B 006B6390
F88F ------------ -------- ------------ -------- 8C16C704-B 8C171D14 00590DE0-B 005969E0 80242EF8-B 8023B0FC 801F2988-B 801EAD18 80109C20-B 80102E20 00219090-B 0021F290 006B1040-B 006B63A4
F880 ------------ -------- ------------ -------- 8C16C704-B 8C171780 00590DE0-B 005964C0 80242EF8-B 8023B9A0 801F2988-B 801EB708 80109C20-B 80103624 00219090-B 0021EC80 006B1040-B 006B5E04 set_trap_damage
F881 ------------ -------- ------------ -------- 8C16C704-B 8C1717BC 00590DE0-B 00596500 80242EF8-B 8023B914 801F2988-B 801EB67C 80109C20-B 80103598 00219090-B 0021ECB0 006B1040-B 006B5E30 get_pl_name
F882 ------------ -------- ------------ -------- 8C16C704-B 8C17181C 00590DE0-B 00596560 80242EF8-B 8023B890 801F2988-B 801EB5F8 80109C20-B 80103514 00219090-B 0021ED20 006B1040-B 006B5E84 get_pl_job
F883 ------------ -------- ------------ -------- 8C16C730-BB 8C171060 00590E10-BB 00595CE0 80242EA0-BB 8023C2BC 801F2930-BB 801EC09C 80109BC8-BB 80103C98 002190C0-BB 0021E210 006B1058-BB 006B572C get_player_proximity
F884 ------------ -------- ------------ -------- 8C16C928-LB 8C171884 00591000-LB 005965D0 80242B10-LB 8023B83C 801F29D0-... 801EB598 80109C68-... 801034B4 00219070-... 0021EDC0 006B1028-... 006B5EDC set_eventflag16
F885 ------------ -------- ------------ -------- 8C16C928-LB 8C1718D0 00591000-LB 00596610 80242B10-LB 8023B7E8 801F29D0-... 801EB448 80109C68-... 80103364 00219070-... 0021EEB0 006B1028-... 006B5F8C set_eventflag32
F886 ------------ -------- ------------ -------- 8C16C730-BB 8C17191C 00590E10-BB 00596640 80242EA0-BB 8023B7A4 801F2930-BB 801EB404 80109BC8-BB 80103360 002190C0-BB 0021EEE0 006B1058-BB 006B5FBC ba_get_place
F887 ------------ -------- ------------ -------- 8C16C730-BB 8C171954 00590E10-BB 00596690 80242EA0-BB 8023B764 801F2930-BB 801EB3C4 80109BC8-BB 8010335C 002190C0-BB 0021EF20 006B1058-BB 006B5FDC ba_get_score
F888 ------------ -------- ------------ -------- 8C16C6F0-() 8C171988 00590DD0-() 005966D0 80242F44-() 8023B758 801F2A10-() 801EB3B8 80109CA8-() 80103358 00219060-() 0021EF50 006B101C-() 006B5FFC enable_win_pfx
F889 ------------ -------- ------------ -------- 8C16C6F0-() 8C171994 00590DD0-() 005966E0 80242F44-() 8023B74C 801F2A10-() 801EB3AC 80109CA8-() 80103354 00219060-() 0021EF60 006B101C-() 006B6008 disable_win_pfx
F88A ------------ -------- ------------ -------- 8C16C730-BB 8C1719A0 00590E10-BB 005966F0 80242EA0-BB 8023B594 801F2930-BB 801EB19C 80109BC8-BB 80102F90 002190C0-BB 0021EF70 006B1058-BB 006B6014 get_player_status
F88B ------------ -------- ------------ -------- 8C16CD18-BS 8C171BB0 00591400-BS 005968B0 80242404-BS 8023B434 801F29D0-... 801EB04C 80109C68-... 80102E40 00219070-... 0021F0B0 006B1028-... 006B6218 send_mail
F88C ------------ -------- ------------ -------- 8C16C704-B 8C171CC0 00590DE0-B 00596980 80242EF8-B 8023B420 801F2988-B 801EB038 80109C20-B 80102E2C 00219090-B 0021F1C0 006B1040-B 006B635C get_game_version
F88D ------------ -------- ------------ -------- 8C16C704-B 8C171CD4 00590DE0-B 005969A0 80242EF8-B 8023B3C8 801F2988-B 801EAFE0 80109C20-B 80102E28 00219090-B 0021F1E0 006B1058-BB 006B6370 chl_set_timerecord
F88E ------------ -------- ------------ -------- 8C16C704-B 8C171CEC 00590DE0-B 005969C0 80242EF8-B 8023B364 801F2988-B 801EAF88 80109C20-B 80102E24 00219090-B 0021F250 006B1040-B 006B6390 chl_get_timerecord
F88F ------------ -------- ------------ -------- 8C16C704-B 8C171D14 00590DE0-B 005969E0 80242EF8-B 8023B0FC 801F2988-B 801EAD18 80109C20-B 80102E20 00219090-B 0021F290 006B1040-B 006B63A4 set_cmode_grave_rates
DC-NTE--------------- DCv1----------------- DCv2----------------- PC------------------- GC1&2NTE------------- GC1&2v11------------- GCEp3USA------------- XBOX-EXE------------- BB-------------------
F890 ------------ -------- ------------ -------- 8C16C6F0-() 8C16F2DC 00590DD0-() 005939B0 80242F44-() 8023F1B4 801F2A10-() 801EF18C 80109CA8-() 801062F8 00219060-() 0021BD80 006B101C-() 006B3A58
F891 ------------ -------- ------------ -------- 8C16C7C8-L 8C171ED0 00590EA0-L 00596B80 80242DA8-L 8023AFE8 801F29D0-... 801EABEC 80109C68-... 80102CF4 00219070-... 0021F420 006B1028-... 006B64C8
F892 ------------ -------- ------------ -------- 8C16C97C-W 8C171EDC 00591040-W 00596B90 80242A98-W 8023AF18 801F2848-W 801EAB88 80109AE0-W 80102C90 00219370-W 0021F430 006B10E0-W 006B64D8
F893 ------------ -------- ------------ -------- 8C16C97C-W 8C171F0C 00591040-W 00596BC0 80242A98-W 8023AE38 801F2848-W 801EAB2C 80109AE0-W 80102C34 00219370-W 0021F480 006B10E0-W 006B657C
F894 ------------ -------- ------------ -------- 8C16C97C-W 8C171F3C 00591040-W 00596BF0 80242A98-W 8023AD80 801F2848-W 801EAAD0 80109AE0-W 80102BD8 00219370-W 0021F4C0 006B10E0-W 006B6638
F895 ------------ -------- ------------ -------- 8C16C97C-W 8C171F6C 00591040-W 00596C20 80242A98-W 8023ACE8 801F2848-W 801EAA74 80109AE0-W 80102B28 00219370-W 0021F510 006B10E0-W 006B66C4
F896 ------------ -------- ------------ -------- 8C16C730-BB 8C171F9C 00590E10-BB 00596C50 80242EA0-BB 8023AC6C 801F2930-BB 801EA9F4 80109BC8-BB 80102AA8 002190C0-BB 0021F550 006B1058-BB 006B674C
F897 ------------ -------- ------------ -------- 8C16C730-BB 8C172000 00590E10-BB 00596CA0 80242EA0-BB 8023ABF0 801F2930-BB 801EA974 80109BC8-BB 80102A28 002190C0-BB 0021F590 006B1058-BB 006B6798
F898 ------------ -------- ------------ -------- 8C16C730-BB 8C172064 00590E10-BB 00596CF0 80242EA0-BB 8023ABD0 801F2930-BB 801EA954 80109BC8-BB 80102A08 002190C0-BB 0021F5D0 006B1058-BB 006B67E4
F899 ------------ -------- ------------ -------- 8C16C730-BB 8C172084 00590E10-BB 00596D20 80242EA0-BB 8023ABB0 801F2930-BB 801EA934 80109BC8-BB 801029E8 002190C0-BB 0021F5F0 006B1058-BB 006B6804
F89A ------------ -------- ------------ -------- 8C16C730-BB 8C1720A4 00590E10-BB 00596D50 80242EA0-BB 8023AB0C 801F2930-BB 801EA88C 80109BC8-BB 80102940 002190C0-BB 0021F610 006B1058-BB 006B6824
F89B ------------ -------- ------------ -------- 8C16C6F0-() 8C172100 00590DD0-() 00596DB0 80242F44-() 8023AA9C 801F2A10-() 801EA828 80109CA8-() 801028F4 00219060-() 0021F670 006B101C-() 006B6898
F89C ------------ -------- ------------ -------- 8C16C704-B 8C172178 00590DE0-B 00596DF0 80242EF8-B 8023A9F4 801F2988-B 801EA78C 80109C20-B 801028F0 00219090-B 0021F6E0 006B1040-B 006B68D8
F89D ------------ -------- ------------ -------- 8C16C6F0-() 8C1721CC 00590DD0-() 00596E50 80242F44-() 8023A9B4 801F2A10-() 801EA758 80109CA8-() 801028EC 00219060-() 0021F770 006B101C-() 006B6924
F89E ------------ -------- ------------ -------- 8C16C7C8-L 8C1721D8 00590EA0-L 00596E60 80242DA8-L 8023A990 801F29D0-... 801EA728 80109C68-... 801028BC 00219070-... 0021F7E0 006B1028-... 006B692C
F89F ------------ -------- ------------ -------- 8C16C704-B 8C1721F8 00590DE0-B 00596E80 80242EF8-B 8023A948 801F2988-B 801EA6E0 80109C20-B 80102874 00219090-B 0021F800 006B1040-B 006B6948
F890 ------------ -------- ------------ -------- 8C16C6F0-() 8C16F2DC 00590DD0-() 005939B0 80242F44-() 8023F1B4 801F2A10-() 801EF18C 80109CA8-() 801062F8 00219060-() 0021BD80 006B101C-() 006B3A58 clear_mainwarp_all
F891 ------------ -------- ------------ -------- 8C16C7C8-L 8C171ED0 00590EA0-L 00596B80 80242DA8-L 8023AFE8 801F29D0-... 801EABEC 80109C68-... 80102CF4 00219070-... 0021F420 006B1028-... 006B64C8 load_enemy_data
F892 ------------ -------- ------------ -------- 8C16C97C-W 8C171EDC 00591040-W 00596B90 80242A98-W 8023AF18 801F2848-W 801EAB88 80109AE0-W 80102C90 00219370-W 0021F430 006B10E0-W 006B64D8 get_physical_data
F893 ------------ -------- ------------ -------- 8C16C97C-W 8C171F0C 00591040-W 00596BC0 80242A98-W 8023AE38 801F2848-W 801EAB2C 80109AE0-W 80102C34 00219370-W 0021F480 006B10E0-W 006B657C get_attack_data
F894 ------------ -------- ------------ -------- 8C16C97C-W 8C171F3C 00591040-W 00596BF0 80242A98-W 8023AD80 801F2848-W 801EAAD0 80109AE0-W 80102BD8 00219370-W 0021F4C0 006B10E0-W 006B6638 get_resist_data
F895 ------------ -------- ------------ -------- 8C16C97C-W 8C171F6C 00591040-W 00596C20 80242A98-W 8023ACE8 801F2848-W 801EAA74 80109AE0-W 80102B28 00219370-W 0021F510 006B10E0-W 006B66C4 get_movement_data
F896 ------------ -------- ------------ -------- 8C16C730-BB 8C171F9C 00590E10-BB 00596C50 80242EA0-BB 8023AC6C 801F2930-BB 801EA9F4 80109BC8-BB 80102AA8 002190C0-BB 0021F550 006B1058-BB 006B674C get_eventflag16
F897 ------------ -------- ------------ -------- 8C16C730-BB 8C172000 00590E10-BB 00596CA0 80242EA0-BB 8023ABF0 801F2930-BB 801EA974 80109BC8-BB 80102A28 002190C0-BB 0021F590 006B1058-BB 006B6798 get_eventflag32
F898 ------------ -------- ------------ -------- 8C16C730-BB 8C172064 00590E10-BB 00596CF0 80242EA0-BB 8023ABD0 801F2930-BB 801EA954 80109BC8-BB 80102A08 002190C0-BB 0021F5D0 006B1058-BB 006B67E4 shift_left
F899 ------------ -------- ------------ -------- 8C16C730-BB 8C172084 00590E10-BB 00596D20 80242EA0-BB 8023ABB0 801F2930-BB 801EA934 80109BC8-BB 801029E8 002190C0-BB 0021F5F0 006B1058-BB 006B6804 shift_right
F89A ------------ -------- ------------ -------- 8C16C730-BB 8C1720A4 00590E10-BB 00596D50 80242EA0-BB 8023AB0C 801F2930-BB 801EA88C 80109BC8-BB 80102940 002190C0-BB 0021F610 006B1058-BB 006B6824 get_random
F89B ------------ -------- ------------ -------- 8C16C6F0-() 8C172100 00590DD0-() 00596DB0 80242F44-() 8023AA9C 801F2A10-() 801EA828 80109CA8-() 801028F4 00219060-() 0021F670 006B101C-() 006B6898 reset_map
F89C ------------ -------- ------------ -------- 8C16C704-B 8C172178 00590DE0-B 00596DF0 80242EF8-B 8023A9F4 801F2988-B 801EA78C 80109C20-B 801028F0 00219090-B 0021F6E0 006B1040-B 006B68D8 disp_chl_retry_menu
F89D ------------ -------- ------------ -------- 8C16C6F0-() 8C1721CC 00590DD0-() 00596E50 80242F44-() 8023A9B4 801F2A10-() 801EA758 80109CA8-() 801028EC 00219060-() 0021F770 006B101C-() 006B6924 chl_reverser
F89E ------------ -------- ------------ -------- 8C16C7C8-L 8C1721D8 00590EA0-L 00596E60 80242DA8-L 8023A990 801F29D0-... 801EA728 80109C68-... 801028BC 00219070-... 0021F7E0 006B1028-... 006B692C ba_forbid_scape_dolls
F89F ------------ -------- ------------ -------- 8C16C704-B 8C1721F8 00590DE0-B 00596E80 80242EF8-B 8023A948 801F2988-B 801EA6E0 80109C20-B 80102874 00219090-B 0021F800 006B1040-B 006B6948 player_recovery
DC-NTE--------------- DCv1----------------- DCv2----------------- PC------------------- GC1&2NTE------------- GC1&2v11------------- GCEp3USA------------- XBOX-EXE------------- BB-------------------
F8A0 ------------ -------- ------------ -------- 8C16C6F0-() 8C172234 00590DD0-() 00596EC0 80242F44-() 8023A900 801F2A10-() 801EA6A4 80109CA8-() 80102870 00219060-() 0021F840 006B101C-() 006B6974
F8A1 ------------ -------- ------------ -------- 8C16C6F0-() 8C172240 00590DD0-() 00596ED0 80242F44-() 8023A8B8 801F2A10-() 801EA668 80109CA8-() 8010286C 00219060-() 0021F890 006B101C-() 006B6980
+56
View File
@@ -0,0 +1,56 @@
#include "AFSArchive.hh"
#include <stdio.h>
#include <string.h>
#include <phosg/Encoding.hh>
#include <phosg/Filesystem.hh>
#include <phosg/Strings.hh>
using namespace std;
AFSArchive::AFSArchive(std::shared_ptr<const std::string> data)
: data(data) {
struct FileHeader {
be_uint32_t magic;
le_uint32_t num_files;
} __attribute__((packed));
struct FileEntry {
le_uint32_t offset;
le_uint32_t size;
} __attribute__((packed));
StringReader r(*this->data);
const auto& header = r.get<FileHeader>();
if (header.magic != 0x41465300) {
throw runtime_error("file is not an AFS archive");
}
while (this->entries.size() < header.num_files) {
const auto& entry = r.get<FileEntry>();
this->entries.emplace_back(Entry{.offset = entry.offset, .size = entry.size});
}
}
std::pair<const void*, size_t> AFSArchive::get(size_t index) const {
const auto& entry = this->entries.at(index);
if (entry.offset > this->data->size()) {
throw out_of_range("entry begins beyond end of archive");
}
if (entry.offset + entry.size > this->data->size()) {
throw out_of_range("entry extends beyond end of archive");
}
return make_pair(this->data->data() + entry.offset, entry.size);
}
std::string AFSArchive::get_copy(size_t index) const {
auto ret = this->get(index);
return string(reinterpret_cast<const char*>(ret.first), ret.second);
}
StringReader AFSArchive::get_reader(size_t index) const {
auto ret = this->get(index);
return StringReader(ret.first, ret.second);
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <stdint.h>
#include <memory>
#include <phosg/Filesystem.hh>
#include <phosg/Strings.hh>
#include <string>
#include <unordered_map>
class AFSArchive {
public:
AFSArchive(std::shared_ptr<const std::string> data);
~AFSArchive() = default;
struct Entry {
uint64_t offset;
uint32_t size;
};
inline const std::vector<Entry>& all_entries() const {
return this->entries;
}
std::pair<const void*, size_t> get(size_t index) const;
std::string get_copy(size_t index) const;
StringReader get_reader(size_t index) const;
private:
std::shared_ptr<const std::string> data;
std::vector<Entry> entries;
};
+14 -15
View File
@@ -149,7 +149,7 @@ void Channel::disconnect() {
this->crypt_out.reset();
}
Channel::Message Channel::recv(bool print_contents) {
Channel::Message Channel::recv() {
struct evbuffer* buf = bufferevent_get_input(this->bev.get());
size_t header_size = (this->version == GameVersion::BB) ? 8 : 4;
@@ -204,7 +204,7 @@ Channel::Message Channel::recv(bool print_contents) {
}
command_data.resize(command_logical_size - header_size);
if (print_contents && (this->terminal_recv_color != TerminalFormat::END)) {
if (command_data_log.should_log(LogLevel::INFO) && (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);
}
@@ -241,11 +241,11 @@ Channel::Message Channel::recv(bool print_contents) {
};
}
void Channel::send(uint16_t cmd, uint32_t flag, bool print_contents) {
this->send(cmd, flag, nullptr, 0, print_contents);
void Channel::send(uint16_t cmd, uint32_t flag, bool silent) {
this->send(cmd, flag, nullptr, 0, silent);
}
void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<const void*, size_t>> blocks, bool print_contents) {
void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<const void*, size_t>> blocks, bool silent) {
if (!this->connected()) {
channel_exceptions_log.warning("Attempted to send command on closed channel; dropping data");
return;
@@ -331,7 +331,7 @@ void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<cons
}
send_data.resize(send_data_size, '\0');
if (print_contents && (this->terminal_send_color != TerminalFormat::END)) {
if (!silent && (command_data_log.should_log(LogLevel::INFO)) && (this->terminal_send_color != TerminalFormat::END)) {
if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) {
print_color_escape(stderr, TerminalFormat::FG_YELLOW, TerminalFormat::BOLD, TerminalFormat::END);
}
@@ -356,16 +356,15 @@ void Channel::send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<cons
evbuffer_add(buf, send_data.data(), send_data.size());
}
void Channel::send(
uint16_t cmd, uint32_t flag, const void* data, size_t size, bool print_contents) {
this->send(cmd, flag, {make_pair(data, size)}, print_contents);
void Channel::send(uint16_t cmd, uint32_t flag, const void* data, size_t size, bool silent) {
this->send(cmd, flag, {make_pair(data, size)}, silent);
}
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(uint16_t cmd, uint32_t flag, const string& data, bool silent) {
this->send(cmd, flag, data.data(), data.size(), silent);
}
void Channel::send(const void* data, size_t size, bool print_contents) {
void Channel::send(const void* data, size_t size, bool silent) {
size_t header_size = (this->version == GameVersion::BB) ? 8 : 4;
const auto* header = reinterpret_cast<const PSOCommandHeader*>(data);
this->send(
@@ -373,11 +372,11 @@ void Channel::send(const void* data, size_t size, bool print_contents) {
header->flag(this->version),
reinterpret_cast<const uint8_t*>(data) + header_size,
size - header_size,
print_contents);
silent);
}
void Channel::send(const string& data, bool print_contents) {
return this->send(data.data(), data.size(), print_contents);
void Channel::send(const string& data, bool silent) {
return this->send(data.data(), data.size(), silent);
}
void Channel::dispatch_on_input(struct bufferevent*, void* ctx) {
+10 -9
View File
@@ -75,22 +75,23 @@ struct Channel {
void disconnect();
// Receives a message. Throws std::out_of_range if no messages are available.
Message recv(bool print_contents = true);
Message recv();
// Sends a message with an automatically-constructed header.
void send(uint16_t cmd, uint32_t flag = 0, bool print_contents = true);
void send(uint16_t cmd, uint32_t flag, const void* data, size_t size, bool print_contents = true);
void send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<const void*, size_t>> blocks, bool print_contents = true);
void send(uint16_t cmd, uint32_t flag, const std::string& data, bool print_contents = true);
void send(uint16_t cmd, uint32_t flag = 0, bool silent = false);
void send(uint16_t cmd, uint32_t flag, const void* data, size_t size, bool silent = false);
void send(uint16_t cmd, uint32_t flag, const std::vector<std::pair<const void*, size_t>> blocks, bool silent = false);
void send(uint16_t cmd, uint32_t flag, const std::string& data, bool silent = false);
template <typename CmdT>
void send(uint16_t cmd, uint32_t flag, const CmdT& data) {
this->send(cmd, flag, &data, sizeof(data));
requires(!std::is_pointer_v<CmdT>)
void send(uint16_t cmd, uint32_t flag, const CmdT& data, bool silent = false) {
this->send(cmd, flag, &data, sizeof(data), silent);
}
// 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);
void send(const void* data, size_t size, bool silent = false);
void send(const std::string& data, bool silent = false);
private:
static void dispatch_on_input(struct bufferevent*, void* ctx);
+623 -344
View File
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -10,7 +10,5 @@
#include "ProxyServer.hh"
#include "ServerState.hh"
void on_chat_command(std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c, const std::u16string& text);
void on_chat_command(std::shared_ptr<ServerState> s,
ProxyServer::LinkedSession& session, const std::u16string& text);
void on_chat_command(std::shared_ptr<Client> c, const std::u16string& text);
void on_chat_command(std::shared_ptr<ProxyServer::LinkedSession> ses, const std::u16string& text);
+74 -4
View File
@@ -12,6 +12,7 @@
#include <phosg/Time.hh>
#include "Loggers.hh"
#include "Server.hh"
#include "Version.hh"
using namespace std;
@@ -37,15 +38,18 @@ ClientOptions::ClientOptions()
suppress_remote_login(false),
zero_remote_guild_card(false),
ep3_infinite_meseta(false),
ep3_infinite_time(false),
red_name(false),
blank_name(false),
function_call_return_value(-1) {}
Client::Client(
shared_ptr<Server> server,
struct bufferevent* bev,
GameVersion version,
ServerBehavior server_behavior)
: id(next_id++),
: server(server),
id(next_id++),
log(string_printf("[C-%" PRIX64 "] ", this->id), client_log.min_level),
bb_game_state(0),
flags(flags_for_version(version, -1)),
@@ -60,7 +64,6 @@ Client::Client(
x(0.0f),
z(0.0f),
area(0),
lobby_id(0),
lobby_client_id(0),
lobby_arrow_color(0),
preferred_lobby_id(-1),
@@ -69,11 +72,22 @@ Client::Client(
bufferevent_get_base(bev), -1, EV_TIMEOUT | EV_PERSIST,
&Client::dispatch_save_game_data, this),
event_free),
send_ping_event(
event_new(
bufferevent_get_base(bev), -1, EV_TIMEOUT,
&Client::dispatch_send_ping, this),
event_free),
idle_timeout_event(
event_new(
bufferevent_get_base(bev), -1, EV_TIMEOUT,
&Client::dispatch_idle_timeout, this),
event_free),
card_battle_table_number(-1),
card_battle_table_seat_number(0),
card_battle_table_seat_state(0),
next_exp_value(0),
can_chat(true),
use_server_rare_tables(false),
pending_bb_save_player_index(0),
dol_base_addr(0) {
this->last_switch_enabled_command.header.subcommand = 0;
@@ -83,6 +97,7 @@ Client::Client(
struct timeval tv = usecs_to_timeval(60000000); // 1 minute
event_add(this->save_game_data_event.get(), &tv);
}
this->reschedule_ping_and_timeout_events();
this->log.info("Created");
}
@@ -98,6 +113,13 @@ Client::~Client() {
this->log.info("Deleted");
}
void Client::reschedule_ping_and_timeout_events() {
struct timeval ping_tv = usecs_to_timeval(30000000); // 30 seconds
event_add(this->send_ping_event.get(), &ping_tv);
struct timeval idle_tv = usecs_to_timeval(60000000); // 1 minute
event_add(this->idle_timeout_event.get(), &idle_tv);
}
QuestScriptVersion Client::quest_version() const {
switch (this->version()) {
case GameVersion::DC:
@@ -127,14 +149,30 @@ QuestScriptVersion Client::quest_version() const {
}
}
void Client::set_license(shared_ptr<const License> l) {
void Client::set_license(shared_ptr<License> l) {
this->license = l;
this->game_data.guild_card_number = this->license->serial_number;
if (this->version() == GameVersion::BB) {
this->game_data.bb_username = this->license->username;
this->game_data.bb_username = this->license->bb_username;
}
}
shared_ptr<ServerState> Client::require_server_state() const {
auto server = this->server.lock();
if (!server) {
throw logic_error("server is deleted");
}
return server->get_state();
}
shared_ptr<Lobby> Client::require_lobby() const {
auto l = this->lobby.lock();
if (!l) {
throw runtime_error("client not in any lobby");
}
return l;
}
ClientConfig Client::export_config() const {
ClientConfig cc;
cc.magic = CLIENT_CONFIG_MAGIC;
@@ -186,3 +224,35 @@ void Client::save_game_data() {
this->game_data.save_player_data();
}
}
void Client::dispatch_send_ping(evutil_socket_t, short, void* ctx) {
reinterpret_cast<Client*>(ctx)->send_ping();
}
void Client::send_ping() {
if (this->version() != GameVersion::PATCH) {
this->log.info("Sending ping command");
// The game doesn't use this timestamp; we only use it for debugging purposes
be_uint64_t timestamp = now();
try {
this->channel.send(0x1D, 0x00, &timestamp, sizeof(be_uint64_t));
} catch (const exception& e) {
this->log.info("Failed to send ping: %s", e.what());
}
}
}
void Client::dispatch_idle_timeout(evutil_socket_t, short, void* ctx) {
reinterpret_cast<Client*>(ctx)->idle_timeout();
}
void Client::idle_timeout() {
this->log.info("Idle timeout expired");
auto s = this->server.lock();
if (s) {
auto c = this->shared_from_this();
s->disconnect_client(c);
} else {
this->log.info("Server is deleted; cannot disconnect client");
}
}
+44 -14
View File
@@ -20,15 +20,18 @@
extern const uint64_t CLIENT_CONFIG_MAGIC;
class Server;
struct Lobby;
struct ClientOptions {
// Options used on both game and proxy server
bool switch_assist;
bool infinite_hp;
bool infinite_tp;
bool debug;
int16_t override_section_id;
int16_t override_lobby_event;
int16_t override_lobby_number;
int16_t override_section_id; // -1 = no override
int16_t override_lobby_event; // -1 = no override
int16_t override_lobby_number; // -1 = no override
int64_t override_random_seed;
// Options used only on proxy server
@@ -40,6 +43,7 @@ struct ClientOptions {
bool suppress_remote_login;
bool zero_remote_guild_card;
bool ep3_infinite_meseta;
bool ep3_infinite_time;
bool red_name;
bool blank_name;
int64_t function_call_return_value; // -1 = don't block function calls
@@ -47,7 +51,7 @@ struct ClientOptions {
ClientOptions();
};
struct Client {
struct Client : public std::enable_shared_from_this<Client> {
enum Flag {
// Client is DC Network Trial Edition, which is missing a lot of features
// and uses some different command numbers than any other version
@@ -96,8 +100,8 @@ struct Client {
LOADING_QUEST = 0x00000040,
// Client is loading a joinable quest that has already started
LOADING_RUNNING_QUEST = 0x00100000,
// Client is waiting to join an Episode 3 card auction
AWAITING_CARD_AUCTION = 0x00010000,
// Client is waiting for other players to join a tournament game
LOADING_TOURNAMENT = 0x00010000,
// Client is in the information menu (login server only)
IN_INFORMATION_MENU = 0x00000080,
// Client is at the welcome message (login server only)
@@ -108,15 +112,20 @@ struct Client {
// Client has received newserv's Episode 3 card definitions, so don't send
// them again
HAS_EP3_CARD_DEFS = 0x00004000,
// Client has received newserv's Episode 3 media updates, so don't send them
// again
HAS_EP3_MEDIA_UPDATES = 0x00800000,
UNUSED_FLAG_BITS = 0xFF800000,
UNUSED_FLAG_BITS = 0xFF010000,
};
std::weak_ptr<Server> server;
std::weak_ptr<ServerState> server_state;
uint64_t id;
PrefixedLogger log;
// License & account
std::shared_ptr<const License> license;
std::shared_ptr<License> license;
// Note: these fields are included in the client config. On GC, the client
// config can be up to 0x20 bytes; on BB it can be 0x28 bytes. We don't use
@@ -143,23 +152,27 @@ struct Client {
ClientOptions options;
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
uint32_t area;
std::weak_ptr<Lobby> lobby;
uint8_t lobby_client_id;
uint8_t lobby_arrow_color;
int64_t preferred_lobby_id; // <0 = no preference
ClientGameData game_data;
std::unique_ptr<struct event, void (*)(struct event*)> save_game_data_event;
std::unique_ptr<struct event, void (*)(struct event*)> send_ping_event;
std::unique_ptr<struct event, void (*)(struct event*)> idle_timeout_event;
int16_t card_battle_table_number;
uint16_t card_battle_table_seat_number;
uint16_t card_battle_table_seat_state;
std::weak_ptr<Episode3::Tournament::Team> ep3_tournament_team;
std::shared_ptr<Episode3::BattleRecord> ep3_prev_battle_record;
std::shared_ptr<const Menu> last_menu_sent;
// Miscellaneous (used by chat commands)
uint32_t next_exp_value; // next EXP value to give
G_SwitchStateChanged_6x05 last_switch_enabled_command;
bool can_chat;
bool use_server_rare_tables;
std::string pending_bb_save_username;
uint8_t pending_bb_save_player_index;
std::deque<std::function<void(uint32_t, uint32_t)>> function_call_response_queue;
@@ -169,15 +182,28 @@ struct Client {
std::shared_ptr<DOLFileIndex::DOLFile> loading_dol_file;
std::unordered_map<std::string, std::shared_ptr<const std::string>> sending_files;
Client(struct bufferevent* bev, GameVersion version, ServerBehavior server_behavior);
Client(
std::shared_ptr<Server> server,
struct bufferevent* bev,
GameVersion version,
ServerBehavior server_behavior);
~Client();
void reschedule_ping_and_timeout_events();
inline uint8_t language() const {
auto p = this->game_data.player(true, false);
return p ? p->inventory.language : 1; // English by default
}
inline GameVersion version() const {
return this->channel.version;
}
QuestScriptVersion quest_version() const;
void set_license(std::shared_ptr<const License> l);
void set_license(std::shared_ptr<License> l);
std::shared_ptr<ServerState> require_server_state() const;
std::shared_ptr<Lobby> require_lobby() const;
ClientConfig export_config() const;
ClientConfigBB export_config_bb() const;
@@ -186,4 +212,8 @@ struct Client {
static void dispatch_save_game_data(evutil_socket_t, short, void* ctx);
void save_game_data();
static void dispatch_send_ping(evutil_socket_t, short, void* ctx);
void send_ping();
static void dispatch_idle_timeout(evutil_socket_t, short, void* ctx);
void idle_timeout();
};
+281 -243
View File
@@ -106,18 +106,18 @@ enum ClientStateBB : uint8_t {
};
struct ClientConfig {
uint64_t magic;
uint32_t flags;
uint32_t specific_version;
uint32_t proxy_destination_address;
uint16_t proxy_destination_port;
uint64_t magic = 0;
uint32_t flags = 0;
uint32_t specific_version = 0;
uint32_t proxy_destination_address = 0;
uint16_t proxy_destination_port = 0;
parray<uint8_t, 0x0A> unused;
} __packed__;
struct ClientConfigBB {
ClientConfig cfg;
uint8_t bb_game_state;
uint8_t bb_player_index;
uint8_t bb_game_state = 0;
uint8_t bb_player_index = 0;
parray<uint8_t, 0x06> unused;
} __packed__;
@@ -634,37 +634,42 @@ struct C_MenuItemInfoRequest_09 {
// command does not softlock, but instead does nothing because the 0E
// second-phase handler is missing.
template <typename CharT>
struct SC_MeetUserExtension {
struct LobbyReference {
le_uint32_t menu_id = 0;
le_uint32_t item_id = 0;
} __packed__;
parray<LobbyReference, 8> lobby_refs;
le_uint32_t unknown_a2 = 0;
ptext<CharT, 0x20> player_name;
} __packed__;
struct S_LegacyJoinGame_PC_0E {
struct UnknownA1 {
struct LobbyData {
le_uint32_t player_tag;
le_uint32_t guild_card_number;
parray<uint8_t, 0x10> unknown_a1;
parray<uint8_t, 0x10> name;
} __packed__;
parray<UnknownA1, 4> unknown_a1;
parray<LobbyData, 4> lobby_data;
parray<uint8_t, 0x20> unknown_a3;
} __packed__;
struct S_LegacyJoinGame_GC_0E {
PlayerLobbyDataDCGC lobby_data[4];
struct UnknownA0 {
parray<uint8_t, 2> unknown_a1;
le_uint16_t unknown_a2 = 0;
le_uint32_t unknown_a3 = 0;
} __packed__;
parray<UnknownA0, 8> unknown_a0;
le_uint32_t unknown_a1 = 0;
parray<uint8_t, 0x20> unknown_a2;
parray<PlayerLobbyDataDCGC, 4> lobby_data;
SC_MeetUserExtension<char> meet_user_extension;
parray<uint8_t, 4> unknown_a3;
} __packed__;
struct S_LegacyJoinGame_XB_0E {
struct UnknownA1 {
struct LobbyData {
le_uint32_t player_tag;
le_uint32_t guild_card_number;
parray<uint8_t, 0x18> unknown_a1;
parray<uint8_t, 0x18> name;
} __packed__;
parray<UnknownA1, 4> unknown_a1;
parray<uint8_t, 0x68> unknown_a2;
parray<LobbyData, 4> lobby_data;
SC_MeetUserExtension<char> meet_user_extension;
parray<uint8_t, 4> unknown_a3;
} __packed__;
// 0F: Invalid command
@@ -945,17 +950,6 @@ struct C_GuildCardSearch_40 {
// 41 (S->C): Guild card search result
// Internal name: RcvUserAns
template <typename CharT>
struct SC_MeetUserExtension {
struct LobbyReference {
le_uint32_t menu_id = 0;
le_uint32_t item_id = 0;
} __packed__;
parray<LobbyReference, 8> lobby_refs;
le_uint32_t unknown_a2 = 0;
ptext<CharT, 0x20> player_name;
} __packed__;
template <typename HeaderT, typename CharT>
struct S_GuildCardSearchResult {
le_uint32_t player_tag = 0x00010000;
@@ -1220,7 +1214,7 @@ struct C_CharacterData_BB_61_98 {
// Header flag = entry count
template <typename LobbyDataT>
struct S_JoinGame {
struct S_JoinGame_DC_PC {
// Note: It seems Sega servers sent uninitialized memory in the variations
// field when sending this command to start an Episode 3 tournament game. This
// can be misleading when reading old logs from those days, but the Episode 3
@@ -1239,15 +1233,6 @@ struct S_JoinGame {
uint8_t section_id = 0;
uint8_t challenge_mode = 0;
le_uint32_t rare_seed = 0;
// Note: The 64 command for PSO DC ends here (the next 4 fields are ignored).
// newserv sends them anyway for code simplicity reasons.
uint8_t episode = 0;
// Similarly, PSO GC ignores the values in the following fields.
uint8_t unused2 = 1; // Should be 1 for PSO PC?
// Note: Only BB uses this field; it's unused on all other versions (since
// only BB has solo mode).
uint8_t solo_mode = 0;
uint8_t unused3 = 0;
} __packed__;
struct S_JoinGame_DCNTE_64 {
@@ -1259,12 +1244,17 @@ struct S_JoinGame_DCNTE_64 {
parray<PlayerLobbyDataDCGC, 4> lobby_data;
} __packed__;
struct S_JoinGame_PC_64 : S_JoinGame<PlayerLobbyDataPC> {
struct S_JoinGame_DC_64 : S_JoinGame_DC_PC<PlayerLobbyDataDCGC> {
} __packed__;
struct S_JoinGame_DC_GC_64 : S_JoinGame<PlayerLobbyDataDCGC> {
struct S_JoinGame_PC_64 : S_JoinGame_DC_PC<PlayerLobbyDataPC> {
} __packed__;
struct S_JoinGame_GC_Ep3_64 : S_JoinGame_DC_GC_64 {
struct S_JoinGame_GC_64 : S_JoinGame_DC_PC<PlayerLobbyDataDCGC> {
uint8_t episode = 0;
parray<uint8_t, 3> unused;
} __packed__;
struct S_JoinGame_GC_Ep3_64 : S_JoinGame_GC_64 {
// This field is only present if the game (and client) is Episode 3. Similarly
// to lobby_data in the base struct, all four of these are always present and
// they are filled in in slot positions.
@@ -1274,11 +1264,17 @@ struct S_JoinGame_GC_Ep3_64 : S_JoinGame_DC_GC_64 {
} __packed__ players_ep3[4];
} __packed__;
struct S_JoinGame_XB_64 : S_JoinGame<PlayerLobbyDataXB> {
struct S_JoinGame_XB_64 : S_JoinGame_DC_PC<PlayerLobbyDataXB> {
uint8_t episode = 0;
parray<uint8_t, 3> unused;
parray<le_uint32_t, 6> unknown_a1;
} __packed__;
struct S_JoinGame_BB_64 : S_JoinGame<PlayerLobbyDataBB> {
struct S_JoinGame_BB_64 : S_JoinGame_DC_PC<PlayerLobbyDataBB> {
uint8_t episode = 0;
uint8_t unused1 = 1;
uint8_t solo_mode = 0;
uint8_t unused2 = 0;
} __packed__;
// 65 (S->C): Add player to game
@@ -1456,6 +1452,8 @@ struct S_GenerateID_DC_PC_V3_80 {
template <typename CharT>
struct SC_SimpleMail_81 {
// If player_tag and from_guild_card_number are zero, the message cannot be
// replied to.
le_uint32_t player_tag = 0x00010000;
le_uint32_t from_guild_card_number = 0;
ptext<CharT, 0x10> from_name;
@@ -2065,14 +2063,14 @@ struct C_SendQuestStatistic_V3_BB_AA {
parray<le_uint32_t, 8> params;
} __packed__;
// AB (S->C): Confirm quest statistic (V3/BB)
// AB (S->C): Call quest function (V3/BB)
// This command is not valid on PSO GC Episodes 1&2 Trial Edition.
// Upon receipt, the client starts a quest thread running the given function.
// Probably this is supposed to be one of the function IDs previously sent in
// the AA command, but the client does not check for this. The server can
// presumably use this command to call any function at any time during a quest.
struct S_ConfirmQuestStatistic_V3_BB_AB {
struct S_CallQuestFunction_V3_BB_AB {
le_uint16_t function_id;
parray<uint8_t, 2> unused;
} __packed__;
@@ -2204,10 +2202,13 @@ struct C_ExecuteCodeResult_B3 {
// B7 (S->C): Rank update (Episode 3)
struct S_RankUpdate_GC_Ep3_B7 {
// If rank is not zero, the client sets its rank text to "<rank>:<rank_text>",
// truncated to 11 characters. If rank is zero, the client uses rank_text
// without modifying it.
le_uint32_t rank = 0;
ptext<char, 0x0C> rank_text;
le_uint32_t meseta = 0;
le_uint32_t max_meseta = 0;
ptext<char, 0x0C> rank_text; // Encrypted (with encrypt_challenge_rank_text)
le_uint32_t current_meseta = 0;
le_uint32_t total_meseta_earned = 0;
le_uint32_t unlocked_jukebox_songs = 0xFFFFFFFF;
} __packed__;
@@ -2305,22 +2306,21 @@ struct S_UpdateMediaHeader_GC_Ep3_B9 {
// This command is not valid on Episode 3 Trial Edition.
// header.flag specifies the transaction purpose. Specific known values:
// 00 = unknown
// 01 = Lobby jukebox object created (C->S; always has a value of 0;
// response request_token must match the last token sent by client)
// 01 = Initialize Meseta subsystem (C->S; always has a value of 0)
// 02 = Spend meseta (at e.g. lobby jukebox or Pinz's shop) (C->S)
// 03 = Spend meseta response (S->C; request_token must match the last token
// sent by client)
// 04 = unknown (C->S; request_token must match the last token sent by client)
struct C_Meseta_GC_Ep3_BA {
struct C_MesetaTransaction_GC_Ep3_BA {
le_uint32_t transaction_num = 0;
le_uint32_t value = 0;
le_uint32_t request_token = 0;
} __packed__;
struct S_Meseta_GC_Ep3_BA {
le_uint32_t remaining_meseta = 0;
le_uint32_t total_meseta_awarded = 0;
struct S_MesetaTransaction_GC_Ep3_BA {
le_uint32_t current_meseta = 0;
le_uint32_t total_meseta_earned = 0;
le_uint32_t request_token = 0; // Should match the token sent by the client
} __packed__;
@@ -2328,7 +2328,7 @@ struct S_Meseta_GC_Ep3_BA {
// This command is not valid on Episode 3 Trial Edition. Because of this, it
// must have been added fairly late in development, but it seems to be unused,
// perhaps because the E1/E3 commands are generally more useful... but the E1/E3
// commands exist in the Trial Edition! So why was this added? Was it just never
// commands exist in Trial Edition! So why was this added? Was it just never
// finished? We may never know...
// header.flag is the number of valid match entries.
@@ -2343,7 +2343,7 @@ struct S_TournamentMatchInformation_GC_Ep3_BB {
le_uint16_t num_teams = 0;
le_uint16_t unknown_a3 = 0; // Probably actually unused
struct MatchEntry {
parray<char, 0x20> name;
ptext<char, 0x20> name;
uint8_t locked = 0;
uint8_t count = 0;
uint8_t max_count = 0;
@@ -2495,16 +2495,15 @@ struct C_SetBlockedSenders_BB_C6 : C_SetBlockedSenders_C6<28> {
// No arguments
// Server does not respond
// C9 (C->S): Unknown (XB)
// No arguments except header.flag
// C9: Broadcast command (Episode 3)
// Internal name: SndCardClientData
// Same as 60, but should be forwarded only to Episode 3 clients.
// newserv uses this command for all server-generated events (in response to CA
// commands), except for map data requests. This differs from Sega's original
// implementation, which sent CA responses via 60 commands instead.
// CA (C->S): Server data request (Episode 3)
// Internal name: SndCardServerData
// The CA command format is the same as that of the 6xB3 commands, and the
// subsubcommands formats are shared as well. Unlike the 6x commands, the server
// is expected to respond to the command appropriately instead of forwarding it.
@@ -2512,12 +2511,14 @@ struct C_SetBlockedSenders_BB_C6 : C_SetBlockedSenders_C6<28> {
// commands in the comments and structure names.
// CB: Broadcast command (Episode 3)
// Internal name: SndKansenPsoData
// Same as 60, but only send to Episode 3 clients.
// This command is identical to C9, except that CB is not valid on Episode 3
// Trial Edition (whereas C9 is valid).
// This command's format is identical to C9, except that CB is not valid on
// Episode 3 Trial Edition (whereas C9 is valid).
// Unlike the 6x and C9 commands, subcommands sent with the CB command are
// forwarded from spectator teams to the primary team. The client only uses this
// behavior for the 6xBE command (sound chat), and newserv enforces this rule.
// behavior for the 6xBE command (sound chat), and newserv enforces that no
// other subcommand can be sent via CB.
// CC (S->C): Confirm tournament entry (Episode 3)
// This command is not valid on Episode 3 Trial Edition.
@@ -2533,7 +2534,7 @@ struct C_SetBlockedSenders_BB_C6 : C_SetBlockedSenders_C6<28> {
struct S_ConfirmTournamentEntry_GC_Ep3_CC {
ptext<char, 0x40> tournament_name;
le_uint16_t num_teams = 0;
le_uint16_t unknown_a1 = 0;
le_uint16_t players_per_team = 0;
le_uint16_t unknown_a2 = 0;
le_uint16_t unknown_a3 = 0;
ptext<char, 0x20> server_name;
@@ -2617,8 +2618,11 @@ struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server
// interaction mode when closed.
// D7 (C->S): Request GBA game file (V3)
// header.flag is used, but it's not clear for what.
// The server should send the requested file using A6/A7 commands.
// This command is sent when the client executes the file_dl_req (F8C0) quest
// opcode. header.flag contains the value of the opcode's first argument; the
// second argument is a pointer to the filename.
// The server should send the requested file using A6/A7 commands; if the file
// does not exist, the server should reply with a D7 command.
// This command exists on XB as well, but it presumably is never sent by the
// client.
@@ -2626,15 +2630,12 @@ struct C_GBAGameRequest_V3_D7 {
ptext<char, 0x10> filename;
} __packed__;
// D7 (S->C): Unknown (V3/BB)
// D7 (S->C): GBA file not found (V3/BB)
// No arguments
// This command is not valid on PSO GC Episodes 1&2 Trial Edition.
// On PSO V3, this command does... something. The command isn't *completely*
// ignored: it sets a global state variable, but it's not clear what that
// variable does. The variable is set to 0 when the client requests a GBA game
// (by sending a D7 command), and set to 1 when the client receives a D7
// command. The S->C D7 command may be used for declining a download or
// signaling an error of some sort.
// This command tells the client that the file it requested via a D7 command
// does not exist. This causes the F8C1 (get_dl_status) quest opcode to return
// 0 (file not found), rather than 1 (download in progress) or 2 (complete).
// PSO BB accepts but completely ignores this command.
// D8 (C->S): Info board request (V3/BB)
@@ -2764,7 +2765,7 @@ struct C_Unknown_BB_06DF {
struct C_Unknown_BB_07DF {
le_uint32_t unused1 = 0xFFFFFFFF;
le_uint32_t unused2 = 0; // ALways 0
le_uint32_t unused2 = 0; // Always 0
parray<le_uint32_t, 5> unknown_a1;
} __packed__;
@@ -2826,10 +2827,10 @@ struct S_GameInformation_GC_Ep3_E1 {
ptext<char, 0x20> description; // Usually something like "FOmarl CLv30 J"
} __packed__;
/* 0024 */ parray<PlayerEntry, 4> player_entries;
/* 00E4 */ parray<uint8_t, 0x20> unknown_a3;
/* 00E4 */ ptext<char, 0x20> map_name;
/* 0104 */ Episode3::Rules rules;
/* 0114 */ parray<uint8_t, 4> unknown_a4;
/* 0118 */ parray<PlayerEntry, 8> spectator_entries;
/* 0298 */
} __packed__;
// E1 (S->C): Team and key config missing? (BB)
@@ -2920,11 +2921,9 @@ struct S_TournamentEntryList_GC_Ep3_E2 {
struct S_TournamentGameDetails_GC_Ep3_E3 {
// These fields are used only if the Rules pane is shown
/* 0004/032C */ ptext<char, 0x20> name;
/* 0024/034C */ ptext<char, 0x20> map_name;
/* 0044/036C */ Episode3::Rules rules;
/* 0054/037C */ parray<uint8_t, 4> unknown_a1;
/* 0004 */ ptext<char, 0x20> name;
/* 0024 */ ptext<char, 0x20> map_name;
/* 0044 */ Episode3::Rules rules;
// This field is used only if the bracket pane is shown
struct BracketEntry {
@@ -2933,7 +2932,7 @@ struct S_TournamentGameDetails_GC_Ep3_E3 {
ptext<char, 0x18> team_name;
parray<uint8_t, 8> unused;
} __packed__;
/* 0058/0380 */ parray<BracketEntry, 0x20> bracket_entries;
/* 0058 */ parray<BracketEntry, 0x20> bracket_entries;
// This field is used only if the Opponents pane is shown. If players_per_team
// is 2, all fields are shown; if player_per_team is 1, team_name and
@@ -2946,13 +2945,14 @@ struct S_TournamentGameDetails_GC_Ep3_E3 {
ptext<char, 0x10> team_name;
parray<PlayerEntry, 2> players;
} __packed__;
/* 04D8/0800 */ parray<TeamEntry, 2> team_entries;
/* 04D8 */ parray<TeamEntry, 2> team_entries;
/* 05B8/08E0 */ le_uint16_t num_bracket_entries = 0;
/* 05BA/08E2 */ le_uint16_t players_per_team = 0;
/* 05BC/08E4 */ le_uint16_t unknown_a4 = 0;
/* 05BE/08E6 */ le_uint16_t num_spectators = 0;
/* 05C0/08E8 */ parray<PlayerEntry, 8> spectator_entries;
/* 05B8 */ le_uint16_t num_bracket_entries = 0;
/* 05BA */ le_uint16_t players_per_team = 0;
/* 05BC */ le_uint16_t unknown_a4 = 0;
/* 05BE */ le_uint16_t num_spectators = 0;
/* 05C0 */ parray<PlayerEntry, 8> spectator_entries;
/* 0740 */
} __packed__;
// E3 (C->S): Player preview request (BB)
@@ -3062,7 +3062,7 @@ struct C_CreateSpectatorTeam_GC_Ep3_E7 {
le_uint32_t unused = 0;
} __packed__;
// E7 (S->C): Unknown (Episode 3)
// E7 (S->C): Tournament entry list for spectating (Episode 3)
// Same format as E2 command.
// E7: Save or load full character data (BB)
@@ -3089,53 +3089,63 @@ struct SC_SyncCharacterSaveFile_BB_00E7 {
/* 2CB8 */ PlayerRecordsBB_Challenge challenge_records;
/* 2DF8 */ parray<uint8_t, 0x0028> tech_menu_config; // player
/* 2E20 */ parray<uint8_t, 0x002C> unknown_a6;
/* 2E4C */ parray<uint8_t, 0x0058> quest_data2; // player
/* 2E4C */ parray<le_uint32_t, 0x0016> quest_data2; // player
/* 2EA4 */ KeyAndTeamConfigBB key_config; // account
/* 3994 */
} __attribute__((packed));
// E8 (S->C): Join spectator team (Episode 3)
// header.flag = player count (including spectators)
// The client will crash if leader_id == client_id. Presumably one of the
// primary game's players should be the leader (this is what newserv does).
struct S_JoinSpectatorTeam_GC_Ep3_E8 {
parray<le_uint32_t, 0x20> variations; // 04-84; unused
/* 0004 */ parray<le_uint32_t, 0x20> variations; // unused
struct PlayerEntry {
PlayerLobbyDataDCGC lobby_data; // 0x20 bytes
PlayerInventory inventory; // 0x34C bytes
PlayerDispDataDCPCV3 disp; // 0xD0 bytes
} __packed__; // 0x43C bytes
parray<PlayerEntry, 4> players; // 84-1174
uint8_t client_id = 0;
uint8_t leader_id = 0;
uint8_t disable_udp = 1;
uint8_t difficulty = 0;
uint8_t battle_mode = 0;
uint8_t event = 0;
uint8_t section_id = 0;
uint8_t challenge_mode = 0;
le_uint32_t rare_seed = 0;
uint8_t episode = 0;
uint8_t unused2 = 1;
uint8_t solo_mode = 0;
uint8_t unused3 = 0;
/* 0000 */ PlayerLobbyDataDCGC lobby_data;
/* 0020 */ PlayerInventory inventory;
/* 036C */ PlayerDispDataDCPCV3 disp;
/* 043C */
} __packed__;
/* 0084 */ parray<PlayerEntry, 4> players;
/* 1174 */ uint8_t client_id = 0;
/* 1175 */ uint8_t leader_id = 0;
/* 1176 */ uint8_t disable_udp = 1;
/* 1177 */ uint8_t difficulty = 0;
/* 1178 */ uint8_t battle_mode = 0;
/* 1179 */ uint8_t event = 0;
/* 117A */ uint8_t section_id = 0;
/* 117B */ uint8_t challenge_mode = 0;
/* 117C */ le_uint32_t rare_seed = 0;
/* 1180 */ uint8_t episode = 0;
/* 1181 */ parray<uint8_t, 3> unused;
struct SpectatorEntry {
le_uint32_t player_tag = 0;
le_uint32_t guild_card_number = 0;
ptext<char, 0x20> name;
uint8_t present = 0;
uint8_t unknown_a3 = 0;
le_uint16_t level = 0;
parray<le_uint32_t, 2> unknown_a5;
parray<le_uint16_t, 2> unknown_a6;
} __packed__; // 0x38 bytes
// It seems that at some point Sega intended to show each player's rank in
// spectator teams. The unused1 and unused3 fields are intended for the
// player's encrypted rank text and rank color (according to old Sega logs),
// but the client ignores them. It's not clear what unused4 may have been
// for, but the client also completely ignores it.
/* 00 */ le_uint32_t player_tag = 0;
/* 04 */ le_uint32_t guild_card_number = 0;
/* 08 */ ptext<char, 0x10> name;
/* 18 */ ptext<char, 0x10> unused1;
/* 28 */ uint8_t present = 0;
/* 29 */ uint8_t unused2 = 0;
/* 2A */ le_uint16_t level = 0;
/* 2C */ le_uint32_t unused3 = 0xFFFFFFFF;
/* 30 */ le_uint32_t name_color = 0xFFFFFFFF; // ARGB8888
/* 34 */ parray<le_uint16_t, 2> unused4;
/* 38 */
} __packed__;
// Somewhat misleadingly, this array also includes the players actually in the
// battle - they appear in the first positions. Presumably the first 4 are
// always for battlers, and the last 8 are always for spectators.
parray<SpectatorEntry, 12> entries; // 1184-1424
ptext<char, 0x20> spectator_team_name;
/* 1184 */ parray<SpectatorEntry, 12> entries;
/* 1424 */ ptext<char, 0x20> spectator_team_name;
// This field doesn't appear to be actually used by the game, but some servers
// send it anyway (and the game presumably ignores it)
parray<PlayerEntry, 8> spectator_players;
// send it anyway (and the game ignores it)
/* 1444 */ parray<PlayerEntry, 8> spectator_players;
/* 3624 */
} __packed__;
// E8 (C->S): Guild card commands (BB)
@@ -3492,9 +3502,9 @@ struct S_CardTradeComplete_GC_Ep3_EE_FlagD4 {
// scrolls to the left.
// EF (C->S): Join card auction (Episode 3)
// This command should be treated like AC (quest barrier); that is, when all
// players in the same game have sent an EF command, the server should send an
// EF back to them all at the same time to start the auction.
// When a card auction is ready to begin, the leader sends this command to
// request the card list. The server then sends an EF command to all players
// to start the auction.
// EF (S->C): Start card auction (Episode 3)
@@ -3632,10 +3642,12 @@ struct G_ExtendedHeader {
// 6x01: Invalid subcommand
// 6x02: Unknown
// This subcommand is completely ignored (at least, by PSO GC).
// This subcommand is completely ignored on V3.
// TODO: It is not ignored on V1 and V2. Figure out what it does and document it.
// 6x03: Unknown (same handler as 02)
// This subcommand is completely ignored (at least, by PSO GC).
// 6x03: Unknown
// This subcommand is completely ignored on V3.
// TODO: It is not ignored on V1 and V2. Figure out what it does and document it.
// 6x04: Unknown
@@ -3699,18 +3711,16 @@ struct G_SendGuildCard_BB_6x06 {
// 6x07: Symbol chat
struct SymbolChat {
// TODO: How does this format differ across PSO versions? The GC version
// treats some fields as unexpectedly large values (for example, face_spec
// through unused2 are byteswapped as an le_uint32_t), so we should verify
// that the order of these fields is the same on other versions.
// Bits: ----------------------DMSSSCCCFF
// S = sound, C = face color, F = face shape, D = capture, M = mute sound
/* 00 */ le_uint32_t spec;
// Corner objects are specified in reading order ([0] is the top-left one).
// Bits (each entry): ---VHCCCZZZZZZZZ
// V = reverse vertical, H = reverse horizontal, C = color, Z = object
// If Z is all 1 bits (0xFF), no corner object is rendered.
/* 04 */ parray<le_uint16_t, 4> corner_objects;
struct FacePart {
uint8_t type; // FF = no part in this slot
uint8_t x;
@@ -3896,7 +3906,7 @@ struct G_SetPlayerVisibility_6x22_6x23 {
// 6x24: Teleport player
struct G_Unknown_6x24 {
struct G_TeleportPlayer_6x24 {
G_ClientIDHeader header;
le_uint32_t unknown_a1;
le_float x;
@@ -4529,21 +4539,25 @@ struct G_SyncFlagState_6x6E_Decompressed {
// unknown fields above.
} __packed__;
// 6x6F: Unknown (used while loading into game)
// 6x6F: Set quest flags (used while loading into game)
struct G_Unknown_6x6F {
struct G_SetQuestFlags_6x6F {
G_UnusedHeader header;
parray<uint8_t, 0x200> unknown_a1;
parray<parray<uint8_t, 0x80>, 4> quest_flags_by_difficulty;
} __packed__;
// 6x70: Sync player disp data and inventory (used while loading into game)
// Annoyingly, they didn't use the same format as the 65/67/68 commands here,
// and instead rearranged a bunch of things.
// The format appears to be the same for all pre-BB PSO versions, although
// Episode 3 does not send this command at all since the relevant data is sent
// to the joining player in the 64 command instead.
struct G_SyncPlayerDispAndInventory_V3_6x70 {
G_ExtendedHeader<G_UnusedHeader> header;
struct G_SyncPlayerDispAndInventory_DC_PC_V3_6x70 {
// Offsets in this struct are relative to the overall command header
/* 000C */ parray<le_uint16_t, 2> unknown_a1;
/* 0004 */ G_ExtendedHeader<G_UnusedHeader> header;
/* 000C */ le_uint16_t client_id;
/* 000E */ le_uint16_t unknown_a1;
// [1] and [3] in this array (and maybe [2] also) appear to be le_floats;
// they could be the player's current (x, y, z) coords
/* 0010 */ parray<le_uint32_t, 7> unknown_a2;
@@ -4561,7 +4575,7 @@ struct G_SyncPlayerDispAndInventory_V3_6x70 {
/* 00A4 */ parray<uint8_t, 0x14> unknown_a9;
/* 00B8 */ le_uint32_t unknown_a10;
/* 00BC */ le_uint32_t unknown_a11;
/* 00C0 */ parray<uint8_t, 0x14> technique_levels; // Last byte is uninitialized
/* 00C0 */ parray<uint8_t, 0x14> technique_levels_v1; // Last byte is uninitialized
/* 00D4 */ PlayerVisualConfig visual;
/* 0124 */ PlayerStats stats;
/* 0148 */ struct {
@@ -4571,6 +4585,7 @@ struct G_SyncPlayerDispAndInventory_V3_6x70 {
parray<PlayerInventoryItem, 0x1E> items;
} __packed__ inventory;
/* 0494 */ le_uint32_t unknown_a15;
/* 0498 */
} __packed__;
// 6x71: Unknown (used while loading into game)
@@ -4592,16 +4607,26 @@ struct G_Unknown_6x73 {
} __packed__;
// 6x74: Word select
// There is a bug in PSO GC with regard to this command: the client does not
// byteswap the header, which means the client_id field is big-endian.
struct G_WordSelect_6x74 {
G_ClientIDHeader header;
le_uint16_t unknown_a1;
le_uint16_t unknown_a2;
parray<le_uint16_t, 8> entries;
le_uint32_t unknown_a3;
struct WordSelectMessage {
le_uint16_t num_tokens;
le_uint16_t target_type;
parray<le_uint16_t, 8> tokens;
le_uint32_t numeric_parameter;
le_uint32_t unknown_a4;
} __packed__;
template <bool IsBigEndian>
struct G_WordSelect_6x74 {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
uint8_t subcommand;
uint8_t size;
U16T client_id;
WordSelectMessage message;
} __packed__;
// 6x75: Set quest flag
struct G_SetQuestFlag_DC_PC_6x75 {
@@ -5129,29 +5154,31 @@ struct G_CardBattleCommandHeader {
} __packed__;
// Unlike all other 6x subcommands, the 6xB3 subcommand is sent to the server in
// a CA command instead of a 6x, C9, or CB command. (For this reason, we refer
// to 6xB3xZZ commands as CAxZZ commands as well.) The server is expected to
// reply to CA commands instead of forwarding them. The logic for doing so is
// primarily implemented in Episode3/Server.cc and the surrounding classes.
// a CA command instead of a 6x, C9, or CB command. (For this reason, we
// generally refer to 6xB3xZZ commands as CAxZZ commands instead.) The server is
// expected to reply to CA commands with one or more 6xB4 subcommands instead of
// forwarding them. The logic for doing so is implemented in Episode3/Server.cc
// and the surrounding classes.
// The 6xB3 subcommand has a longer header than 6xB4 and 6xB5. This header is
// common to all 6xB3x (CAx) subcommands.
struct G_CardServerDataCommandHeader {
uint8_t subcommand = 0xB3;
uint8_t size = 0x00;
le_uint16_t unused1 = 0x0000;
uint8_t subsubcommand = 0x00; // See 6xBx subcommand table (after this table)
uint8_t sender_client_id = 0x00;
uint8_t mask_key = 0x00; // Same meaning as in G_CardBattleCommandHeader
uint8_t unused2 = 0x00;
be_uint32_t sequence_num;
be_uint32_t context_token;
/* 00 */ uint8_t subcommand = 0xB3;
/* 01 */ uint8_t size = 0x00;
/* 02 */ le_uint16_t unused1 = 0x0000;
/* 04 */ uint8_t subsubcommand = 0x00; // See 6xBx subcommand table (after this table)
/* 05 */ uint8_t sender_client_id = 0x00;
/* 06 */ uint8_t mask_key = 0x00; // Same meaning as in G_CardBattleCommandHeader
/* 07 */ uint8_t unused2 = 0x00;
/* 08 */ be_uint32_t sequence_num;
/* 0C */ be_uint32_t context_token;
/* 10 */
} __packed__;
// 6xB4: Unknown (XBOX; voice chat)
// 6xB4: CARD battle server response (Episode 3) - see 6xB3 (above)
// 6xB5: CARD battle client command (Episode 3) - see 6xB3 (above)
// 6xB4: CARD battle server response (Episode 3) - see 6xB3 above
// 6xB5: CARD battle client command (Episode 3) - see 6xB3 above
// 6xB5: BB shop request (handled by the server)
@@ -5179,7 +5206,7 @@ struct G_MapList_GC_Ep3_6xB6x40 {
le_uint16_t compressed_data_size;
le_uint16_t unused;
// PRS-compressed map list data follows here. newserv generates this from the
// map index at startup; see the MapList struct in Episode3/DataIndexes.hh
// map index when requested; see the MapList struct in Episode3/DataIndexes.hh
// and Episode3::MapIndex::get_compressed_map_list for details on the format.
} __packed__;
@@ -5294,7 +5321,7 @@ struct G_CardCounts_GC_Ep3_6xBC {
struct G_BankContentsHeader_BB_6xBC {
G_ExtendedHeader<G_UnusedHeader> header;
le_uint32_t checksum; // can be random; client won't notice
le_uint32_t numItems;
le_uint32_t num_items;
le_uint32_t meseta;
// Item data follows
} __packed__;
@@ -5469,8 +5496,7 @@ struct G_ExchangeItemForTeamPoints_BB_6xCC {
struct G_RestartBattle_BB_6xCF {
G_UnusedHeader header;
parray<le_uint32_t, 11> unknown_a1;
le_uint32_t unknown_a2;
BattleRules rules;
} __packed__;
// 6xD0: Battle mode level up (BB; handled by server)
@@ -5518,17 +5544,17 @@ struct G_Unknown_BB_6xD4 {
struct G_ExchangeItemInQuest_BB_6xD5 {
G_ClientIDHeader header;
ItemData unknown_a1; // Only data1[0]-[2] are used
ItemData unknown_a2; // Only data1[0]-[2] are used
le_uint16_t unknown_a3;
le_uint16_t unknown_a4;
ItemData find_item; // Only data1[0]-[2] are used
ItemData replace_item; // Only data1[0]-[2] are used
le_uint16_t success_function_id;
le_uint16_t failure_function_id;
} __packed__;
// 6xD6: Wrap item (BB; handled by server)
struct G_WrapItem_BB_6xD6 {
G_ClientIDHeader header;
ItemData item_data;
ItemData item;
uint8_t unknown_a1;
parray<uint8_t, 3> unused;
} __packed__;
@@ -5538,9 +5564,9 @@ struct G_WrapItem_BB_6xD6 {
struct G_PaganiniPhotonDropExchange_BB_6xD7 {
G_ClientIDHeader header;
ItemData unknown_a1; // Only data1[0]-[2] are used
le_uint16_t request_id;
le_uint16_t unknown_a3;
ItemData new_item; // Only data1[0]-[2] are used
le_uint16_t success_function_id;
le_uint16_t failure_function_id;
} __packed__;
// 6xD8: Add S-rank weapon special (BB; handled by server)
@@ -5560,8 +5586,8 @@ struct G_AddSRankWeaponSpecial_BB_6xD8 {
struct G_MomokaItemExchange_BB_6xD9 {
G_ClientIDHeader header;
ItemData unknown_a1;
ItemData unknown_a2;
ItemData find_item; // Only data1[0]-[2] are used
ItemData replace_item; // Only data1[0]-[2] are used
le_uint32_t unknown_a3;
le_uint32_t unknown_a4;
le_uint16_t unknown_a5;
@@ -5573,13 +5599,13 @@ struct G_MomokaItemExchange_BB_6xD9 {
struct G_UpgradeWeaponAttribute_BB_6xDA {
G_ClientIDHeader header;
ItemData unknown_a1; // Only data1[0]-[2] are used
le_uint32_t item_id;
le_uint32_t attribute;
le_uint32_t unknown_a4; // 0 or 1
le_uint32_t unknown_a5;
le_uint16_t request_id;
le_uint16_t unknown_a7;
ItemData item; // Only data1[0-2] are used (argsA[1-3])
le_uint32_t item_id; // argsA[0]
le_uint32_t attribute; // argsA[4]
le_uint32_t payment_count; // Number of PD or PS (argsA[5])
le_uint32_t payment_type; // 0 = Photon Drops, 1 = Photon Spheres
le_uint16_t success_function_id; // argsA[6]
le_uint16_t failure_function_id; // argsA[7]
} __packed__;
// 6xDB: Exchange item in quest (BB)
@@ -5617,10 +5643,10 @@ struct G_GoodLuckQuestActions_BB_6xDE {
le_uint16_t unknown_a3;
} __packed__;
// 6xDF: Black Paper's Deal Photon Drop exchange (BB; handled by server)
// 6xDF: Black Paper's Deal Photon Crystal exchange (BB; handled by server)
// The client sends this when it executes an F95D quest opcode.
struct G_BlackPaperDealPhotonDropExchange_BB_6xE0 {
struct G_BlackPaperDealPhotonCrystalExchange_BB_6xDF {
G_ClientIDHeader header;
} __packed__;
@@ -5629,7 +5655,12 @@ struct G_BlackPaperDealPhotonDropExchange_BB_6xE0 {
struct G_BlackPaperDealRewards_BB_6xE0 {
G_ClientIDHeader header;
parray<uint8_t, 12> unknown_a1; // TODO: There might be uint16_ts and uint32_ts in here.
uint8_t unknown_a1;
uint8_t unknown_a2; // argsA[0]
uint8_t unknown_a3;
uint8_t unknown_a4;
le_float unknown_a5; // argsA[1]
le_float unknown_a6; // argsA[2]
} __packed__;
// 6xE1: Gallon's Plan quest (BB; handled by server)
@@ -5744,7 +5775,7 @@ struct G_UpdateShortStatuses_GC_Ep3_6xB4x04 {
struct G_UpdateMap_GC_Ep3_6xB4x05 {
G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateMap_GC_Ep3_6xB4x05) / 4, 0, 0x05, 0, 0, 0};
Episode3::MapAndRulesState state;
uint8_t unknown_a1 = 0;
uint8_t start_battle = 0;
parray<uint8_t, 3> unused;
} __packed__;
@@ -5940,9 +5971,6 @@ struct G_StartBattle_GC_Ep3_6xB3x1D_CAx1D {
struct G_ActionResult_GC_Ep3_6xB4x1E {
G_CardBattleCommandHeader header = {0xB4, sizeof(G_ActionResult_GC_Ep3_6xB4x1E) / 4, 0, 0x1E, 0, 0, 0};
// TODO: Is this supposed to be big-endian or little-endian? The client makes
// it look like it should be little-endian, but logs from the Sega servers
// make it look like it should be big-endian.
be_uint32_t sequence_num = 0;
uint8_t error_code = 0;
uint8_t response_phase = 0;
@@ -6044,11 +6072,12 @@ struct G_Unknown_GC_Ep3_6xB4x2A {
parray<uint8_t, 2> unused;
} __packed__;
// 6xB3x2B / CAx2B: Unknown
// It seems Sega's servers completely ignored this command.
// 6xB3x2B / CAx2B: Legacy set card
// It seems Sega's servers completely ignored this command. The command name is
// based on a debug message found nearby.
struct G_Unknown_GC_Ep3_6xB3x2B_CAx2B {
G_CardServerDataCommandHeader header = {0xB3, sizeof(G_Unknown_GC_Ep3_6xB3x2B_CAx2B) / 4, 0, 0x2B, 0, 0, 0, 0, 0};
struct G_ExecLegacyCard_GC_Ep3_6xB3x2B_CAx2B {
G_CardServerDataCommandHeader header = {0xB3, sizeof(G_ExecLegacyCard_GC_Ep3_6xB3x2B_CAx2B) / 4, 0, 0x2B, 0, 0, 0, 0, 0};
le_uint16_t unused2 = 0;
parray<uint8_t, 2> unused3;
} __packed__;
@@ -6069,9 +6098,11 @@ struct G_Unknown_GC_Ep3_6xB4x2C {
struct G_Unknown_GC_Ep3_6xB5x2D {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x2D) / 4, 0, 0x2D, 0, 0, 0};
// This array is indexed into by a global variable. I don't have any examples
// of this command, so I don't know how long the array should be - 4 is a
// probably-incorrect guess.
// This array is indexed by client ID. When a client receives this command, it
// sends a 6x70 command to itself. It's not clear what the function of this is
// intended to be.
// TODO: Figure out if tournament fast loading can be implemented using this
// to fix the stuck-in-wall glitch.
parray<uint8_t, 4> unknown_a1;
} __packed__;
@@ -6083,11 +6114,10 @@ struct G_BattleEndNotification_GC_Ep3_6xB5x2E {
parray<uint8_t, 3> unused;
} __packed__;
// 6xB5x2F: Unknown
// TODO: Document this from Episode 3 client/server disassembly
// 6xB5x2F: Set deck in battle setup menu
struct G_Unknown_GC_Ep3_6xB5x2F {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x2F) / 4, 0, 0x2F, 0, 0, 0};
struct G_SetDeckInBattleSetupMenu_GC_Ep3_6xB5x2F {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_SetDeckInBattleSetupMenu_GC_Ep3_6xB5x2F) / 4, 0, 0x2F, 0, 0, 0};
parray<uint8_t, 4> unknown_a1;
parray<uint8_t, 0x18> unknown_a2;
@@ -6102,18 +6132,18 @@ struct G_Unknown_GC_Ep3_6xB5x2F {
} __packed__;
// 6xB5x30: Unknown
// TODO: Document this from Episode 3 client/server disassembly
// The client never sends this command, and when the client received this
// command, it does nothing.
struct G_Unknown_GC_Ep3_6xB5x30 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x30) / 4, 0, 0x30, 0, 0, 0};
// No arguments
} __packed__;
// 6xB5x31: Unknown
// TODO: Document this from Episode 3 client/server disassembly
// 6xB5x31: Confirm deck selection
struct G_Unknown_GC_Ep3_6xB5x31 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x31) / 4, 0, 0x31, 0, 0, 0};
struct G_ConfirmDeckSelection_GC_Ep3_6xB5x31 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_ConfirmDeckSelection_GC_Ep3_6xB5x31) / 4, 0, 0x31, 0, 0, 0};
// Note: This command uses header_b1 for... something.
uint8_t unknown_a1 = 0; // Must be 0 or 1
uint8_t unknown_a2 = 0; // Must be < 4
@@ -6123,15 +6153,18 @@ struct G_Unknown_GC_Ep3_6xB5x31 {
parray<uint8_t, 3> unused;
} __packed__;
// 6xB5x32: Unknown
// TODO: Document this from Episode 3 client/server disassembly
// 6xB5x32: Move shared menu cursor
struct G_Unknown_GC_Ep3_6xB5x32 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x32) / 4, 0, 0x32, 0, 0, 0};
// Note: This command uses header_b1 for... something.
le_uint16_t unknown_a1 = 0;
le_uint16_t unknown_a2 = 0;
parray<uint8_t, 8> unknown_a3;
struct G_MoveSharedMenuCursor_GC_Ep3_6xB5x32 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_MoveSharedMenuCursor_GC_Ep3_6xB5x32) / 4, 0, 0x32, 0, 0, 0};
le_uint16_t selected_item_index = 0xFFFF;
le_uint16_t chosen_item_index = 0xFFFF;
uint8_t unknown_a1 = 0;
uint8_t unknown_a2 = 0;
uint8_t unknown_a3 = 0;
uint8_t unknown_a4 = 0;
uint8_t unknown_a5 = 0;
parray<uint8_t, 3> unused;
} __packed__;
// 6xB4x33: Subtract ally ATK points (e.g. for photon blast)
@@ -6161,17 +6194,16 @@ struct G_PhotonBlastStatus_GC_Ep3_6xB4x35 {
le_uint16_t card_ref = 0xFFFF;
} __packed__;
// 6xB5x36: Unknown
// TODO: Document this from Episode 3 client/server disassembly
// Setting unknown_a1 to a value 4 or greater while in a game causes the player
// 6xB5x36: Recreate player
// Setting client_id to a value 4 or greater while in a game causes the player
// to be temporarily replaced with a default HUmar and placed inside the central
// column in the Morgue, rendering them unable to move. The only ways out of
// this predicament appear to be either to disconnect (e.g. select Quit Game
// from the pause menu) or receive an ED (force leave game) command.
struct G_Unknown_GC_Ep3_6xB5x36 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x36) / 4, 0, 0x36, 0, 0, 0};
uint8_t unknown_a1 = 0; // Must be < 12 (maybe lobby or spectator team client ID)
struct G_RecreatePlayer_GC_Ep3_6xB5x36 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_RecreatePlayer_GC_Ep3_6xB5x36) / 4, 0, 0x36, 0, 0, 0};
uint8_t client_id = 0;
parray<uint8_t, 3> unused;
} __packed__;
@@ -6203,18 +6235,21 @@ struct G_UpdateAllPlayerStatistics_GC_Ep3_6xB4x39 {
parray<Episode3::PlayerBattleStats, 4> stats;
} __packed__;
// 6xB3x3A / CAx3A: Unknown
// It seems Sega's servers completely ignored this command.
// 6xB3x3A / CAx3A: Overall time limit expired
// It seems Sega's servers completely ignored this command and used server-side
// timing instead. newserv does the same.
struct G_Unknown_GC_Ep3_6xB3x3A_CAx3A {
G_CardServerDataCommandHeader header = {0xB3, sizeof(G_Unknown_GC_Ep3_6xB3x3A_CAx3A) / 4, 0, 0x3A, 0, 0, 0, 0, 0};
struct G_OverallTimeLimitExpired_GC_Ep3_6xB3x3A_CAx3A {
G_CardServerDataCommandHeader header = {0xB3, sizeof(G_OverallTimeLimitExpired_GC_Ep3_6xB3x3A_CAx3A) / 4, 0, 0x3A, 0, 0, 0, 0, 0};
} __packed__;
// 6xB4x3B: Unknown
// TODO: Document this from Episode 3 client/server disassembly
// 6xB4x3B: Load current environment
// This command is used to send spectators in a spectator team to the main
// battle. A 6xB4x05 and 6xB6x41 command shouldhave been sent before this, to
// set the map state that should appear for the new spectator.
struct G_Unknown_GC_Ep3_6xB4x3B {
G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x3B) / 4, 0, 0x3B, 0, 0, 0};
struct G_LoadCurrentEnvironment_GC_Ep3_6xB4x3B {
G_CardBattleCommandHeader header = {0xB4, sizeof(G_LoadCurrentEnvironment_GC_Ep3_6xB4x3B) / 4, 0, 0x3B, 0, 0, 0};
parray<uint8_t, 4> unused;
} __packed__;
@@ -6240,7 +6275,6 @@ struct G_SetPlayerSubstatus_GC_Ep3_6xB5x3C {
struct G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D {
G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0};
Episode3::Rules rules;
parray<uint8_t, 4> unknown_a1;
struct Entry {
uint8_t type = 0; // 0 = no player, 1 = human, 2 = COM
ptext<char, 0x10> player_name;
@@ -6321,7 +6355,10 @@ struct G_InitiateCardAuction_GC_Ep3_6xB5x42 {
} __packed__;
// 6xB5x43: Unknown
// TODO: Document this from Episode 3 client/server disassembly
// This command stores the card IDs and counts in a global array on the client,
// but this array is never read from. It's likely this is a remnant of an
// unimplemented or removed feature, or an earlier implementation of the card
// trade window.
struct G_Unknown_GC_Ep3_6xB5x43 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x43) / 4, 0, 0x43, 0, 0, 0};
@@ -6329,7 +6366,7 @@ struct G_Unknown_GC_Ep3_6xB5x43 {
// Both fields here are masked. To get the actual values used by the game,
// XOR the values here with 0x39AB.
le_uint16_t masked_card_id = 0xFFFF; // Must be < 0x2F1 (when unmasked)
le_uint16_t masked_unknown_a1 = 0; // Must be in [1, 99] (when unmasked)
le_uint16_t masked_count = 0; // Must be in [1, 99] (when unmasked)
} __packed__;
parray<Entry, 0x14> entries;
} __packed__;
@@ -6387,13 +6424,12 @@ struct G_ServerVersionStrings_GC_Ep3_6xB4x46 {
le_uint32_t unused = 0;
} __packed__;
// 6xB5x47: Unknown
// TODO: Document this from Episode 3 client/server disassembly
// 6xB5x47: Set spectator's CARD level
// header.sender_client_id is the spectator's client ID.
struct G_Unknown_GC_Ep3_6xB5x47 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x47) / 4, 0, 0x47, 0, 0, 0};
// Note: This command uses header_b1, which must be < 12.
le_uint32_t unknown_a1 = 0;
struct G_SetSpectatorCARDLevel_GC_Ep3_6xB5x47 {
G_CardBattleCommandHeader header = {0xB5, sizeof(G_SetSpectatorCARDLevel_GC_Ep3_6xB5x47) / 4, 0, 0x47, 0, 0, 0};
le_uint32_t clv = 0;
} __packed__;
// 6xB3x48 / CAx48: End turn
@@ -6577,8 +6613,10 @@ struct G_SetGameMetadata_GC_Ep3_6xB4x52 {
// 30+ = icon with 12 spectators (red)
le_uint16_t total_spectators = 0; // Clamped to [0, 999] by the client
le_uint16_t unused = 0;
le_uint16_t size = 0; // Number of used bytes in unknown_a2 (clamped to 0xFF)
parray<uint8_t, 0x100> unknown_a2;
// If text_size is not zero, the text is shown in the top bar instead of the
// usual message ("Viewing Battle", "Time left: XX:XX", and the like).
le_uint16_t text_size = 0;
ptext<char, 0x100> text;
} __packed__;
// 6xB4x53: Reject battle start request
+3 -1
View File
@@ -2,6 +2,8 @@
#include "StaticGameData.hh"
using namespace std;
CommonItemSet::CommonItemSet(shared_ptr<const string> data)
: gsl(data, true) {}
@@ -13,7 +15,7 @@ const CommonItemSet::Table<true>& CommonItemSet::get_table(
((mode == GameMode::CHALLENGE) ? "c" : ""),
((episode == Episode::EP2) ? "l" : ""),
tolower(abbreviation_for_difficulty(difficulty)),
secid);
(mode == GameMode::CHALLENGE) ? 0 : secid);
auto data = this->gsl.get(filename);
if (data.second < sizeof(Table<true>)) {
throw runtime_error(string_printf(
+11 -9
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <phosg/Encoding.hh>
#include "GSLArchive.hh"
@@ -18,14 +19,14 @@ struct ProbabilityTable {
void push(ItemT item) {
if (this->count == MaxCount) {
throw runtime_error("push to full probability table");
throw std::runtime_error("push to full probability table");
}
this->items[this->count++] = item;
}
ItemT pop() {
if (this->count == 0) {
throw runtime_error("pop from empty probability table");
throw std::runtime_error("pop from empty probability table");
}
return this->items[--this->count];
}
@@ -41,7 +42,7 @@ struct ProbabilityTable {
ItemT sample(PSOLFGEncryption& random_crypt) const {
if (this->count == 0) {
throw runtime_error("pop from empty probability table");
throw std::runtime_error("pop from empty probability table");
} else if (this->count == 1) {
return this->items[0];
} else {
@@ -103,7 +104,7 @@ public:
// This table specifies how many areas each weapon subtype appears in. For
// example, if Sword (subtype 02, which is index 1 in this table and the
// table above) has a subtype base of -2 and a subtype area lneght of 4,
// table above) has a subtype base of -2 and a subtype area length of 4,
// then Sword items can be found when area - 1 is 2, 3, 4, or 5 (Cave 1
// through Mine 1), and Gigush (the next sword subtype) can be found in Mine
// 1 through Ruins 3.
@@ -200,8 +201,9 @@ public:
/* 04CC */ parray<parray<Range<uint8_t>, 0x0A>, 0x13> technique_level_ranges;
// Each byte in this table (indexed by enemy_type) represents the percent
// chance that the enemy drops anything at all. (This check is done after
// the rare drop check, so it only applies to non-rare items.)
// chance that the enemy drops anything at all. (This check is done before
// the rare drop check, so the chance of getting a rare item from an enemy
// is essentially this probability multiplied by the rare drop rate.)
/* 0648 */ parray<uint8_t, 0x64> enemy_type_drop_probs;
// This array (indexed by enemy_id) specifies the range of meseta values
@@ -303,7 +305,7 @@ public:
using WeightTableEntry32 = WeightTableEntry<be_uint32_t>;
protected:
std::shared_ptr<const string> data;
std::shared_ptr<const std::string> data;
StringReader r;
struct TableSpec {
@@ -320,7 +322,7 @@ protected:
const T* entries = &r.pget<T>(
spec.offset + index * spec.entries_per_table * sizeof(T),
spec.entries_per_table * sizeof(T));
return make_pair(entries, spec.entries_per_table);
return std::make_pair(entries, spec.entries_per_table);
}
};
@@ -418,7 +420,7 @@ private:
uint8_t section_id) const;
int8_t get_luck(uint32_t start_offset, uint8_t delta_index) const;
std::shared_ptr<const string> data;
std::shared_ptr<const std::string> data;
StringReader r;
struct DeltaProbabilityEntry {
+27 -14
View File
@@ -289,7 +289,7 @@ string prs_compress_optimal(
// For each node, populate the literal value, and the best ways to get to the
// following nodes
for (size_t z = 0; z < in_size; z++) {
if ((z & 0xFFF) == 0) {
if ((z & 0xFFF) == 0 && progress_fn) {
progress_fn(CompressPhase::CONSTRUCT_PATHS, z, in_size, 0);
}
@@ -794,7 +794,8 @@ string prs_compress_indexed(const string& data, ProgressCallback progress_fn) {
return prs_compress_indexed(data.data(), data.size(), progress_fn);
}
PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size) {
PRSDecompressResult prs_decompress_with_meta(
const void* data, size_t size, size_t max_output_size, bool allow_unterminated) {
// PRS is an LZ77-based compression algorithm. Compressed data is split into
// two streams: a control stream and a data stream. The control stream is read
// one bit at a time, and the data stream is read one byte at a time. The
@@ -839,7 +840,11 @@ PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size
// Control 1 = literal byte
if (cr.read()) {
if (max_output_size && w.size() == max_output_size) {
throw runtime_error("maximum output size exceeded");
if (allow_unterminated) {
return {std::move(w.str()), r.where()};
} else {
throw runtime_error("maximum output size exceeded");
}
}
w.put_u8(r.get_u8());
@@ -882,7 +887,11 @@ PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size
}
for (size_t z = 0; z < count; z++) {
if (max_output_size && w.size() == max_output_size) {
throw runtime_error("maximum output size exceeded");
if (allow_unterminated) {
return {std::move(w.str()), r.where()};
} else {
throw out_of_range("maximum output size exceeded");
}
}
w.put_u8(w.str()[read_offset + z]);
}
@@ -892,21 +901,21 @@ PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size
return {std::move(w.str()), r.where()};
}
PRSDecompressResult prs_decompress_with_meta(const string& data, size_t max_output_size) {
return prs_decompress_with_meta(data.data(), data.size(), max_output_size);
PRSDecompressResult prs_decompress_with_meta(const string& data, size_t max_output_size, bool allow_unterminated) {
return prs_decompress_with_meta(data.data(), data.size(), max_output_size, allow_unterminated);
}
string prs_decompress(const void* data, size_t size, size_t max_output_size) {
auto ret = prs_decompress_with_meta(data, size, max_output_size);
string prs_decompress(const void* data, size_t size, size_t max_output_size, bool allow_unterminated) {
auto ret = prs_decompress_with_meta(data, size, max_output_size, allow_unterminated);
return std::move(ret.data);
}
string prs_decompress(const string& data, size_t max_output_size) {
auto ret = prs_decompress_with_meta(data.data(), data.size(), max_output_size);
string prs_decompress(const string& data, size_t max_output_size, bool allow_unterminated) {
auto ret = prs_decompress_with_meta(data.data(), data.size(), max_output_size, allow_unterminated);
return std::move(ret.data);
}
size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size) {
size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size, bool allow_unterminated) {
size_t ret = 0;
StringReader r(data, size);
ControlStreamReader cr(r);
@@ -943,15 +952,19 @@ size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size
}
if (max_output_size && ret > max_output_size) {
throw runtime_error("maximum output size exceeded");
if (allow_unterminated) {
return max_output_size;
} else {
throw out_of_range("maximum output size exceeded");
}
}
}
return ret;
}
size_t prs_decompress_size(const string& data, size_t max_output_size) {
return prs_decompress_size(data.data(), data.size(), max_output_size);
size_t prs_decompress_size(const string& data, size_t max_output_size, bool allow_unterminated) {
return prs_decompress_size(data.data(), data.size(), max_output_size, allow_unterminated);
}
void prs_disassemble(FILE* stream, const void* data, size_t size) {
+6 -6
View File
@@ -184,15 +184,15 @@ struct PRSDecompressResult {
std::string data;
size_t input_bytes_used;
};
PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size = 0);
PRSDecompressResult prs_decompress_with_meta(const std::string& data, size_t max_output_size = 0);
std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0);
std::string prs_decompress(const std::string& data, size_t max_output_size = 0);
PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false);
PRSDecompressResult prs_decompress_with_meta(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false);
std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false);
std::string prs_decompress(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false);
// Returns the decompressed size of PRS-compressed data, without actually
// decompressing it.
size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size = 0);
size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0);
size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size = 0, bool allow_unterminated = false);
size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0, bool allow_unterminated = false);
// Prints the command stream from a PRS-compressed buffer.
void prs_disassemble(FILE* stream, const void* data, size_t size);
+28 -28
View File
@@ -1,4 +1,4 @@
#include "Product.hh"
#include "DCSerialNumbers.hh"
#include <stdint.h>
@@ -1163,23 +1163,23 @@ static char replace_char_reverse(char ch) {
static constexpr uint64_t INVALID_PRODUCT = 0xFFFFFFFFFFFFFFFF;
static uint64_t decode_product_str(const string& s) {
static uint64_t decode_dc_serial_number_str(const string& s) {
if (s.size() != 8) {
return INVALID_PRODUCT;
}
uint64_t product = 0;
uint64_t serial_number = 0;
for (char ch : s) {
char new_ch = replace_char_forward(ch);
if (new_ch == '\0') {
return INVALID_PRODUCT;
}
product = (product << 4) | value_for_hex_char(new_ch);
serial_number = (serial_number << 4) | value_for_hex_char(new_ch);
}
return product;
return serial_number;
}
static uint32_t decode_product_int(uint32_t v) {
static uint32_t decode_dc_serial_number_int(uint32_t v) {
return (replace_nybble_forward(v >> 28) << 28) |
(replace_nybble_forward(v >> 24) << 24) |
(replace_nybble_forward(v >> 20) << 20) |
@@ -1190,7 +1190,7 @@ static uint32_t decode_product_int(uint32_t v) {
(replace_nybble_forward(v));
}
static uint32_t encode_product_int(uint32_t v) {
static uint32_t encode_dc_serial_number_int(uint32_t v) {
return (replace_nybble_reverse(v >> 28) << 28) |
(replace_nybble_reverse(v >> 24) << 24) |
(replace_nybble_reverse(v >> 20) << 20) |
@@ -1215,9 +1215,9 @@ static pair<size_t, size_t> compute_offset1_and_limit1(
}
}
bool product_is_valid_slow(const string& s, uint8_t domain, uint8_t subdomain) {
uint64_t product = decode_product_str(s);
if (product == INVALID_PRODUCT) {
bool dc_serial_number_is_valid_slow(const string& s, uint8_t domain, uint8_t subdomain) {
uint64_t serial_number = decode_dc_serial_number_str(s);
if (serial_number == INVALID_PRODUCT) {
return false;
}
@@ -1229,7 +1229,7 @@ bool product_is_valid_slow(const string& s, uint8_t domain, uint8_t subdomain) {
for (; offset1 < limit1; offset1++) {
for (size_t offset2 = 0; offset2 < sizeof(primes2) / sizeof(primes2[0]); offset2++) {
for (size_t offset3 = 0; offset3 < sizeof(primes3) / sizeof(primes3[0]); offset3++) {
if (primes1[offset1] * primes2[offset2] * primes3[offset3] == product) {
if (primes1[offset1] * primes2[offset2] * primes3[offset3] == serial_number) {
return true;
}
}
@@ -1238,14 +1238,14 @@ bool product_is_valid_slow(const string& s, uint8_t domain, uint8_t subdomain) {
return false;
}
bool decoded_product_is_valid_fast(uint32_t product, uint8_t domain, uint8_t subdomain) {
bool decoded_dc_serial_number_is_valid_fast(uint32_t serial_number, uint8_t domain, uint8_t subdomain) {
auto [offset1_start, limit1] = compute_offset1_and_limit1(domain, subdomain);
if (limit1 == 0) {
return false;
}
for (uint64_t prefix = 0; prefix < 0x000000E800000000; prefix += 0x0000000100000000) {
uint64_t sub0 = product | prefix;
uint64_t sub0 = serial_number | prefix;
for (size_t offset1 = offset1_start; offset1 < limit1; offset1++) {
if (sub0 % primes1[offset1]) {
continue;
@@ -1264,19 +1264,19 @@ bool decoded_product_is_valid_fast(uint32_t product, uint8_t domain, uint8_t sub
return false;
}
bool product_is_valid_fast(const string& s, uint8_t domain, uint8_t subdomain) {
uint64_t product = decode_product_str(s);
if (product == INVALID_PRODUCT) {
bool dc_serial_number_is_valid_fast(const string& s, uint8_t domain, uint8_t subdomain) {
uint64_t serial_number = decode_dc_serial_number_str(s);
if (serial_number == INVALID_PRODUCT) {
return false;
}
return decoded_product_is_valid_fast(product, domain, subdomain);
return decoded_dc_serial_number_is_valid_fast(serial_number, domain, subdomain);
}
bool product_is_valid_fast(uint32_t product, uint8_t domain, uint8_t subdomain) {
return decoded_product_is_valid_fast(decode_product_int(product), domain, subdomain);
bool dc_serial_number_is_valid_fast(uint32_t serial_number, uint8_t domain, uint8_t subdomain) {
return decoded_dc_serial_number_is_valid_fast(decode_dc_serial_number_int(serial_number), domain, subdomain);
}
string generate_product(uint8_t domain, uint8_t subdomain) {
string generate_dc_serial_number(uint8_t domain, uint8_t subdomain) {
size_t offset1, limit1;
if (domain == 0) {
offset1 = 0x00;
@@ -1305,7 +1305,7 @@ string generate_product(uint8_t domain, uint8_t subdomain) {
return ret;
}
unordered_map<uint32_t, string> generate_all_products(uint8_t domain, uint8_t subdomain) {
unordered_map<uint32_t, string> generate_all_dc_serial_numbers(uint8_t domain, uint8_t subdomain) {
vector<uint8_t> domains;
if (domain == 0xFF) {
domains.emplace_back(0x00);
@@ -1345,16 +1345,16 @@ unordered_map<uint32_t, string> generate_all_products(uint8_t domain, uint8_t su
for (size_t index2 = 0; index2 < sizeof(primes2) / sizeof(primes2[0]); index2++) {
for (size_t index3 = 0; index3 < sizeof(primes3) / sizeof(primes3[0]); index3++) {
uint32_t value = primes1[index1] * primes2[index2] * primes3[index3];
ret[encode_product_int(value)].push_back(((domain << 2) & 3) | (subdomain & 3));
ret[encode_dc_serial_number_int(value)].push_back(((domain << 2) & 3) | (subdomain & 3));
}
fprintf(stderr, "... domain=%hhu subdomain=%hhu index2=%zu products=%zu (0x%zX)\n", domain, subdomain, index2, ret.size(), ret.size());
fprintf(stderr, "... domain=%hhu subdomain=%hhu index2=%zu results=%zu (0x%zX)\n", domain, subdomain, index2, ret.size(), ret.size());
}
}
}
return ret;
}
void product_speed_test(uint64_t seed) {
void dc_serial_number_speed_test(uint64_t seed) {
uint32_t effective_seed = (seed & 0xFFFFFFFF00000000) ? random_object<uint32_t>() : seed;
fprintf(stderr, "Product speed test with seed=%08" PRIX32 "\n", effective_seed);
PSOV2Encryption crypt(effective_seed);
@@ -1366,11 +1366,11 @@ void product_speed_test(uint64_t seed) {
string s = string_printf("%08X", crypt.next());
uint64_t start = now();
bool is_valid_fast = product_is_valid_fast(s, 1, 0xFF);
bool is_valid_fast = dc_serial_number_is_valid_fast(s, 1, 0xFF);
time_fast += now() - start;
start = now();
bool is_valid_slow = product_is_valid_slow(s, 1, 0xFF);
bool is_valid_slow = dc_serial_number_is_valid_slow(s, 1, 0xFF);
time_slow += now() - start;
if (((z & 0xF) == 0) || is_valid_slow || is_valid_fast) {
@@ -1381,8 +1381,8 @@ void product_speed_test(uint64_t seed) {
}
}
fprintf(stderr, "Total time (slow): %" PRId64 " usecs (%" PRIu64 " per product)\n", time_slow, time_slow / count);
fprintf(stderr, "Total time (fast): %" PRId64 " usecs (%" PRIu64 " per product)\n", time_fast, time_fast / count);
fprintf(stderr, "Total time (slow): %" PRId64 " usecs (%" PRIu64 " per serial number)\n", time_slow, time_slow / count);
fprintf(stderr, "Total time (fast): %" PRId64 " usecs (%" PRIu64 " per serial number)\n", time_fast, time_fast / count);
fprintf(stderr, "Fast vs. slow speedup: %zux\n", static_cast<size_t>(time_slow / time_fast));
fprintf(stderr, "Disagreements: %zu\n", num_disagreements);
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <stdint.h>
#include <string>
#include <unordered_map>
// dc_serial_number_is_valid_slow is Sega's implementation;
// dc_serial_number_is_valid_fast produces identical results but is between 3000
// and 7500 times faster, depending on the compiler's optimization level.
bool dc_serial_number_is_valid_slow(
const std::string& s, uint8_t domain, uint8_t subdomain = 0xFF);
bool dc_serial_number_is_valid_fast(
const std::string& s, uint8_t domain, uint8_t subdomain = 0xFF);
bool dc_serial_number_is_valid_fast(
uint32_t serial_number, uint8_t domain, uint8_t subdomain = 0xFF);
bool decoded_dc_serial_number_is_valid_fast(
uint32_t serial_number, uint8_t domain, uint8_t subdomain = 0xFF);
std::string generate_dc_serial_number(uint8_t domain, uint8_t subdomain = 0xFF);
std::unordered_map<uint32_t, std::string> generate_all_dc_serial_numbers(uint8_t domain = 0xFF, uint8_t subdomain = 0xFF);
void dc_serial_number_speed_test(uint64_t seed = 0xFFFFFFFFFFFFFFFF);
+4 -2
View File
@@ -127,7 +127,8 @@ const BattleRecord::Event* BattleRecord::get_first_event() const {
void BattleRecord::add_player(
const PlayerLobbyDataDCGC& lobby_data,
const PlayerInventory& inventory,
const PlayerDispDataDCPCV3& disp) {
const PlayerDispDataDCPCV3& disp,
uint32_t level) {
if (!this->is_writable) {
throw logic_error("cannot write to battle record");
}
@@ -141,6 +142,7 @@ void BattleRecord::add_player(
player.lobby_data = lobby_data;
player.inventory = inventory;
player.disp = disp;
player.level = level;
}
void BattleRecord::delete_player(uint8_t client_id) {
@@ -349,7 +351,7 @@ void BattleRecordPlayer::schedule_events() {
send_command(l, (ev.data.size() >= 0x400) ? 0x6C : 0x60, 0x00, ev.data);
break;
case BattleRecord::Event::Type::EP3_GAME_COMMAND:
send_command(l, 0xCB, 0x00, ev.data);
send_command(l, 0xC9, 0x00, ev.data);
break;
case BattleRecord::Event::Type::CHAT_MESSAGE:
send_chat_message(l, ev.guild_card_number, decode_sjis(ev.data));
+4 -2
View File
@@ -23,6 +23,7 @@ public:
PlayerLobbyDataDCGC lobby_data;
PlayerInventory inventory;
PlayerDispDataDCPCV3 disp;
le_uint32_t level;
} __attribute__((packed));
struct Event {
@@ -65,7 +66,8 @@ public:
void add_player(
const PlayerLobbyDataDCGC& lobby_data,
const PlayerInventory& inventory,
const PlayerDispDataDCPCV3& disp);
const PlayerDispDataDCPCV3& disp,
uint32_t level);
void delete_player(uint8_t client_id);
void add_command(Event::Type type, const void* data, size_t size);
void add_command(Event::Type type, std::string&& data);
@@ -78,7 +80,7 @@ public:
void set_battle_end_timestamp();
private:
static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC5A;
static constexpr uint64_t SIGNATURE = 0x14C946D56D1DAC50;
static bool is_map_definition_event(const Event& ev);
+108 -19
View File
@@ -49,7 +49,7 @@ void Card::init() {
this->max_hp = this->def_entry->def.hp.stat;
this->current_hp = this->def_entry->def.hp.stat;
if (this->sc_card_ref == this->card_ref) {
int16_t rules_char_hp = this->server()->base()->map_and_rules1->rules.char_hp;
int16_t rules_char_hp = this->server()->map_and_rules->rules.char_hp;
int16_t base_char_hp = (rules_char_hp == 0) ? 15 : rules_char_hp;
int16_t hp = clamp<int16_t>(base_char_hp + this->def_entry->def.hp.stat, 1, 99);
this->max_hp = hp;
@@ -107,6 +107,7 @@ ssize_t Card::apply_abnormal_condition(
int16_t value,
int8_t dice_roll_value,
int8_t random_percent) {
auto log = this->server()->log_stack(string_printf("apply_abnormal_condition(%02hhX, @%04X, @%04X, %hd, %hhd, %hhd): ", def_effect_index, target_card_ref, sc_card_ref, value, dice_roll_value, random_percent));
ssize_t existing_cond_index;
for (size_t z = 0; z < this->action_chain.conditions.size(); z++) {
@@ -132,9 +133,13 @@ ssize_t Card::apply_abnormal_condition(
break;
}
}
log.debug("existing_cond_index < 0 (new condition) => cond_index = %zd", cond_index);
} else {
log.debug("existing_cond_index = %zd (existing condition)", existing_cond_index);
}
if (cond_index < 0) {
log.debug("no space for condition");
return -1;
}
@@ -142,10 +147,10 @@ ssize_t Card::apply_abnormal_condition(
auto& cond = this->action_chain.conditions[cond_index];
if ((eff.type == ConditionType::MV_BONUS) && (cond.type == ConditionType::MV_BONUS)) {
existing_cond_value = clamp<int16_t>(cond.value, -99, 99);
log.debug("MV_BONUS combines => existing_cond_value = %hd", existing_cond_value);
}
this->server()->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond(
cond, this->shared_from_this());
this->server()->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond(cond, this->shared_from_this());
cond.type = eff.type;
cond.card_ref = target_card_ref;
cond.condition_giver_card_ref = sc_card_ref;
@@ -178,7 +183,19 @@ ssize_t Card::apply_abnormal_condition(
cond.remaining_turns = atoi(&eff.arg1[1]);
}
string cond_str = cond.str();
log.debug("wrote condition %zd => %s", cond_index, cond_str.c_str());
this->server()->card_special->update_condition_orders(this->shared_from_this());
for (size_t z = 0; z < this->action_chain.conditions.size(); z++) {
if (this->action_chain.conditions[z].type == ConditionType::NONE) {
continue;
}
string cond_str = cond.str();
log.debug("sorted conditions: [%zu] => %s", z, cond_str.c_str());
}
return cond_index;
}
@@ -227,9 +244,12 @@ void Card::commit_attack(
G_ApplyConditionEffect_GC_Ep3_6xB4x06* cmd,
size_t strike_number,
int16_t* out_effective_damage) {
auto log = this->server()->log_stack(string_printf("commit_attack(@%04hX #%04hX, @%04hX #%04hX => %hd (str%zu)): ", this->get_card_ref(), this->get_card_id(), attacker_card->get_card_ref(), attacker_card->get_card_id(), damage, strike_number));
int16_t effective_damage = damage;
this->server()->card_special->adjust_attack_damage_due_to_conditions(
this->shared_from_this(), &effective_damage, attacker_card->get_card_ref());
log.debug("adjusted damage = %hd", effective_damage);
size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id);
for (size_t z = 0; z < num_assists; z++) {
@@ -247,29 +267,36 @@ void Card::commit_attack(
}
}
}
log.debug("after assists = %hd", effective_damage);
if (this->action_metadata.check_flag(0x10)) {
effective_damage = 0;
log.debug("flag 0x10 => effective damage = %hd", effective_damage);
}
auto attacker_ps = attacker_card->player_state();
attacker_ps->stats.damage_given += effective_damage;
this->player_state()->stats.damage_taken += effective_damage;
log.debug("updated stats");
this->current_hp = clamp<int16_t>(
this->current_hp - effective_damage, 0, this->max_hp);
this->current_hp = clamp<int16_t>(this->current_hp - effective_damage, 0, this->max_hp);
log.debug("hp set to %hd", this->current_hp);
if ((effective_damage > 0) &&
(attacker_ps->stats.max_attack_damage < effective_damage)) {
attacker_ps->stats.max_attack_damage = effective_damage;
log.debug("attacker new max damage %hd", effective_damage);
}
this->last_attack_final_damage = effective_damage;
log.debug("last attack final damage = %hd", effective_damage);
if (effective_damage > 0) {
this->card_flags = this->card_flags | 4;
log.debug("set flag 4");
}
if (this->current_hp < 1) {
this->destroy_set_card(attacker_card);
log.debug("card destroyed");
}
G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd_to_send;
@@ -358,7 +385,7 @@ void Card::destroy_set_card(shared_ptr<Card> attacker_card) {
}
}
if ((this->server()->base()->map_and_rules1->rules.hp_type == HPType::DEFEAT_TEAM) &&
if ((this->server()->map_and_rules->rules.hp_type == HPType::DEFEAT_TEAM) &&
(this->player_state()->get_sc_card().get() == this)) {
for (size_t set_index = 0; set_index < 8; set_index++) {
auto card = this->player_state()->get_set_card(set_index);
@@ -396,7 +423,7 @@ void Card::destroy_set_card(shared_ptr<Card> attacker_card) {
}
int32_t Card::error_code_for_move_to_location(const Location& loc) const {
if (this->player_state()->assist_flags & 0x80) {
if (this->player_state()->assist_flags & AssistFlag::IS_SKIPPING_TURN) {
return -0x76;
}
if (this->card_flags & 2) {
@@ -420,12 +447,18 @@ void Card::execute_attack(shared_ptr<Card> attacker_card) {
return;
}
auto log = this->server()->log_stack(string_printf("execute_attack(@%04X #%04X, @%04X #%04X): ", this->get_card_ref(), this->get_card_id(), attacker_card->get_card_ref(), attacker_card->get_card_id()));
this->card_flags = this->card_flags & 0xFFFFFFF3;
int16_t attack_ap = this->action_metadata.attack_bonus;
int16_t attack_tp = 0;
int16_t defense_power = this->compute_defense_power_for_attacker_card(attacker_card);
log.debug("ap=%hd, tp=%hd, defense=%hd", attack_ap, attack_tp, defense_power);
if ((attack_ap == 0) && !this->action_metadata.check_flag(0x20)) {
log.debug("ap == 0 and flag 0x20 not set");
return;
} else {
log.debug("ap != 0 or flag 0x20 set; continuing...");
}
G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd;
@@ -447,12 +480,15 @@ void Card::execute_attack(shared_ptr<Card> attacker_card) {
uint16_t attacker_card_ref = attacker_card->get_card_ref();
this->server()->card_special->compute_attack_ap(
this->shared_from_this(), &attack_ap, attacker_card_ref);
log.debug("computed ap %hd", attack_ap);
this->apply_ap_adjust_assists_to_attack(attacker_card, &attack_ap, &defense_power);
log.debug("assist adjusts ap=%hd, defense=%hd", attack_ap, defense_power);
int16_t raw_damage = attack_ap - defense_power;
// Note: The original code uses attack_tp here, even though it is always
// zero at this point
int16_t preliminary_damage = max<int16_t>(raw_damage, 0) - attack_tp;
this->last_attack_preliminary_damage = preliminary_damage;
log.debug("raw_damage=%hd, preliminary_damange=%hd", raw_damage, preliminary_damage);
uint16_t targeted_card_ref = this->get_card_ref();
uint32_t unknown_a9 = 0;
@@ -460,15 +496,20 @@ void Card::execute_attack(shared_ptr<Card> attacker_card) {
targeted_card_ref, 1, 0, attacker_card_ref, 0xFFFF, 0, &unknown_a9, 0xFF, 0, 0xFFFF);
if (!target) {
target = this->shared_from_this();
log.debug("target is not replaced");
} else {
log.debug("target replaced with @%04hX #%04hX", target->get_card_ref(), target->get_card_id());
}
if (unknown_a9 != 0) {
preliminary_damage = 0;
log.debug("a9 nonzero; preliminary_damage = 0");
}
if (!(this->card_flags & 2) &&
(!attacker_card || !(attacker_card->card_flags & 2))) {
this->server()->card_special->check_for_defense_interference(
attacker_card, this->shared_from_this(), &preliminary_damage);
log.debug("checked for defense interference");
}
cmd.effect.current_hp = min<int16_t>(attack_ap, 99);
@@ -476,6 +517,7 @@ void Card::execute_attack(shared_ptr<Card> attacker_card) {
cmd.effect.tp = attack_tp;
this->player_state()->stats.num_attacks_taken++;
if (!(target->card_flags & 2)) {
log.debug("flag 2 not set");
for (size_t strike_num = 0; strike_num < attacker_card->action_chain.chain.strike_count; strike_num++) {
int16_t final_effective_damage = 0;
target->commit_attack(
@@ -487,9 +529,11 @@ void Card::execute_attack(shared_ptr<Card> attacker_card) {
0, this->current_defense_power - final_effective_damage);
}
} else {
log.debug("flag 2 set; committing zero-damage attack");
target->commit_attack(0, attacker_card, &cmd, 0, nullptr);
}
if (this != target.get()) {
log.debug("target was replaced; committing zero-damage attack on original card");
this->commit_attack(0, attacker_card, &cmd, 0, nullptr);
}
@@ -515,6 +559,10 @@ uint16_t Card::get_card_ref() const {
return this->card_ref;
}
uint16_t Card::get_card_id() const {
return this->get_definition()->def.card_id;
}
uint8_t Card::get_client_id() const {
return this->client_id;
}
@@ -585,7 +633,7 @@ int32_t Card::move_to_location(const Location& loc) {
}
void Card::propagate_shared_hp_if_needed() {
if ((this->server()->base()->map_and_rules1->rules.hp_type == HPType::COMMON_HP) &&
if ((this->server()->map_and_rules->rules.hp_type == HPType::COMMON_HP) &&
((this->def_entry->def.type == CardType::HUNTERS_SC) || (this->def_entry->def.type == CardType::ARKZ_SC))) {
for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) {
auto other_ps = this->server()->player_states[other_client_id];
@@ -724,24 +772,34 @@ void Card::clear_action_chain_and_metadata_and_most_flags() {
void Card::compute_action_chain_results(
bool apply_action_conditions, bool ignore_this_card_ap_tp) {
auto log = this->server()->log_stack(string_printf("compute_action_chain_results(@%04hX #%04hX): ", this->get_card_ref(), this->get_card_id()));
this->action_chain.compute_attack_medium(this->server());
this->action_chain.chain.strike_count = 1;
this->action_chain.chain.ap_effect_bonus = 0;
this->action_chain.chain.tp_effect_bonus = 0;
log.debug("(initial) medium=%s, strike_count=%hhu, ap_effect_bonus=%hhd, tp_effect_bonus=%hhd",
name_for_attack_medium(this->action_chain.chain.attack_medium),
this->action_chain.chain.strike_count,
this->action_chain.chain.ap_effect_bonus,
this->action_chain.chain.tp_effect_bonus);
int16_t card_ap;
int16_t card_tp;
auto stat_swap_type = this->server()->card_special->compute_stat_swap_type(this->shared_from_this());
log.debug("stat_swap_type = %zu (0=none, 1=a/t, 2=a/h)", static_cast<size_t>(stat_swap_type));
this->server()->card_special->get_effective_ap_tp(
stat_swap_type, &card_ap, &card_tp, this->get_current_hp(), this->ap, this->tp);
log.debug("card_ap = %hd, card_tp = %hd", card_ap, card_tp);
int16_t effective_tp = card_tp;
int16_t effective_ap = card_ap;
int16_t effective_tp = card_tp;
for (size_t z = 0; (!ignore_this_card_ap_tp && (z < 8) && (z < this->action_chain.chain.attack_action_card_ref_count)); z++) {
auto ce = this->server()->definition_for_card_ref(this->action_chain.chain.attack_action_card_refs[z]);
if (ce) {
effective_ap += ce->def.ap.stat;
effective_tp += ce->def.tp.stat;
log.debug("(action card @%04hX) updated effective_ap = %hd, effective_tp = %hd", this->action_chain.chain.attack_action_card_refs[z].load(), effective_ap, effective_tp);
}
}
@@ -754,6 +812,8 @@ void Card::compute_action_chain_results(
stat_swap_type, &card_ap, &card_tp, card->get_current_hp(), card->ap, card->tp);
effective_ap += card_ap;
effective_tp += card_tp;
log.debug("(mag card set_index %zu @%04hX) updated effective_ap = %hd, effective_tp = %hd",
set_index, card->get_card_ref(), effective_ap, effective_tp);
}
}
}
@@ -763,18 +823,25 @@ void Card::compute_action_chain_results(
sc_card->compute_action_chain_results(apply_action_conditions, true);
effective_ap += sc_card->action_chain.chain.effective_ap + sc_card->action_chain.chain.ap_effect_bonus;
effective_tp += sc_card->action_chain.chain.effective_tp + sc_card->action_chain.chain.tp_effect_bonus;
log.debug("(item is attacking; adding SC stats) updated effective_ap = %hd, effective_tp = %hd",
effective_ap, effective_tp);
}
if (!this->action_chain.check_flag(0x10)) {
this->action_chain.chain.effective_ap = min<int16_t>(effective_ap, 99);
log.debug("set chain effective_ap = %hd", this->action_chain.chain.effective_ap);
}
if (!this->action_chain.check_flag(0x20)) {
this->action_chain.chain.effective_tp = min<int16_t>(effective_tp, 99);
log.debug("set chain effective_tp = %hd", this->action_chain.chain.effective_tp);
}
if (apply_action_conditions) {
this->server()->card_special->apply_action_conditions(
3, this->shared_from_this(), this->shared_from_this(), 1, nullptr);
log.debug("applied action conditions (1)");
} else {
log.debug("skipped applying action conditions (1)");
}
size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id);
@@ -887,18 +954,27 @@ void Card::compute_action_chain_results(
int16_t damage = 0;
if (this->action_chain.chain.attack_medium == AttackMedium::TECH) {
damage = this->action_chain.chain.effective_tp + this->action_chain.chain.tp_effect_bonus;
log.debug("(tech) damage = %hhd (eff) + %hhd (bonus) = %hd", this->action_chain.chain.effective_tp, this->action_chain.chain.tp_effect_bonus, damage);
} else if (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL) {
damage = this->action_chain.chain.effective_ap + this->action_chain.chain.ap_effect_bonus;
log.debug("(physical) damage = %hhd (eff) + %hhd (bonus) = %hd", this->action_chain.chain.effective_ap, this->action_chain.chain.ap_effect_bonus, damage);
} else {
log.debug("(unknown attack medium) damage = 0");
}
this->action_chain.chain.damage = min<int16_t>(
damage * this->action_chain.chain.damage_multiplier, 99);
log.debug("overall chain damage = %hd (base) * %hhd (mult) = %hhd", damage, this->action_chain.chain.damage_multiplier, this->action_chain.chain.damage);
if (apply_action_conditions) {
this->server()->card_special->apply_action_conditions(
3, this->shared_from_this(), this->shared_from_this(), 2, nullptr);
0x03, this->shared_from_this(), this->shared_from_this(), 2, nullptr);
log.debug("applied action conditions (2)");
if (this->action_chain.check_flag(0x100)) {
this->action_chain.chain.damage = min<int16_t>(this->action_chain.chain.damage + 5, 99);
log.debug("(has flag 0x100) chain damage = %hhd", this->action_chain.chain.damage);
}
} else {
log.debug("applied action conditions (2)");
}
num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->get_client_id());
@@ -983,6 +1059,7 @@ void Card::unknown_80235B10() {
}
void Card::unknown_80236374(shared_ptr<Card> other_card, const ActionState* as) {
auto log = this->server()->log_stack(string_printf("unknown_80236374(@%04hX #%04hX, @%04hX #%04hX): ", this->get_card_ref(), this->get_card_id(), other_card->get_card_ref(), other_card->get_card_id()));
auto check_card = [&](shared_ptr<Card> card) -> void {
if (card) {
if (!card->unknown_80236554(other_card, as)) {
@@ -1112,6 +1189,16 @@ bool Card::is_guard_item() const {
}
bool Card::unknown_80236554(shared_ptr<Card> other_card, const ActionState* as) {
auto log = this->server()->log_stack(other_card
? string_printf("unknown_80236554(@%04hX #%04hX, @%04hX #%04hX): ", this->get_card_ref(), this->get_card_id(), other_card->get_card_ref(), other_card->get_card_id())
: string_printf("unknown_80236554(@%04hX #%04hX, null): ", this->get_card_ref(), this->get_card_id()));
if (as) {
string as_str = as->str();
log.debug("as = %s", as_str.c_str());
} else {
log.debug("as = null");
}
bool ret = false;
int16_t attack_bonus = 0;
@@ -1121,6 +1208,7 @@ bool Card::unknown_80236554(shared_ptr<Card> other_card, const ActionState* as)
if (other_card->action_chain.chain.target_card_refs[z] == this->get_card_ref()) {
attack_bonus = other_card->action_chain.chain.damage;
ret = true;
log.debug("attack_bonus = %hd (matched other_card->action_chain.chain.target_card_refs)", attack_bonus);
break;
}
}
@@ -1128,6 +1216,7 @@ bool Card::unknown_80236554(shared_ptr<Card> other_card, const ActionState* as)
for (size_t z = 0; (z < 4 * 9) && (as->target_card_refs[z] != 0xFFFF); z++) {
if (as->target_card_refs[z] == this->get_card_ref()) {
attack_bonus = other_card->action_chain.chain.damage;
log.debug("attack_bonus = %hd (matched as->target_card_refs)", attack_bonus);
ret = true;
break;
}
@@ -1136,23 +1225,22 @@ bool Card::unknown_80236554(shared_ptr<Card> other_card, const ActionState* as)
}
this->action_metadata.attack_bonus = max<int16_t>(attack_bonus, 0);
log.debug("attack_bonus = %hhd", this->action_metadata.attack_bonus);
this->last_attack_preliminary_damage = 0;
this->last_attack_final_damage = 0;
log.debug("last attack damage stats cleared");
if (other_card) {
this->server()->card_special->apply_action_conditions(
3, other_card, this->shared_from_this(), 0x20, as);
this->server()->card_special->apply_action_conditions(
0x17, other_card, this->shared_from_this(), 0x40, as);
this->server()->card_special->apply_action_conditions(0x03, other_card, this->shared_from_this(), 0x20, as);
this->server()->card_special->apply_action_conditions(0x17, other_card, this->shared_from_this(), 0x40, as);
if (other_card->action_chain.check_flag(0x20000)) {
this->action_metadata.attack_bonus = 0;
return ret;
}
}
if (!(this->card_flags & 2)) {
return ret;
if (this->card_flags & 2) {
this->action_metadata.attack_bonus = 0;
}
this->action_metadata.attack_bonus = 0;
return ret;
}
@@ -1174,8 +1262,9 @@ void Card::unknown_802362D8(shared_ptr<Card> other_card) {
}
}
void Card::unknown_80237734() {
if (!this->action_chain.unknown_8024DEC4()) {
void Card::apply_attack_result() {
auto log = this->server()->log_stack(string_printf("apply_attack_result(@%04hX #%04hX): ", this->get_card_ref(), this->get_card_id()));
if (!this->action_chain.can_apply_attack()) {
return;
}
+2 -2
View File
@@ -10,7 +10,6 @@
namespace Episode3 {
class ServerBase;
class Server;
class PlayerState;
@@ -60,6 +59,7 @@ public:
uint16_t* out_value) const;
std::shared_ptr<const CardIndex::CardEntry> get_definition() const;
uint16_t get_card_ref() const;
uint16_t get_card_id() const;
uint8_t get_client_id() const;
uint8_t get_current_hp() const;
uint8_t get_max_hp() const;
@@ -90,7 +90,7 @@ public:
bool is_guard_item() const;
bool unknown_80236554(std::shared_ptr<Card> other_card, const ActionState* as);
void unknown_802362D8(std::shared_ptr<Card> other_card);
void unknown_80237734();
void apply_attack_result();
private:
std::weak_ptr<Server> w_server;
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -87,8 +87,9 @@ public:
AttackEnvStats();
void clear();
void print(FILE* stream) const;
uint32_t at(size_t offset) const;
uint32_t at(size_t index) const;
} __attribute__((packed));
CardSpecial(std::shared_ptr<Server> server);
@@ -164,7 +165,7 @@ public:
ConditionType cond_type, uint16_t card_ref) const;
size_t count_action_cards_with_condition_for_current_attack(
std::shared_ptr<const Card> card, ConditionType cond_type, uint16_t card_ref) const;
size_t count_cards_with_card_id_set_by_player_except_card_ref(
size_t count_cards_with_card_id_except_card_ref(
uint16_t card_id, uint16_t card_ref) const;
std::vector<std::shared_ptr<const Card>> get_all_set_cards_by_team_and_class(
CardClass card_class, uint8_t team_id, bool exclude_destroyed_cards) const;
@@ -194,7 +195,7 @@ public:
int16_t expr_value,
int16_t unknown_p5,
ConditionType cond_type,
uint unknown_p7,
uint32_t unknown_p7,
uint16_t attacker_card_ref);
const Condition* find_condition_with_parameters(
std::shared_ptr<const Card> card,
@@ -275,8 +276,8 @@ public:
std::shared_ptr<const Card> attacker_card,
std::shared_ptr<Card> target_card,
int16_t* inout_unknown_p4);
void unknown_8024C2B0(
uint32_t when,
void evaluate_and_apply_effects(
uint8_t when,
uint16_t set_card_ref,
const ActionState& as,
uint16_t sc_card_ref,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+43 -1
View File
@@ -11,7 +11,7 @@ NameEntry::NameEntry() {
void NameEntry::clear() {
this->client_id = 0xFF;
this->present = 0;
this->unused_by_server = 0;
this->is_cpu_player = 0;
this->unused = 0;
}
@@ -273,4 +273,46 @@ void DeckState::shuffle() {
}
}
static const char* name_for_card_state(DeckState::CardState st) {
switch (st) {
case DeckState::CardState::DRAWABLE:
return "DRAWABLE";
case DeckState::CardState::STORY_CHARACTER:
return "STORY_CHARACTER";
case DeckState::CardState::IN_HAND:
return "IN_HAND";
case DeckState::CardState::IN_PLAY:
return "IN_PLAY";
case DeckState::CardState::DISCARDED:
return "DISCARDED";
case DeckState::CardState::INVALID:
return "INVALID";
default:
return "__UNKNOWN__";
}
}
void DeckState::print(FILE* stream, std::shared_ptr<const CardIndex> card_index) const {
fprintf(stream, "DeckState: client_id=%hhu draw_index=%hhu card_ref_base=@%04hX shuffle=%s loop=%s\n",
this->client_id, this->draw_index, this->card_ref_base, this->shuffle_enabled ? "true" : "false", this->loop_enabled ? "true" : "false");
for (size_t z = 0; z < 31; z++) {
const auto& e = this->entries[z];
shared_ptr<const CardIndex::CardEntry> ce;
if (card_index) {
try {
ce = card_index->definition_for_id(e.card_id);
} catch (const out_of_range&) {
}
}
if (ce) {
string name = ce->def.en_name;
fprintf(stream, " (%02zu) index=%02hhX ref=@%04hX card_id=#%04hX \"%s\" %s\n",
z, e.deck_index, this->card_refs[z], e.card_id, name.c_str(), name_for_card_state(e.state));
} else {
fprintf(stream, " (%02zu) index=%02hhX ref=@%04hX card_id=#%04hX %s\n",
z, e.deck_index, this->card_refs[z], e.card_id, name_for_card_state(e.state));
}
}
}
} // namespace Episode3
+5 -2
View File
@@ -6,6 +6,7 @@
#include "../PSOEncryption.hh"
#include "../Text.hh"
#include "DataIndexes.hh"
namespace Episode3 {
@@ -13,7 +14,7 @@ struct NameEntry {
parray<char, 0x10> name;
uint8_t client_id;
uint8_t present;
uint8_t unused_by_server;
uint8_t is_cpu_player;
uint8_t unused;
NameEntry();
@@ -23,7 +24,7 @@ struct NameEntry {
struct DeckEntry {
ptext<char, 0x10> name;
le_uint32_t team_id;
parray<le_uint16_t, 0x1F> card_ids;
parray<le_uint16_t, 31> card_ids;
// If the following flag is not set to 3, then the God Whim assist effect can
// use cards that are hidden from the player during deck building. The client
// always sets this to 3, and it's not clear why this even exists.
@@ -93,6 +94,8 @@ public:
void shuffle();
void do_mulligan();
void print(FILE* stream, std::shared_ptr<const CardIndex> card_index = nullptr) const;
private:
struct CardEntry {
uint16_t card_id;
-1
View File
@@ -47,7 +47,6 @@ void MapAndRulesState::clear() {
this->map_number = 0;
this->unused4 = 0;
this->rules.clear();
this->unused5 = 0;
}
bool MapAndRulesState::loc_is_within_bounds(uint8_t x, uint8_t y) const {
-1
View File
@@ -34,7 +34,6 @@ struct MapAndRulesState {
le_uint32_t map_number;
uint32_t unused4;
Rules rules;
uint32_t unused5;
MapAndRulesState();
void clear();
File diff suppressed because it is too large Load Diff
+27 -1
View File
@@ -12,9 +12,33 @@
namespace Episode3 {
class ServerBase;
class Server;
enum AssistFlag : uint32_t {
// Note: This enum is a uint32_t even though only 16 bits are used because
// the corresponding field in the protocol is a 32-bit field. There may also
// be bits used only by the client which are not documented here.
// clang-format off
READY_TO_END_PHASE = 0x0001,
DICE_WERE_EXCHANGED = 0x0002,
HAS_WON_BATTLE = 0x0004,
READY_TO_END_STARTER_ROLL_PHASE = 0x0008,
FIXED_RANGE = 0x0010,
SUMMONING_IS_FREE = 0x0020,
LIMIT_MOVE_TO_1 = 0x0040,
IS_SKIPPING_TURN = 0x0080,
IMMORTAL = 0x0100,
SAME_CARD_BANNED = 0x0200,
CANNOT_SET_FIELD_CHARACTERS = 0x0400,
WINNER_DECIDED_BY_DEFEAT = 0x0800,
WINNER_DECIDED_BY_RANDOM = 0x1000,
READY_TO_END_ACTION_PHASE = 0x2000,
BATTLE_DID_NOT_END_DUE_TO_TIME_LIMIT = 0x4000,
ELIGIBLE_FOR_DICE_BOOST = 0x8000,
// clang-format on
};
class PlayerState : public std::enable_shared_from_this<PlayerState> {
public:
PlayerState(uint8_t client_id, std::shared_ptr<Server> server);
@@ -98,9 +122,11 @@ public:
bool subtract_or_check_atk_or_def_points_for_action(
const ActionState& pa, bool deduct_points);
void subtract_atk_points(uint8_t cost);
G_UpdateHand_GC_Ep3_6xB4x02 prepare_6xB4x02() const;
void update_hand_and_equip_state_and_send_6xB4x02_if_needed(
bool always_send = false);
void set_random_assist_card_from_hand_for_free();
G_UpdateShortStatuses_GC_Ep3_6xB4x04 prepare_6xB4x04() const;
void send_6xB4x04_if_needed(bool always_send = false);
std::vector<uint16_t> get_card_refs_within_range_from_all_players(
const parray<uint8_t, 9 * 9>& range,
+44 -8
View File
@@ -477,7 +477,7 @@ void ActionChainWithConds::set_action_subphase_from_card(
this->chain.action_subphase = card->server()->get_current_action_subphase();
}
bool ActionChainWithConds::unknown_8024DEC4() const {
bool ActionChainWithConds::can_apply_attack() const {
return this->check_flag(4) ? false : (this->chain.target_card_ref_count != 0);
}
@@ -609,7 +609,7 @@ std::string HandAndEquipState::str() const {
"assist_flags=%08" PRIX32 ", hand_refs=%s, "
"assist_ref=@%04hX, set_refs=%s, sc_ref=@%04hX, "
"hand_refs2=%s, set_refs2=%s, assist_ref2=@%04hX, "
"assist_set_num=%hu, assist_card_id=%04hX, "
"assist_set_num=%hu, assist_card_id=#%04hX, "
"assist_turns=%hhu, assit_dely=%hhu, atk_bonus=%hhu, "
"def_bonus=%hhu, u2=[%hhu, %hhu]]",
this->dice_results[0],
@@ -759,38 +759,74 @@ const char* PlayerBattleStats::name_for_rank(uint8_t rank) {
return RANK_NAMES[rank];
}
bool is_card_within_range(
static bool is_card_within_range(
const parray<uint8_t, 9 * 9>& range,
const Location& anchor_loc,
const CardShortStatus& ss) {
const CardShortStatus& ss,
PrefixedLogger* log) {
if (ss.card_ref == 0xFFFF) {
if (log) {
log->debug("is_card_within_range: (false) ss.card_ref missing");
}
return false;
}
if (range[0] == 2) {
if (log) {
log->debug("is_card_within_range: (true) range is entire field");
}
return true;
}
if ((ss.loc.x < anchor_loc.x - 4) || (ss.loc.x > anchor_loc.x + 4)) {
if (log) {
log->debug("is_card_within_range: (false) outside x range (ss.loc.x=%hhu, anchor_loc.x=%hhu)", ss.loc.x, anchor_loc.x);
}
return false;
}
if ((ss.loc.y < anchor_loc.y - 4) || (ss.loc.y > anchor_loc.y + 4)) {
if (log) {
log->debug("is_card_within_range: (false) outside y range (ss.loc.y=%hhu, anchor_loc.y=%hhu)", ss.loc.y, anchor_loc.y);
}
return false;
}
return (range[(ss.loc.x - anchor_loc.x) + ((ss.loc.y - anchor_loc.y) + 4) * 9 + 4] != 0);
uint8_t y_index = (ss.loc.y - anchor_loc.y) + 4;
uint8_t x_index = (ss.loc.x - anchor_loc.x) + 4;
bool ret = (range[y_index * 9 + x_index] != 0);
if (log) {
log->debug("is_card_within_range: (%s) (ss.loc=(%hhu,%hhu), anchor_loc=(%hhu,%hhu), indexes=(%hhu,%hhu))",
ret ? "true" : "false", ss.loc.x, ss.loc.y, anchor_loc.x, anchor_loc.y, x_index, y_index);
}
return ret;
}
vector<uint16_t> get_card_refs_within_range(
const parray<uint8_t, 9 * 9>& range,
const Location& loc,
const parray<CardShortStatus, 0x10>& short_statuses) {
const parray<CardShortStatus, 0x10>& short_statuses,
PrefixedLogger* log) {
vector<uint16_t> ret;
if (is_card_within_range(range, loc, short_statuses[0])) {
if (is_card_within_range(range, loc, short_statuses[0], log)) {
if (log) {
log->debug("get_card_refs_within_range: sc card @%04hX within range", short_statuses[0].card_ref.load());
}
ret.emplace_back(short_statuses[0].card_ref);
} else {
if (log) {
log->debug("get_card_refs_within_range: sc card @%04hX not within range", short_statuses[0].card_ref.load());
}
}
for (size_t card_index = 7; card_index < 15; card_index++) {
const auto& ss = short_statuses[card_index];
if (is_card_within_range(range, loc, ss)) {
if (is_card_within_range(range, loc, ss, log)) {
if (log) {
log->debug("get_card_refs_within_range: card @%04hX within range", ss.card_ref.load());
}
ret.emplace_back(ss.card_ref);
} else {
if (log) {
log->debug("get_card_refs_within_range: card @%04hX not within range", ss.card_ref.load());
}
}
}
return ret;
+3 -3
View File
@@ -9,7 +9,6 @@
namespace Episode3 {
class ServerBase;
class Server;
class Card;
@@ -160,7 +159,7 @@ struct ActionChainWithConds {
uint16_t* out_value) const;
void set_action_subphase_from_card(std::shared_ptr<const Card> card);
bool unknown_8024DEC4() const;
bool can_apply_attack() const;
std::string str() const;
} __attribute__((packed));
@@ -270,6 +269,7 @@ struct PlayerBattleStats {
std::vector<uint16_t> get_card_refs_within_range(
const parray<uint8_t, 9 * 9>& range,
const Location& loc,
const parray<CardShortStatus, 0x10>& short_statuses);
const parray<CardShortStatus, 0x10>& short_statuses,
PrefixedLogger* log = nullptr);
} // namespace Episode3
+98 -103
View File
@@ -11,7 +11,14 @@ void compute_effective_range(
shared_ptr<const CardIndex> card_index,
uint16_t card_id,
const Location& loc,
shared_ptr<const MapAndRulesState> map_and_rules) {
shared_ptr<const MapAndRulesState> map_and_rules,
PrefixedLogger* log) {
if (log && log->should_log(LogLevel::DEBUG)) {
string loc_str = loc.str();
log->debug("compute_effective_range: card_id=#%04hX, loc=%s", card_id, loc_str.c_str());
log->debug("compute_effective_range: map_and_rules->map:");
map_and_rules->map.print(stderr);
}
ret.clear(0);
parray<uint32_t, 6> range_def;
@@ -29,10 +36,16 @@ void compute_effective_range(
range_def[z] = ce->def.range[z];
}
}
if (log) {
log->debug("compute_effective_range: range_def: %05" PRIX32 " %05" PRIX32 " %05" PRIX32 " %05" PRIX32 " %05" PRIX32 " %05" PRIX32, range_def[0], range_def[1], range_def[2], range_def[3], range_def[4], range_def[5]);
}
if (range_def[0] == 0x000FFFFF) {
// Entire field
ret.clear(2);
if (log) {
log->debug("compute_effective_range: entire field (2)");
}
return;
}
@@ -46,82 +59,54 @@ void compute_effective_range(
row >>= 4;
}
}
if (log) {
for (size_t y = 0; y < 9; y++) {
log->debug("compute_effective_range: decoded_range: %hhX %hhX %hhX %hhX %hhX %hhX %hhX %hhX %hhX",
decoded_range[y * 9 + 0], decoded_range[y * 9 + 1], decoded_range[y * 9 + 2], decoded_range[y * 9 + 3], decoded_range[y * 9 + 4], decoded_range[y * 9 + 5], decoded_range[y * 9 + 6], decoded_range[y * 9 + 7], decoded_range[y * 9 + 8]);
}
}
switch (loc.direction) {
case Direction::LEFT:
for (int16_t y = 0; y < 9; y++) {
int16_t map_y = loc.y + y - 4;
if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.height))) {
for (int16_t x = 0; x < 9; x++) {
int16_t map_x = loc.x + x - 4;
if (!map_and_rules || ((map_x >= 0) && (map_x < map_and_rules->map.width))) {
ret[y * 9 + x] = decoded_range[(8 - x) * 9 + y];
} else {
for (int16_t y = 0; y < 9; y++) {
int16_t map_y = y + loc.y - 4;
if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.height))) {
for (int16_t x = 0; x < 9; x++) {
int16_t map_x = x + loc.x - 4;
if (!map_and_rules || ((map_x >= 0) && (map_x < map_and_rules->map.width))) {
int16_t up_x, up_y;
switch (loc.direction) {
case Direction::LEFT:
up_x = y;
up_y = 9 - x - 1;
break;
}
case Direction::RIGHT:
up_x = 9 - y - 1;
up_y = x;
break;
case Direction::UP:
up_x = x;
up_y = y;
break;
case Direction::DOWN:
up_x = 9 - x - 1;
up_y = 9 - y - 1;
break;
default:
throw logic_error("invalid direction");
}
ret[y * 9 + x] = decoded_range[up_y * 9 + up_x];
if (log) {
log->debug("compute_effective_range: x=%hd y=%hd up_x=%hd up_y=%hd v=%hhX", x, y, up_x, up_y, ret[y * 9 + x]);
}
} else {
break;
}
}
break;
}
}
case Direction::RIGHT:
for (int16_t y = 0; y < 9; y++) {
int16_t map_y = loc.y + y - 4;
if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.height))) {
for (int16_t x = 0; x < 9; x++) {
int16_t map_x = loc.x + x - 4;
if (!map_and_rules || ((map_x >= 0) && (map_x < map_and_rules->map.width))) {
ret[y * 9 + x] = decoded_range[((x * 9) - y) + 8];
} else {
break;
}
}
} else {
break;
}
}
break;
case Direction::UP:
for (int16_t y = 0; y < 9; y++) {
int16_t map_y = loc.y + y - 4;
if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.height))) {
for (int16_t x = 0; x < 9; x++) {
int16_t map_x = loc.x + x - 4;
if (!map_and_rules || ((map_x >= 0) && (map_x < map_and_rules->map.width))) {
ret[y * 9 + x] = decoded_range[y * 9 + x];
} else {
break;
}
}
} else {
break;
}
}
break;
case Direction::DOWN:
for (int16_t y = 0; y < 9; y++) {
int16_t map_y = loc.y + y - 4;
if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.height))) {
for (int16_t x = 0; x < 9; x++) {
int16_t map_y = loc.x + x - 4;
if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.width))) {
ret[y * 9 + x] = decoded_range[((8 - y) * 9 - x) + 8];
} else {
break;
}
}
} else {
break;
}
}
break;
default:
break;
if (log) {
for (size_t y = 0; y < 9; y++) {
log->debug("compute_effective_range: ret: %hhX %hhX %hhX %hhX %hhX %hhX %hhX %hhX %hhX",
ret[y * 9 + 0], ret[y * 9 + 1], ret[y * 9 + 2], ret[y * 9 + 3], ret[y * 9 + 4], ret[y * 9 + 5], ret[y * 9 + 6], ret[y * 9 + 7], ret[y * 9 + 8]);
}
}
}
@@ -602,7 +587,7 @@ bool RulerServer::card_ref_can_move(
return false;
}
if ((this->hand_and_equip_states[client_id]->assist_flags & 0x80)) {
if ((this->hand_and_equip_states[client_id]->assist_flags & AssistFlag::IS_SKIPPING_TURN)) {
return false;
}
@@ -685,7 +670,7 @@ bool RulerServer::card_ref_or_any_set_card_has_condition_46(
}
uint8_t client_id = client_id_for_card_ref(card_ref);
if (this->hand_and_equip_states[client_id]->assist_flags & 0x100) {
if (this->hand_and_equip_states[client_id]->assist_flags & AssistFlag::IMMORTAL) {
auto ce = this->definition_for_card_id(card_id);
if (!ce) {
return false;
@@ -918,6 +903,8 @@ bool RulerServer::check_usability_or_condition_apply(
uint8_t def_effect_index,
bool is_item_usability_check,
AttackMedium attack_medium) const {
auto log = this->server()->log_stack(string_printf("check_usability_or_condition_apply(%02hhX, #%04hX, %02hhX, #%04hX, #%04hX, %02hhX, %s, %s): ", client_id1, card_id1, client_id2, card_id2, card_id3, def_effect_index, is_item_usability_check ? "true" : "false", name_for_attack_medium(attack_medium)));
if (static_cast<uint8_t>(attack_medium) & 0x80) {
attack_medium = AttackMedium::UNKNOWN;
}
@@ -926,21 +913,25 @@ bool RulerServer::check_usability_or_condition_apply(
auto ce2 = this->definition_for_card_id(card_id2);
auto ce3 = this->definition_for_card_id(card_id3);
if (!ce1) {
log.debug("ce1 missing");
return false;
}
if ((ce1->def.type == CardType::ITEM) && this->card_id_is_boss_sc(card_id2)) {
log.debug("ce1 is item and card_id2 is boss sc");
return false;
}
CriterionCode criterion_code;
if (def_effect_index & 0xFF) {
if (def_effect_index == 0xFF) {
criterion_code = ce1->def.usable_criterion;
} else {
if (def_effect_index > 2) {
log.debug("invalid def_effect_index");
return false;
}
criterion_code = ce1->def.effects[def_effect_index].apply_criterion;
}
log.debug("criterion_code=%s", name_for_criterion_code(criterion_code));
// For item usability checks, prevent criteria that depend on player
// positioning/team setup
@@ -948,9 +939,10 @@ bool RulerServer::check_usability_or_condition_apply(
((criterion_code == CriterionCode::SAME_TEAM) ||
(criterion_code == CriterionCode::SAME_PLAYER) ||
(criterion_code == CriterionCode::SAME_TEAM_NOT_SAME_PLAYER) ||
(criterion_code == CriterionCode::UNKNOWN_07) ||
(criterion_code == CriterionCode::FC) ||
(criterion_code == CriterionCode::NOT_SC) ||
(criterion_code == CriterionCode::SC))) {
log.debug("criterion is forbidden");
criterion_code = CriterionCode::NONE;
}
@@ -998,14 +990,13 @@ bool RulerServer::check_usability_or_condition_apply(
return true;
}
break;
case CriterionCode::UNKNOWN_07:
// Like NOT_SC, but for ce3 instead of ce2
if (ce3 && (ce3->def.type != CardType::HUNTERS_SC) && (ce3->def.type != CardType::ARKZ_SC)) {
case CriterionCode::FC:
if (!ce3 || ((ce3->def.type != CardType::HUNTERS_SC) && (ce3->def.type != CardType::ARKZ_SC))) {
return ret;
}
break;
case CriterionCode::NOT_SC:
if (ce2 && (ce2->def.type != CardType::HUNTERS_SC) && (ce2->def.type != CardType::ARKZ_SC)) {
if (!ce2 || ((ce2->def.type != CardType::HUNTERS_SC) && (ce2->def.type != CardType::ARKZ_SC))) {
return ret;
}
break;
@@ -1019,7 +1010,7 @@ bool RulerServer::check_usability_or_condition_apply(
return ret;
}
break;
case CriterionCode::HUNTER_HUMAN_SC: {
case CriterionCode::HUNTER_NON_ANDROID_SC: {
static const unordered_set<uint16_t> card_ids = {
0x0001, // Orland
0x0002, // Kranz
@@ -1073,6 +1064,7 @@ bool RulerServer::check_usability_or_condition_apply(
0x02B1, // H-RAcaseal
0x02B3, // H-FOmarl
0x02B5, // H-FOnewearl
// Note: Seems like 0x02CD (H-RAmarl) should be here, but she isn't.
0x02CE, // H-FOmarl
0x02CF, // H-HUnewearl
0x02D1, // H-RAmarl
@@ -1082,7 +1074,7 @@ bool RulerServer::check_usability_or_condition_apply(
};
return ret && card_ids.count(card_id2);
}
case CriterionCode::HUNTER_HU_OR_FO_CLASS_HUMAN_SC: {
case CriterionCode::HUNTER_NON_RA_CLASS_HUMAN_SC: {
static const unordered_set<uint16_t> card_ids = {
0x0001, // Orland
0x0003, // Ino'lis
@@ -1116,7 +1108,7 @@ bool RulerServer::check_usability_or_condition_apply(
};
return ret && card_ids.count(card_id2);
}
case CriterionCode::UNKNOWN_10: {
case CriterionCode::HUNTER_NON_RA_CLASS_NON_NEWMAN_SC: {
static const unordered_set<uint16_t> card_ids = {
0x0001, // Orland
0x0003, // Ino'lis
@@ -1136,7 +1128,7 @@ bool RulerServer::check_usability_or_condition_apply(
};
return ret && card_ids.count(card_id2);
}
case CriterionCode::UNKNOWN_11: {
case CriterionCode::HUNTER_NON_NEWMAN_NON_FORCE_MALE_SC: {
static const unordered_set<uint16_t> card_ids = {
0x0001, // Orland
0x0002, // Kranz
@@ -1147,6 +1139,7 @@ bool RulerServer::check_usability_or_condition_apply(
0x02AE, // H-RAmar
0x02B0, // H-RAcast
0x02CC, // H-HUmar
// Seems like H-RAmarl shouldn't be here, but she is.
0x02CD, // H-RAmarl
0x02D0, // H-RAcast
0x02D7, // H-HUcast
@@ -1239,7 +1232,7 @@ bool RulerServer::check_usability_or_condition_apply(
};
return ret && card_ids.count(card_id2);
}
case CriterionCode::HUNTER_FEMALE_HUMAN_SC: {
case CriterionCode::HUNTER_HUMAN_FEMALE_SC: {
static const unordered_set<uint16_t> card_ids = {
0x0003, // Ino'lis
0x0004, // Sil'fer
@@ -1298,7 +1291,7 @@ bool RulerServer::check_usability_or_condition_apply(
return ret;
}
break;
case CriterionCode::UNKNOWN_20:
case CriterionCode::NON_PHYSICAL_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC:
if ((attack_medium != AttackMedium::PHYSICAL) && (attack_medium != AttackMedium::UNKNOWN)) {
return false;
}
@@ -1306,7 +1299,7 @@ bool RulerServer::check_usability_or_condition_apply(
return ret;
}
break;
case CriterionCode::UNKNOWN_21:
case CriterionCode::NON_PHYSICAL_NON_TECH_ATTACK_MEDIUM_NON_SC:
if ((attack_medium != AttackMedium::PHYSICAL) && (attack_medium != AttackMedium::TECH)) {
return false;
}
@@ -1314,7 +1307,7 @@ bool RulerServer::check_usability_or_condition_apply(
return ret;
}
break;
case CriterionCode::UNKNOWN_22:
case CriterionCode::NON_PHYSICAL_NON_TECH_NON_UNKNOWN_ATTACK_MEDIUM_NON_SC:
if ((attack_medium != AttackMedium::UNKNOWN) && (attack_medium != AttackMedium::PHYSICAL) && (attack_medium != AttackMedium::TECH)) {
return false;
}
@@ -1323,6 +1316,7 @@ bool RulerServer::check_usability_or_condition_apply(
}
}
log.debug("default return (false)");
return false;
}
@@ -1356,8 +1350,8 @@ uint16_t RulerServer::compute_attack_or_defense_costs(
sc_card_ref_if_item = this->short_statuses[client_id]->at(0).card_ref;
}
if (this->find_condition_on_card_ref(pa.attacker_card_ref, ConditionType::UNKNOWN_15) ||
this->find_condition_on_card_ref(sc_card_ref_if_item, ConditionType::UNKNOWN_15)) {
if (this->find_condition_on_card_ref(pa.attacker_card_ref, ConditionType::ADD_1_TO_MV_COST) ||
this->find_condition_on_card_ref(sc_card_ref_if_item, ConditionType::ADD_1_TO_MV_COST)) {
cost_bias = 1;
}
@@ -1634,7 +1628,7 @@ int32_t RulerServer::error_code_for_client_setting_card(
return -0x7D;
}
if (hes->assist_flags & 0x80) {
if (hes->assist_flags & AssistFlag::IS_SKIPPING_TURN) {
return -0x76;
}
@@ -1643,7 +1637,7 @@ int32_t RulerServer::error_code_for_client_setting_card(
}
uint16_t card_id = this->card_id_for_card_ref(card_ref);
if ((hes->assist_flags & 0x200) && (card_id != 0xFFFF)) {
if ((hes->assist_flags & AssistFlag::SAME_CARD_BANNED) && (card_id != 0xFFFF)) {
for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) {
auto other_hes = this->hand_and_equip_states[other_client_id];
if (!other_hes) {
@@ -1685,7 +1679,7 @@ int32_t RulerServer::error_code_for_client_setting_card(
return -0x75;
}
} else if (hes->assist_flags & 0x400) { // Item or creature
} else if (hes->assist_flags & AssistFlag::CANNOT_SET_FIELD_CHARACTERS) { // Item or creature
return -0x76;
}
@@ -1709,6 +1703,7 @@ int32_t RulerServer::error_code_for_client_setting_card(
for (size_t z = 1; z < 7; z++) {
if (short_statuses->at(z).card_ref == card_ref) {
card_in_hand = true;
break;
}
}
if (!card_in_hand) {
@@ -2043,12 +2038,12 @@ bool RulerServer::get_creature_summon_area(
loc.direction = static_cast<Direction>(
(this->map_and_rules->start_facing_directions >> ((client_id & 0x0F) << 2)) & 0x000F);
switch (loc.direction) {
case Direction::LEFT:
case Direction::RIGHT:
loc.x = 1;
loc.y = 0;
region_size = this->map_and_rules->map.width - 3;
break;
case Direction::RIGHT:
case Direction::LEFT:
loc.x = this->map_and_rules->map.width - 2;
loc.y = 0;
region_size = this->map_and_rules->map.width - 3;
@@ -2120,11 +2115,11 @@ ssize_t RulerServer::get_path_cost(
ssize_t cost_penalty) const {
for (size_t x = 0; x < 9; x++) {
const auto& cond = chain.conditions[x];
if (cond.type == ConditionType::UNKNOWN_12) {
if (cond.type == ConditionType::SET_MV_COST_TO_0) {
path_length = 0;
} else if (cond.type == ConditionType::UNKNOWN_15) {
} else if (cond.type == ConditionType::ADD_1_TO_MV_COST) {
path_length++;
} else if (cond.type == ConditionType::HASTE) {
} else if (cond.type == ConditionType::SCALE_MV_COST) {
path_length *= cond.value;
}
}
@@ -2157,7 +2152,7 @@ bool RulerServer::is_attack_valid(const ActionState& pa) {
}
if (this->hand_and_equip_states[client_id] &&
(this->hand_and_equip_states[client_id]->assist_flags & 0x80)) {
(this->hand_and_equip_states[client_id]->assist_flags & AssistFlag::IS_SKIPPING_TURN)) {
this->error_code3 = -0x70;
return false;
}
@@ -2285,7 +2280,7 @@ bool RulerServer::is_attack_or_defense_valid(const ActionState& pa) {
return false;
}
if (hes->assist_flags & 0x80) {
if (hes->assist_flags & AssistFlag::IS_SKIPPING_TURN) {
this->error_code3 = -0x70;
return false;
}
@@ -2352,7 +2347,7 @@ bool RulerServer::is_defense_valid(const ActionState& pa) {
}
if (this->hand_and_equip_states[pa.client_id] &&
(this->hand_and_equip_states[pa.client_id]->assist_flags & 0x80)) {
(this->hand_and_equip_states[pa.client_id]->assist_flags & AssistFlag::IS_SKIPPING_TURN)) {
this->error_code3 = -0x64;
return false;
}
@@ -2387,7 +2382,7 @@ bool RulerServer::is_defense_valid(const ActionState& pa) {
}
if (this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::HOLD) ||
this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::UNKNOWN_07)) {
this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::CANNOT_DEFEND)) {
this->error_code3 = -0x63;
return false;
}
@@ -2512,11 +2507,11 @@ void RulerServer::register_player(
this->set_card_action_metadatas[client_id] = set_card_action_metadatas;
}
void RulerServer::replace_D1_D2_rarity_cards_with_Attack(
void RulerServer::replace_D1_D2_rank_cards_with_Attack(
parray<le_uint16_t, 0x1F>& card_ids) const {
for (size_t z = 0; z < card_ids.size(); z++) {
auto ce = this->definition_for_card_id(card_ids[z]);
if (ce && ((ce->def.rarity == CardRarity::D1) || (ce->def.rarity == CardRarity::D2))) {
if (ce && ((ce->def.rank == CardRank::D1) || (ce->def.rank == CardRank::D2))) {
card_ids[z] = 0x008A; // Attack action card
}
}
+3 -3
View File
@@ -18,7 +18,8 @@ void compute_effective_range(
std::shared_ptr<const CardIndex> card_index,
uint16_t card_id,
const Location& loc,
std::shared_ptr<const MapAndRulesState> map_and_rules);
std::shared_ptr<const MapAndRulesState> map_and_rules,
PrefixedLogger* log = nullptr);
bool card_linkage_is_valid(
std::shared_ptr<const CardIndex::CardEntry> right_def,
@@ -196,8 +197,7 @@ public:
std::shared_ptr<DeckEntry> deck_entry,
std::shared_ptr<parray<ActionChainWithConds, 9>> set_card_action_chains,
std::shared_ptr<parray<ActionMetadata, 9>> set_card_action_metadatas);
void replace_D1_D2_rarity_cards_with_Attack(
parray<le_uint16_t, 0x1F>& card_ids) const;
void replace_D1_D2_rank_cards_with_Attack(parray<le_uint16_t, 0x1F>& card_ids) const;
AttackMedium get_attack_medium(const ActionState& pa) const;
void set_client_team_id(uint8_t client_id, uint8_t team_id);
int32_t set_cost_for_card(uint8_t client_id, uint16_t card_ref) const;
+535 -311
View File
File diff suppressed because it is too large Load Diff
+119 -106
View File
@@ -2,6 +2,7 @@
#include <stdint.h>
#include <array>
#include <memory>
#include "../Channel.hh"
@@ -12,95 +13,87 @@
#include "MapState.hh"
#include "PlayerState.hh"
#include "RulerServer.hh"
#include "Tournament.hh"
struct Lobby;
namespace Episode3 {
/**
* This implementation of Episode 3 battles (contained in all files in the
* src/Episode3 directory, except for DataIndexes.hh/cc) is derived from Sega's
* original server implementation, reverse-engineered from the Episode 3 client
* This implementation of Episode 3 battles is derived from Sega's original
* server implementation, reverse-engineered from the Episode 3 client
* executable. The control flow, function breakdown, and structure definitions
* in these files map very closely to how their server implementation was
* written; notable differences (due to necessary environment differences or bug
* fixes) are described in the comments therein.
*
* Some debugging functions have been added which are not part of the original
* implementation. Notably, this applies to functions like debug message senders
* and loggers and all str() functions.
* The following files are direct reverse-engineerings of Sega's original code,
* except where noted in the comments:
* AssistServer.hh/cc
* Card.hh/cc
* CardSpecial.hh/cc
* DeckState.hh/cc
* MapState.hh/cc
* PlayerState.hh/cc
* PlayerStateSubordinates.hh/cc
* RulerServer.hh/cc
* Server.hh/cc
*
* There are likely undiscovered bugs in this code, some originally written by
* Sega, but more written by me as I manually transcribed and updated this code.
*
* Class ownership levels (classes may only contain weak_ptrs, not shared_ptrs,
* to classes at the same or higher level):
* - Server
* - - RulerServer
* - - - AssistServer
* - - - CardSpecial
* - - - - StateFlags
* - - - - DeckEntry
* - - - - PlayerState
* - - - - - Card
* - - - - - - CardShortStatus
* - - - - - - DeckState
* - - - - - - HandAndEquipState
* - - - - - - MapAndRulesState / OverlayState
* - - - - - - - Everything within DataIndexes
*/
// Class ownership levels (classes may only contain weak_ptrs, not shared_ptrs,
// to classes at the same or higher level):
// - ServerBase
// - - Server
// - - - RulerServer
// - - - - AssistServer
// - - - - CardSpecial
// - - - - - StateFlags
// - - - - - DeckEntry
// - - - - - PlayerState
// - - - - - - Card
// - - - - - - - CardShortStatus
// - - - - - - - DeckState
// - - - - - - - HandAndEquipState
// - - - - - - - MapAndRulesState / OverlayState
// - - - - - - - - Everything within DataIndexes
class Server;
class ServerBase : public std::enable_shared_from_this<ServerBase> {
public:
ServerBase(
std::shared_ptr<Lobby> lobby,
std::shared_ptr<const CardIndex> card_index,
std::shared_ptr<const MapIndex> map_index,
uint32_t behavior_flags,
std::shared_ptr<PSOLFGEncryption> random_crypt,
std::shared_ptr<const MapIndex::MapEntry> map_if_tournament);
void init();
void reset();
void recreate_server();
struct PresenceEntry {
uint8_t player_present;
uint8_t deck_valid;
uint8_t is_cpu_player;
PresenceEntry();
void clear();
} __attribute__((packed));
std::weak_ptr<Lobby> lobby;
std::shared_ptr<const CardIndex> card_index;
std::shared_ptr<const MapIndex> map_index;
uint32_t behavior_flags;
PrefixedLogger log;
std::shared_ptr<PSOLFGEncryption> random_crypt;
bool is_tournament;
std::shared_ptr<const MapIndex::MapEntry> last_chosen_map;
std::shared_ptr<MapAndRulesState> map_and_rules1;
std::shared_ptr<MapAndRulesState> map_and_rules2;
std::shared_ptr<DeckEntry> deck_entries[4];
std::shared_ptr<Server> server;
parray<PresenceEntry, 4> presence_entries;
uint8_t num_clients_present;
parray<NameEntry, 4> name_entries;
parray<uint8_t, 4> name_entries_valid;
OverlayState overlay_state;
parray<parray<uint8_t, 0x2F0>, 4> client_card_counts;
};
class Server : public std::enable_shared_from_this<Server> {
// In the original code, there is a TCardServerBase class and a TCardServer
// class, with the former containing some basic parts of the game state and
// a pointer to the latter. It seems these two classes exist (instead of one
// big class) so that the force reset command could be implemented; however,
// it appears that that command is never sent by the client, so we combine
// the two classes into one in our implementation.
public:
explicit Server(std::shared_ptr<ServerBase> base);
struct Options {
std::shared_ptr<const CardIndex> card_index;
std::shared_ptr<const MapIndex> map_index;
uint32_t behavior_flags;
std::shared_ptr<PSOLFGEncryption> random_crypt;
std::shared_ptr<const Tournament> tournament;
std::array<std::vector<uint16_t>, 5> trap_card_ids;
};
Server(std::shared_ptr<Lobby> lobby, Options&& options);
~Server() noexcept(false);
void init();
std::shared_ptr<ServerBase> base();
std::shared_ptr<const ServerBase> base() const;
class StackLogger : public PrefixedLogger {
public:
StackLogger(const Server* s, const std::string& prefix);
StackLogger(const Server* s, const std::string& prefix, LogLevel min_level);
StackLogger(const StackLogger&) = delete;
StackLogger(StackLogger&&);
StackLogger& operator=(const StackLogger&) = delete;
StackLogger& operator=(StackLogger&&);
~StackLogger() noexcept(false);
private:
const Server* server;
};
StackLogger log_stack(const std::string& prefix) const;
const StackLogger& log() const;
int8_t get_winner_team_id() const;
@@ -119,10 +112,10 @@ public:
this->send(&cmd, cmd.header.size * 4);
}
void send(const void* data, size_t size) const;
void send_commands_for_joining_spectator(Channel& ch, uint8_t language, bool is_trial) const;
void send_commands_for_joining_spectator(Channel& ch, bool is_trial) const;
__attribute__((format(printf, 2, 3))) void log_debug(const char* fmt, ...) const;
void force_battle_result(uint8_t surrendered_client_id, bool set_winner);
void force_destroy_field_character(uint8_t client_id, size_t set_index);
__attribute__((format(printf, 2, 3))) void send_debug_message_printf(const char* fmt, ...) const;
__attribute__((format(printf, 2, 3))) void send_info_message_printf(const char* fmt, ...) const;
@@ -185,33 +178,34 @@ public:
void move_phase_before();
void set_player_deck_valid(uint8_t client_id);
void setup_and_start_battle();
G_SetStateFlags_GC_Ep3_6xB4x03 prepare_6xB4x03() const;
void update_battle_state_flags_and_send_6xB4x03_if_needed(
bool always_send = false);
bool update_registration_phase();
void on_server_data_input(const std::string& data);
void handle_6xB3x0B_mulligan_hand(const std::string& data);
void handle_6xB3x0C_end_mulligan_phase(const std::string& data);
void handle_6xB3x0D_end_non_action_phase(const std::string& data);
void handle_6xB3x0E_discard_card_from_hand(const std::string& data);
void handle_6xB3x0F_set_card_from_hand(const std::string& data);
void handle_6xB3x10_move_fc_to_location(const std::string& data);
void handle_6xB3x11_enqueue_attack_or_defense(const std::string& data);
void handle_6xB3x12_end_attack_list(const std::string& data);
void handle_6xB3x13_update_map_during_setup(const std::string& data);
void handle_6xB3x14_update_deck_during_setup(const std::string& data);
void handle_6xB3x15_unused_hard_reset_server_state(const std::string& data);
void handle_6xB3x1B_update_player_name(const std::string& data);
void handle_6xB3x1D_start_battle(const std::string& data);
void handle_6xB3x21_end_battle(const std::string& data);
void handle_6xB3x28_end_defense_list(const std::string& data);
void handle_6xB3x2B_ignored(const std::string&);
void handle_6xB3x34_subtract_ally_atk_points(const std::string& data);
void handle_6xB3x37_client_ready_to_advance_from_starter_roll_phase(const std::string& data);
void handle_6xB3x3A_ignored(const std::string& data);
void handle_6xB3x40_map_list_request(const std::string& data);
void handle_6xB3x41_map_request(const std::string& data);
void handle_6xB3x48_end_turn(const std::string& data);
void handle_6xB3x49_card_counts(const std::string& data);
void on_server_data_input(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx0B_mulligan_hand(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx0C_end_mulligan_phase(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx0D_end_non_action_phase(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx0E_discard_card_from_hand(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx0F_set_card_from_hand(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx10_move_fc_to_location(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx11_enqueue_attack_or_defense(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx12_end_attack_list(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx13_update_map_during_setup(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx14_update_deck_during_setup(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx15_unused_hard_reset_server_state(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx1B_update_player_name(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx1D_start_battle(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx21_end_battle(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx28_end_defense_list(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx2B_legacy_set_card(std::shared_ptr<Client> sender_c, const std::string&);
void handle_CAx34_subtract_ally_atk_points(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx37_client_ready_to_advance_from_starter_roll_phase(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx3A_time_limit_expired(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx40_map_list_request(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx41_map_request(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx48_end_turn(std::shared_ptr<Client> sender_c, const std::string& data);
void handle_CAx49_card_counts(std::shared_ptr<Client> sender_c, const std::string& data);
void compute_losing_team_id_and_add_winner_flags(uint32_t flags);
uint32_t get_team_exp(uint8_t team_id) const;
uint32_t send_6xB4x06_if_card_ref_invalid(
@@ -233,22 +227,44 @@ public:
G_UpdateDecks_GC_Ep3_6xB4x07 prepare_6xB4x07_decks_update() const;
G_SetPlayerNames_GC_Ep3_6xB4x1C prepare_6xB4x1C_names_update() const;
static std::string prepare_6xB6x41_map_definition(
std::shared_ptr<const MapIndex::MapEntry> map, bool is_trial);
std::shared_ptr<const MapIndex::Map> map, uint8_t language, bool is_trial);
void send_6xB6x41_to_all_clients() const;
G_SetTrapTileLocations_GC_Ep3_6xB4x50 prepare_6xB4x50_trap_tile_locations() const;
std::vector<std::shared_ptr<Card>> const_cast_set_cards_v(
const std::vector<std::shared_ptr<const Card>>& cards);
private:
typedef void (Server::*handler_t)(const std::string&);
typedef void (Server::*handler_t)(std::shared_ptr<Client>, const std::string&);
static const std::unordered_map<uint8_t, handler_t> subcommand_handlers;
std::weak_ptr<ServerBase> w_base;
public:
bool tournament_match_result_sent; // Not part of original implementation
uint8_t override_environment_number; // Not part of original implementation
// These fields are not part of the original implementation
std::weak_ptr<Lobby> lobby;
Options options;
std::shared_ptr<const MapIndex::Map> last_chosen_map;
bool tournament_match_result_sent;
uint8_t override_environment_number;
mutable std::deque<StackLogger*> logger_stack;
// These fields were originally contained in the TCardServerBase object
struct PresenceEntry {
uint8_t player_present;
uint8_t deck_valid;
uint8_t is_cpu_player;
PresenceEntry();
void clear();
} __attribute__((packed));
std::shared_ptr<MapAndRulesState> map_and_rules;
std::shared_ptr<DeckEntry> deck_entries[4];
parray<PresenceEntry, 4> presence_entries;
uint8_t num_clients_present;
parray<NameEntry, 4> name_entries;
parray<uint8_t, 4> name_entries_valid;
OverlayState overlay_state;
parray<parray<uint8_t, 0x2F0>, 4> client_card_counts;
// These fields were originally contained in the TCardServer object
uint32_t battle_finished;
uint32_t battle_in_progress;
uint32_t round_num;
@@ -263,7 +279,6 @@ public:
uint32_t num_pending_attacks;
parray<uint8_t, 4> client_done_enqueuing_attacks;
parray<uint8_t, 4> player_ready_to_end_phase;
std::shared_ptr<PSOLFGEncryption> random_crypt;
uint32_t unknown_a10;
uint32_t overall_time_expired;
// Note: In the original implementation, this is a uint32_t and is measured in
@@ -290,8 +305,6 @@ public:
parray<uint32_t, 2> team_client_count;
parray<uint32_t, 2> team_num_ally_fcs_destroyed;
parray<uint32_t, 2> team_num_cards_destroyed;
uint32_t hard_reset_flag;
uint8_t tournament_flag;
parray<uint8_t, 5> num_trap_tiles_of_type;
parray<uint8_t, 5> chosen_trap_tile_index_of_type;
parray<parray<parray<uint8_t, 2>, 8>, 5> trap_tile_locs;
+374 -179
View File
@@ -9,9 +9,14 @@ using namespace std;
namespace Episode3 {
Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number)
Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number, const string& player_name)
: serial_number(serial_number),
com_deck() {}
player_name(player_name) {}
Tournament::PlayerEntry::PlayerEntry(shared_ptr<Client> c)
: serial_number(c->license->serial_number),
client(c),
player_name(encode_sjis(c->game_data.player()->disp.name)) {}
Tournament::PlayerEntry::PlayerEntry(
shared_ptr<const COMDeckDefinition> com_deck)
@@ -50,14 +55,18 @@ string Tournament::Team::str() const {
this->password.c_str(), this->num_rounds_cleared);
for (const auto& player : this->players) {
if (player.is_human()) {
ret += string_printf(" %08" PRIX32, player.serial_number);
if (player.player_name.empty()) {
ret += string_printf(" %08" PRIX32, player.serial_number);
} else {
ret += string_printf(" %08" PRIX32 " (%s)", player.serial_number, player.player_name.c_str());
}
}
}
return ret + "]";
}
void Tournament::Team::register_player(
uint32_t serial_number,
shared_ptr<Client> c,
const string& team_name,
const string& password) {
if (this->players.size() >= this->max_players) {
@@ -72,17 +81,17 @@ void Tournament::Team::register_player(
if (!tournament) {
throw runtime_error("tournament has been deleted");
}
if (!tournament->all_player_serial_numbers.emplace(serial_number).second) {
if (!tournament->all_player_serial_numbers.emplace(c->license->serial_number).second) {
throw runtime_error("player already registered in same tournament");
}
for (const auto& player : this->players) {
if (player.is_human() && (player.serial_number == serial_number)) {
if (player.is_human() && (player.serial_number == c->license->serial_number)) {
throw logic_error("player already registered in team but not in tournament");
}
}
this->players.emplace_back(serial_number);
this->players.emplace_back(c);
if (this->name.empty()) {
this->name = team_name;
@@ -200,24 +209,32 @@ string Tournament::Match::str() const {
return string_printf("[Match round=%zu winner=%s]", this->round_num, winner_str.c_str());
}
bool Tournament::Match::resolve_if_no_human_players() {
bool Tournament::Match::resolve_if_skippable() {
if (this->winner_team) {
return true;
}
// If both matches before this one are resolved and neither winner team has
// any humans on it, skip this match entirely and just make one team advance
// arbitrarily
if (this->preceding_a->winner_team &&
this->preceding_b->winner_team &&
!this->preceding_a->winner_team->has_any_human_players() &&
!this->preceding_b->winner_team->has_any_human_players()) {
this->set_winner_team((random_object<uint8_t>() & 1)
? this->preceding_b->winner_team
: this->preceding_a->winner_team);
return true;
} else {
auto winner_a = this->preceding_a->winner_team;
auto winner_b = this->preceding_b->winner_team;
// If at least one match before this is not resolved, don't resolve this one
if (!winner_a || !winner_b) {
return false;
}
// If one of the preceding winner teams is empty, make the other the winner
if (winner_a->players.empty() != winner_b->players.empty()) {
this->set_winner_team(winner_a->players.empty() ? winner_b : winner_a);
return true;
}
// If neither preceding winner team has any humans on it, skip this match
// entirely and just make one team advance arbitrarily (note that this also
// handles the case where both preceding winner teams are empty)
if (!winner_a->has_any_human_players() && !winner_b->has_any_human_players()) {
this->set_winner_team((random_object<uint8_t>() & 1) ? winner_b : winner_a);
return true;
}
return false;
}
void Tournament::Match::on_winner_team_set() {
@@ -231,7 +248,7 @@ void Tournament::Match::on_winner_team_set() {
// Resolve the following match if possible (this skips CPU-only matches). If
// the following match can't be resolved, mark it pending.
auto following = this->following.lock();
if (following && !following->resolve_if_no_human_players()) {
if (following && !following->resolve_if_skippable()) {
tournament->pending_matches.emplace(following);
}
@@ -239,6 +256,20 @@ void Tournament::Match::on_winner_team_set() {
if (tournament->pending_matches.empty()) {
tournament->current_state = Tournament::State::COMPLETE;
}
// Unlink the losing team's players (if any) - this allows them to enter
// another tournament before this tournament has ended
if (this->preceding_a && this->preceding_b) {
auto losing_team = (this->winner_team == this->preceding_a->winner_team)
? this->preceding_b->winner_team
: this->preceding_a->winner_team;
for (auto& player : losing_team->players) {
auto c = player.client.lock();
if (c) {
c->ep3_tournament_team.reset();
}
}
}
}
void Tournament::Match::set_winner_team_without_triggers(shared_ptr<Team> team) {
@@ -282,22 +313,21 @@ shared_ptr<Tournament::Team> Tournament::Match::opponent_team_for_team(
Tournament::Tournament(
shared_ptr<const MapIndex> map_index,
shared_ptr<const COMDeckIndex> com_deck_index,
uint8_t number,
const string& name,
shared_ptr<const MapIndex::MapEntry> map,
shared_ptr<const MapIndex::Map> map,
const Rules& rules,
size_t num_teams,
bool is_2v2)
: log(string_printf("[Tournament/%02hhX] ", number)),
uint8_t flags)
: log(string_printf("[Tournament/%s] ", name.c_str())),
map_index(map_index),
com_deck_index(com_deck_index),
number(number),
name(name),
map(map),
rules(rules),
num_teams(num_teams),
is_2v2(is_2v2),
current_state(State::REGISTRATION) {
flags(flags),
current_state(State::REGISTRATION),
menu_item_id(0xFFFFFFFF) {
if (this->num_teams < 4) {
throw invalid_argument("team count must be 4 or more");
}
@@ -312,13 +342,11 @@ Tournament::Tournament(
Tournament::Tournament(
shared_ptr<const MapIndex> map_index,
shared_ptr<const COMDeckIndex> com_deck_index,
uint8_t number,
const JSON& json)
: log(string_printf("[Tournament/%02hhX] ", number)),
: log(string_printf("[Tournament/%s] ", json.get_string("name").c_str())),
map_index(map_index),
com_deck_index(com_deck_index),
source_json(json),
number(number),
current_state(State::REGISTRATION) {}
void Tournament::init() {
@@ -327,9 +355,12 @@ void Tournament::init() {
bool is_registration_complete;
if (!this->source_json.is_null()) {
this->name = this->source_json.get_string("name");
this->map = this->map_index->definition_for_number(this->source_json.get_int("map_number"));
this->map = this->map_index->for_number(this->source_json.get_int("map_number"));
this->rules = Rules(this->source_json.at("rules"));
this->is_2v2 = this->source_json.get_bool("is_2v2");
this->flags = this->source_json.get_int("flags", 0x02);
if (this->source_json.get_bool("is_2v2", false)) {
this->flags |= Flag::IS_2V2;
}
is_registration_complete = this->source_json.get_bool("is_registration_complete");
for (const auto& team_json : this->source_json.get_list("teams")) {
@@ -341,11 +372,18 @@ void Tournament::init() {
team->password = team_json->get_string("password");
team_index_to_rounds_cleared.emplace_back(team_json->get_int("num_rounds_cleared"));
for (const auto& player_json : team_json->get_list("player_specs")) {
if (player_json->is_int()) {
team->players.emplace_back(player_json->as_int());
this->all_player_serial_numbers.emplace(player_json->as_int());
} else {
if (player_json->is_list()) {
uint32_t serial_number = player_json->at(0).as_int();
team->players.emplace_back(serial_number, player_json->at(1).as_string());
this->all_player_serial_numbers.emplace(serial_number);
} else if (player_json->is_int()) {
uint32_t serial_number = player_json->as_int();
team->players.emplace_back(serial_number);
this->all_player_serial_numbers.emplace(serial_number);
} else if (player_json->is_string()) {
team->players.emplace_back(this->com_deck_index->deck_for_name(player_json->as_string()));
} else {
throw runtime_error("invalid player spec");
}
}
}
@@ -357,40 +395,18 @@ void Tournament::init() {
// Create empty teams
while (this->teams.size() < this->num_teams) {
auto t = make_shared<Team>(
this->shared_from_this(), this->teams.size(), this->is_2v2 ? 2 : 1);
this->shared_from_this(), this->teams.size(), (this->flags & Flag::IS_2V2) ? 2 : 1);
this->teams.emplace_back(t);
}
is_registration_complete = false;
}
// Create the match structure
while (this->zero_round_matches.size() < this->num_teams) {
this->zero_round_matches.emplace_back(make_shared<Match>(
this->shared_from_this(), this->teams[this->zero_round_matches.size()]));
}
// Create the bracket matches
vector<shared_ptr<Match>> current_round_matches = this->zero_round_matches;
while (current_round_matches.size() > 1) {
vector<shared_ptr<Match>> next_round_matches;
for (size_t z = 0; z < current_round_matches.size(); z += 2) {
auto m = make_shared<Match>(
this->shared_from_this(),
current_round_matches[z],
current_round_matches[z + 1]);
current_round_matches[z]->following = m;
current_round_matches[z + 1]->following = m;
next_round_matches.emplace_back(std::move(m));
}
current_round_matches = std::move(next_round_matches);
}
this->final_match = current_round_matches.at(0);
// Compute the match state from the teams' states
if (is_registration_complete) {
this->current_state = State::IN_PROGRESS;
this->create_bracket_matches();
// Start with all first-round matches in the match queue
// Start with all zero-round matches in the match queue
unordered_set<shared_ptr<Match>> match_queue;
for (auto match : this->zero_round_matches) {
match_queue.emplace(match->following.lock());
@@ -446,23 +462,61 @@ void Tournament::init() {
}
} else {
// Make all the zero round matches pending (this is needed so that start()
// will auto-resolve all-CPU matches in the first round)
for (auto m : this->zero_round_matches) {
this->pending_matches.emplace(m);
}
this->current_state = State::REGISTRATION;
}
}
void Tournament::create_bracket_matches() {
if (this->teams.size() < 4) {
throw logic_error("tournaments must have at least 4 teams");
}
if (this->teams.size() > 32) {
throw logic_error("tournaments must have at most 32 teams");
}
if (this->teams.size() & (this->teams.size() - 1)) {
throw logic_error("tournaments team count is not a power of 2");
}
// Create the zero-round matches, and make them all pending if registration
// is still open
this->zero_round_matches.clear();
for (const auto& team : this->teams) {
auto m = make_shared<Match>(this->shared_from_this(), team);
this->zero_round_matches.emplace_back(m);
if (this->current_state == State::REGISTRATION) {
this->pending_matches.emplace(m);
}
}
// Create the bracket matches
vector<shared_ptr<Match>> current_round_matches = this->zero_round_matches;
while (current_round_matches.size() > 1) {
vector<shared_ptr<Match>> next_round_matches;
for (size_t z = 0; z < current_round_matches.size(); z += 2) {
auto m = make_shared<Match>(
this->shared_from_this(),
current_round_matches[z],
current_round_matches[z + 1]);
current_round_matches[z]->following = m;
current_round_matches[z + 1]->following = m;
next_round_matches.emplace_back(std::move(m));
}
current_round_matches = std::move(next_round_matches);
}
this->final_match = current_round_matches.at(0);
}
JSON Tournament::json() const {
auto teams_list = JSON::list();
for (auto team : this->teams) {
auto players_list = JSON::list();
for (const auto& player : team->players) {
if (player.is_human()) {
players_list.emplace_back(player.serial_number);
if (!player.player_name.empty()) {
players_list.emplace_back(JSON::list({player.serial_number, player.player_name}));
} else {
players_list.emplace_back(player.serial_number);
}
} else {
players_list.emplace_back(player.com_deck->deck_name);
}
@@ -477,50 +531,21 @@ JSON Tournament::json() const {
}
return JSON::dict({
{"name", this->name},
{"map_number", this->map->map.map_number.load()},
{"map_number", this->map->map_number},
{"rules", this->rules.json()},
{"is_2v2", this->is_2v2},
{"flags", this->flags},
{"is_registration_complete", (this->current_state != State::REGISTRATION)},
{"teams", std::move(teams_list)},
});
}
uint8_t Tournament::get_number() const {
return this->number;
}
const string& Tournament::get_name() const {
return this->name;
}
shared_ptr<const MapIndex::MapEntry> Tournament::get_map() const {
return this->map;
}
const Rules& Tournament::get_rules() const {
return this->rules;
}
bool Tournament::get_is_2v2() const {
return this->is_2v2;
}
Tournament::State Tournament::get_state() const {
return this->current_state;
}
const vector<shared_ptr<Tournament::Team>>& Tournament::all_teams() const {
return this->teams;
}
shared_ptr<Tournament::Team> Tournament::get_team(size_t index) const {
return this->teams.at(index);
}
shared_ptr<Tournament::Team> Tournament::get_winner_team() const {
if (this->current_state != State::COMPLETE) {
return nullptr;
}
if (!this->final_match) {
throw logic_error("tournament is complete but final match is missing");
}
if (!this->final_match->winner_team) {
throw logic_error("tournament is complete but winner is not set");
}
@@ -574,14 +599,75 @@ void Tournament::start() {
throw runtime_error("tournament has already started");
}
this->current_state = State::IN_PROGRESS;
bool has_com_teams = (this->flags & Flag::HAS_COM_TEAMS);
// Assign names to COM teams, and assign COM decks to all empty slots
// If there aren't enough entrants (1 if has_com_teams is false, else 2),
// don't allow the tournament to start (because it would enter the COMPLETE
// state immediately)
size_t num_human_teams = 0;
for (size_t z = 0; z < this->teams.size(); z++) {
if (this->teams[z]->has_any_human_players()) {
num_human_teams++;
}
}
if (num_human_teams < (has_com_teams ? 1 : 2)) {
throw runtime_error("not enough registrants to start tournament");
}
if ((this->flags & Flag::SHUFFLE_ENTRIES) && (this->flags & Flag::RESIZE_ON_START)) {
// If both of these flags are set, pack the human teams into the lowest part
// of the teams list so we can resize the tournament to the smallest
// possible size. This is OK since we're going to shuffle them later anyway
size_t r_offset = 0, w_offset = 0;
for (; r_offset < this->teams.size(); r_offset++) {
if (this->teams[r_offset]->has_any_human_players()) {
if (r_offset != w_offset) {
this->teams[r_offset].swap(this->teams[w_offset]);
}
w_offset++;
}
}
}
if (this->flags & Flag::RESIZE_ON_START) {
// Resize the tournament by repeatedly deleting the second half of it, until
// the second half contains human players or the tournament size is 4
while (this->teams.size() > 4) {
size_t z;
for (z = this->teams.size() >> 1; z < this->teams.size(); z++) {
if (this->teams[z]->has_any_human_players()) {
break;
}
}
if (z == this->teams.size()) {
this->teams.resize(this->teams.size() >> 1);
} else {
break;
}
}
this->num_teams = this->teams.size();
}
if (this->flags & Flag::SHUFFLE_ENTRIES) {
// Shuffle all the tournament entries
for (size_t z = this->teams.size(); z > 0; z--) {
size_t index = random_object<uint32_t>() % z;
if (index != z - 1) {
this->teams[z - 1].swap(this->teams[index]);
}
}
}
this->current_state = State::IN_PROGRESS;
this->create_bracket_matches();
// Assign names to COM teams, and assign COM decks to all empty slots unless
// has_com_teams is false
for (size_t z = 0; z < this->zero_round_matches.size(); z++) {
auto m = this->zero_round_matches[z];
auto t = m->winner_team;
if (t->name.empty()) {
t->name = string_printf("COM:%zu", z);
t->name = has_com_teams ? string_printf("COM:%zu", z) : "(no entrant)";
}
for (const auto& player : t->players) {
if (player.is_com()) {
@@ -591,19 +677,54 @@ void Tournament::start() {
if (this->com_deck_index->num_decks() < t->max_players - t->players.size()) {
throw runtime_error("not enough COM decks to complete team");
}
// TODO: Don't allow duplicate COM decks, nor duplicate COM SCs on the same
// team
while (t->players.size() < t->max_players) {
t->players.emplace_back(this->com_deck_index->random_deck());
// If we allow all-COM teams, or this is a 2v2 tournament and the team has
// only one human on it, add a COM
if (has_com_teams || !t->players.empty()) {
// TODO: Don't allow duplicate COM decks, nor duplicate COM SCs on the
// same team
while (t->players.size() < t->max_players) {
t->players.emplace_back(this->com_deck_index->random_deck());
}
}
}
// Resolve all possible CPU-only matches
// Resolve all possible skippable matches
for (auto m : this->zero_round_matches) {
m->on_winner_team_set();
}
}
void Tournament::send_all_state_updates() const {
for (const auto& team : this->teams) {
for (const auto& player : team->players) {
auto c = player.client.lock();
// Note: The last check here is to make sure the client is still linked
// with this instance of the tournament - an intervening shell command
// `reload ep3` could have changed the client's linkage
if (c &&
(c->flags & Client::Flag::IS_EPISODE_3) &&
!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION) &&
(c->ep3_tournament_team.lock() == team)) {
send_ep3_confirm_tournament_entry(c, this->shared_from_this());
}
}
}
}
void Tournament::send_all_state_updates_on_deletion() const {
for (const auto& team : this->teams) {
for (const auto& player : team->players) {
auto c = player.client.lock();
if (c &&
(c->flags & Client::Flag::IS_EPISODE_3) &&
!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION) &&
(c->ep3_tournament_team.lock() == team)) {
send_ep3_confirm_tournament_entry(c, nullptr);
}
}
}
}
void Tournament::print_bracket(FILE* stream) const {
function<void(shared_ptr<Match>, size_t)> print_match = [&](shared_ptr<Match> m, size_t indent_level) -> void {
for (size_t z = 0; z < indent_level; z++) {
@@ -619,12 +740,20 @@ void Tournament::print_bracket(FILE* stream) const {
print_match(m->preceding_b, indent_level + 1);
}
};
fprintf(stream, "Tournament %02hhX: %s\n", this->number, this->name.c_str());
string map_name = this->map->map.name;
fprintf(stream, " Map: %08" PRIX32 " (%s)\n", this->map->map.map_number.load(), map_name.c_str());
fprintf(stream, "Tournament \"%s\"\n", this->name.c_str());
auto en_vm = this->map->version(1);
if (en_vm) {
string map_name = en_vm->map->name;
fprintf(stream, " Map: %08" PRIX32 " (%s)\n", this->map->map_number, map_name.c_str());
} else {
fprintf(stream, " Map: %08" PRIX32 "\n", this->map->map_number);
}
string rules_str = this->rules.str();
fprintf(stream, " Rules: %s\n", rules_str.c_str());
fprintf(stream, " Structure: %s, %zu entries\n", this->is_2v2 ? "2v2" : "1v1", this->num_teams);
fprintf(stream, " Structure: %s, %zu entries\n", (this->flags & Flag::IS_2V2) ? "2v2" : "1v1", this->num_teams);
fprintf(stream, " COM teams: %s\n", (this->flags & Flag::HAS_COM_TEAMS) ? "allowed" : "forbidden");
fprintf(stream, " Shuffle entries: %s\n", (this->flags & Flag::SHUFFLE_ENTRIES) ? "yes" : "no");
fprintf(stream, " Resize on start: %s\n", (this->flags & Flag::RESIZE_ON_START) ? "yes" : "no");
switch (this->current_state) {
case State::REGISTRATION:
fprintf(stream, " State: REGISTRATION\n");
@@ -639,12 +768,22 @@ void Tournament::print_bracket(FILE* stream) const {
fprintf(stream, " State: UNKNOWN\n");
break;
}
fprintf(stream, " Standings:\n");
print_match(this->final_match, 2);
fprintf(stream, " Pending matches:\n");
for (const auto& match : this->pending_matches) {
string match_str = match->str();
fprintf(stream, " %s\n", match_str.c_str());
if (this->final_match) {
fprintf(stream, " Standings:\n");
print_match(this->final_match, 2);
}
if (this->current_state == State::REGISTRATION) {
fprintf(stream, " Teams:\n");
for (const auto& team : this->teams) {
string team_str = team->str();
fprintf(stream, " %s\n", team_str.c_str());
}
} else {
fprintf(stream, " Pending matches:\n");
for (const auto& match : this->pending_matches) {
string match_str = match->str();
fprintf(stream, " %s\n", match_str.c_str());
}
}
}
@@ -660,15 +799,45 @@ TournamentIndex::TournamentIndex(
return;
}
auto json = JSON::parse(load_file(this->state_filename));
if (json.size() != 0x20) {
throw runtime_error("tournament JSON list length is incorrect");
JSON json;
try {
json = JSON::parse(load_file(this->state_filename));
} catch (const cannot_open_file&) {
json = JSON::list();
}
for (size_t z = 0; z < 0x20; z++) {
if (!json.at(z).is_null()) {
this->tournaments[z].reset(new Tournament(this->map_index, this->com_deck_index, z, json.at(z)));
this->tournaments[z]->init();
if (json.is_list()) {
if (json.size() > 0x20) {
throw runtime_error("tournament JSON list length is incorrect");
}
for (size_t z = 0; z < min<size_t>(json.size(), 0x20); z++) {
if (!json.at(z).is_null()) {
shared_ptr<Tournament> tourn(new Tournament(this->map_index, this->com_deck_index, json.at(z)));
tourn->init();
if (!this->name_to_tournament.emplace(tourn->get_name(), tourn).second) {
throw runtime_error("multiple tournaments have the same name: " + tourn->get_name());
}
tourn->set_menu_item_id(this->menu_item_id_to_tournament.size());
this->menu_item_id_to_tournament.emplace_back(tourn);
}
}
} else if (json.is_dict()) {
if (json.size() > 0x20) {
throw runtime_error("tournament JSON dict length is incorrect");
}
for (const auto& it : json.as_dict()) {
shared_ptr<Tournament> tourn(new Tournament(this->map_index, this->com_deck_index, *it.second));
tourn->init();
if (!this->name_to_tournament.emplace(tourn->get_name(), tourn).second) {
// This is logic_error instead of runtime_error because JSON dicts are
// supposed to already have unique keys
throw logic_error("multiple tournaments have the same name: " + tourn->get_name());
}
tourn->set_menu_item_id(this->menu_item_id_to_tournament.size());
this->menu_item_id_to_tournament.emplace_back(tourn);
}
} else {
throw runtime_error("tournament state root JSON is not a list or dict");
}
}
@@ -677,75 +846,68 @@ void TournamentIndex::save() const {
return;
}
auto list = JSON::list();
for (size_t z = 0; z < 0x20; z++) {
if (this->tournaments[z]) {
list.emplace_back(this->tournaments[z]->json());
} else {
list.emplace_back(nullptr);
}
auto json = JSON::dict();
for (const auto& it : this->name_to_tournament) {
json.emplace(it.second->get_name(), it.second->json());
}
save_file(this->state_filename, list.serialize(JSON::SerializeOption::FORMAT));
}
vector<shared_ptr<Tournament>> TournamentIndex::all_tournaments() const {
vector<shared_ptr<Tournament>> ret;
for (size_t z = 0; z < 0x20; z++) {
if (this->tournaments[z]) {
ret.emplace_back(this->tournaments[z]);
}
}
return ret;
save_file(this->state_filename, json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS));
}
shared_ptr<Tournament> TournamentIndex::create_tournament(
const string& name,
shared_ptr<const MapIndex::MapEntry> map,
shared_ptr<const MapIndex::Map> map,
const Rules& rules,
size_t num_teams,
bool is_2v2) {
// Find an unused tournament number
uint8_t number;
for (number = 0; number < 0x20; number++) {
if (!this->tournaments[number]) {
break;
}
}
if (number >= 0x20) {
throw runtime_error("all tournament slots are full");
uint8_t flags) {
if (this->name_to_tournament.size() >= 0x20) {
throw runtime_error("there can be at most 32 tournaments at a time");
}
auto t = make_shared<Tournament>(
this->map_index, this->com_deck_index, number, name, map, rules, num_teams, is_2v2);
this->map_index, this->com_deck_index, name, map, rules, num_teams, flags);
t->init();
this->tournaments[number] = t;
if (!this->name_to_tournament.emplace(t->get_name(), t).second) {
throw runtime_error("a tournament with the same name already exists");
}
size_t z;
for (z = 0; z < this->menu_item_id_to_tournament.size(); z++) {
if (!this->menu_item_id_to_tournament[z]) {
t->set_menu_item_id(z);
this->menu_item_id_to_tournament[z] = t;
break;
}
}
if (z == this->menu_item_id_to_tournament.size()) {
t->set_menu_item_id(this->menu_item_id_to_tournament.size());
this->menu_item_id_to_tournament.emplace_back(t);
}
this->save();
return t;
}
void TournamentIndex::delete_tournament(uint8_t number) {
this->tournaments[number].reset();
}
shared_ptr<Tournament> TournamentIndex::get_tournament(uint8_t number) const {
return this->tournaments[number];
}
shared_ptr<Tournament> TournamentIndex::get_tournament(const string& name) const {
for (size_t z = 0; z < 0x20; z++) {
if (this->tournaments[z] && (this->tournaments[z]->get_name() == name)) {
return this->tournaments[z];
bool TournamentIndex::delete_tournament(const string& name) {
auto it = this->name_to_tournament.find(name);
if (it == this->name_to_tournament.end()) {
return false;
}
for (size_t z = 0; z < this->menu_item_id_to_tournament.size(); z++) {
if (this->menu_item_id_to_tournament[z] == it->second) {
this->menu_item_id_to_tournament[z] = nullptr;
it->second->set_menu_item_id(0xFFFFFFFF);
}
}
return nullptr;
it->second->send_all_state_updates_on_deletion();
this->name_to_tournament.erase(it);
this->save();
return true;
}
shared_ptr<Tournament::Team> TournamentIndex::team_for_serial_number(
uint32_t serial_number) const {
for (size_t z = 0; z < 0x20; z++) {
if (!this->tournaments[z]) {
continue;
}
auto team = this->tournaments[z]->team_for_serial_number(serial_number);
shared_ptr<Tournament::Team> TournamentIndex::team_for_serial_number(uint32_t serial_number) const {
for (const auto& it : this->name_to_tournament) {
const auto& tourn = it.second;
auto team = tourn->team_for_serial_number(serial_number);
if (team) {
return team;
}
@@ -753,4 +915,37 @@ shared_ptr<Tournament::Team> TournamentIndex::team_for_serial_number(
return nullptr;
}
void TournamentIndex::link_client(shared_ptr<Client> c) {
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
return;
}
auto team = this->team_for_serial_number(c->license->serial_number);
auto tourn = team ? team->tournament.lock() : nullptr;
if (team && team->is_active && tourn) {
for (auto& player : team->players) {
if (player.serial_number == c->license->serial_number) {
c->ep3_tournament_team = team;
player.client = c;
if (!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION)) {
send_ep3_confirm_tournament_entry(c, tourn);
}
return;
}
}
throw logic_error("tournament team found for player, but player not found on team");
} else {
c->ep3_tournament_team.reset();
if (!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION)) {
send_ep3_confirm_tournament_entry(c, nullptr);
}
}
}
void TournamentIndex::link_all_clients(std::shared_ptr<ServerState> s) {
for (const auto& c_it : s->channel_to_client) {
this->link_client(c_it.second);
}
}
} // namespace Episode3
+81 -29
View File
@@ -13,13 +13,19 @@
#include "../Player.hh"
struct Lobby;
struct Client;
struct ServerState;
namespace Episode3 {
// The comment in Server.hh does not apply to this file (and Tournament.cc).
class Tournament : public std::enable_shared_from_this<Tournament> {
public:
enum Flag : uint8_t {
IS_2V2 = 0x01,
HAS_COM_TEAMS = 0x02,
SHUFFLE_ENTRIES = 0x04,
RESIZE_ON_START = 0x08,
};
enum class State {
REGISTRATION = 0,
IN_PROGRESS,
@@ -32,11 +38,18 @@ public:
uint32_t serial_number;
std::shared_ptr<const COMDeckDefinition> com_deck;
explicit PlayerEntry(uint32_t serial_number);
// client is valid if serial_number is nonzero and the client is connected
std::weak_ptr<Client> client;
std::string player_name; // Not used for COM decks
explicit PlayerEntry(uint32_t serial_number, const std::string& player_name = "");
explicit PlayerEntry(std::shared_ptr<Client> c);
explicit PlayerEntry(std::shared_ptr<const COMDeckDefinition> com_deck);
bool is_com() const;
bool is_human() const;
JSON json() const;
};
struct Team : public std::enable_shared_from_this<Team> {
@@ -57,7 +70,7 @@ public:
std::string str() const;
void register_player(
uint32_t serial_number,
std::shared_ptr<Client> c,
const std::string& team_name,
const std::string& password);
bool unregister_player(uint32_t serial_number);
@@ -84,7 +97,7 @@ public:
std::shared_ptr<Team> winner_team);
std::string str() const;
bool resolve_if_no_human_players();
bool resolve_if_skippable();
void on_winner_team_set();
void set_winner_team(std::shared_ptr<Team> team);
void set_winner_team_without_triggers(std::shared_ptr<Team> team);
@@ -94,31 +107,48 @@ public:
Tournament(
std::shared_ptr<const MapIndex> map_index,
std::shared_ptr<const COMDeckIndex> com_deck_index,
uint8_t number,
const std::string& name,
std::shared_ptr<const MapIndex::MapEntry> map,
std::shared_ptr<const MapIndex::Map> map,
const Rules& rules,
size_t num_teams,
bool is_2v2);
uint8_t flags);
Tournament(
std::shared_ptr<const MapIndex> map_index,
std::shared_ptr<const COMDeckIndex> com_deck_index,
uint8_t number,
const JSON& json);
~Tournament() = default;
void init();
JSON json() const;
uint8_t get_number() const;
const std::string& get_name() const;
std::shared_ptr<const MapIndex::MapEntry> get_map() const;
const Rules& get_rules() const;
bool get_is_2v2() const;
State get_state() const;
inline const std::string& get_name() const {
return this->name;
}
inline std::shared_ptr<const MapIndex::Map> get_map() const {
return this->map;
}
inline const Rules& get_rules() const {
return this->rules;
}
inline uint8_t get_flags() const {
return this->flags;
}
inline State get_state() const {
return this->current_state;
}
inline const std::vector<std::shared_ptr<Team>>& all_teams() const {
return this->teams;
}
inline std::shared_ptr<Team> get_team(size_t index) const {
return this->teams.at(index);
}
inline uint32_t get_menu_item_id() const {
return this->menu_item_id;
}
inline void set_menu_item_id(uint32_t menu_item_id) {
this->menu_item_id = menu_item_id;
}
const std::vector<std::shared_ptr<Team>>& all_teams() const;
std::shared_ptr<Team> get_team(size_t index) const;
std::shared_ptr<Team> get_winner_team() const;
std::shared_ptr<Match> next_match_for_team(std::shared_ptr<Team> team) const;
std::shared_ptr<Match> get_final_match() const;
@@ -127,21 +157,26 @@ public:
void start();
void send_all_state_updates() const;
void send_all_state_updates_on_deletion() const;
void print_bracket(FILE* stream) const;
private:
void create_bracket_matches();
PrefixedLogger log;
std::shared_ptr<const MapIndex> map_index;
std::shared_ptr<const COMDeckIndex> com_deck_index;
JSON source_json;
uint8_t number;
std::string name;
std::shared_ptr<const MapIndex::MapEntry> map;
std::shared_ptr<const MapIndex::Map> map;
Rules rules;
size_t num_teams;
bool is_2v2;
uint8_t flags;
State current_state;
uint32_t menu_item_id;
std::set<uint32_t> all_player_serial_numbers;
std::unordered_set<std::shared_ptr<Match>> pending_matches;
@@ -170,26 +205,43 @@ public:
void save() const;
std::vector<std::shared_ptr<Tournament>> all_tournaments() const;
inline const std::unordered_map<std::string, std::shared_ptr<Tournament>>& all_tournaments() const {
return this->name_to_tournament;
}
inline std::shared_ptr<Tournament> get_tournament(uint32_t menu_item_id) const {
try {
return this->menu_item_id_to_tournament.at(menu_item_id);
} catch (const std::out_of_range&) {
return nullptr;
}
}
inline std::shared_ptr<Tournament> get_tournament(const std::string& name) const {
try {
return this->name_to_tournament.at(name);
} catch (const std::out_of_range&) {
return nullptr;
}
}
std::shared_ptr<Tournament> create_tournament(
const std::string& name,
std::shared_ptr<const MapIndex::MapEntry> map,
std::shared_ptr<const MapIndex::Map> map,
const Rules& rules,
size_t num_teams,
bool is_2v2);
void delete_tournament(uint8_t number);
std::shared_ptr<Tournament> get_tournament(uint8_t number) const;
std::shared_ptr<Tournament> get_tournament(const std::string& name) const;
uint8_t flags);
bool delete_tournament(const std::string& name);
std::shared_ptr<Tournament::Team> team_for_serial_number(
uint32_t serial_number) const;
std::shared_ptr<Tournament::Team> team_for_serial_number(uint32_t serial_number) const;
void link_client(std::shared_ptr<Client> c);
void link_all_clients(std::shared_ptr<ServerState> s);
private:
std::shared_ptr<const MapIndex> map_index;
std::shared_ptr<const COMDeckIndex> com_deck_index;
std::string state_filename;
std::shared_ptr<Tournament> tournaments[0x20];
std::unordered_map<std::string, std::shared_ptr<Tournament>> name_to_tournament;
std::vector<std::shared_ptr<Tournament>> menu_item_id_to_tournament;
};
} // namespace Episode3
+9 -15
View File
@@ -7,13 +7,11 @@
#include <phosg/Time.hh>
using namespace std;
class FileContentsCache {
public:
struct File {
std::string name;
shared_ptr<const std::string> data;
std::shared_ptr<const std::string> data;
uint64_t load_time;
File() = delete;
@@ -37,10 +35,8 @@ public:
return this->name_to_file.erase(key);
}
std::shared_ptr<const File> replace(
const std::string& name, std::string&& data, uint64_t t = 0);
std::shared_ptr<const File> replace(
const std::string& name, const void* data, size_t size, uint64_t t = 0);
std::shared_ptr<const File> replace(const std::string& name, std::string&& data, uint64_t t = 0);
std::shared_ptr<const File> replace(const std::string& name, const void* data, size_t size, uint64_t t = 0);
struct GetResult {
std::shared_ptr<const File> file;
@@ -52,10 +48,8 @@ public:
std::shared_ptr<const File> get_or_throw(const std::string& name);
std::shared_ptr<const File> get_or_throw(const char* name);
GetResult get(
const std::string& name, std::function<std::string(const std::string&)> generate);
GetResult get(
const char* name, std::function<std::string(const std::string&)> generate);
GetResult get(const std::string& name, std::function<std::string(const std::string&)> generate);
GetResult get(const char* name, std::function<std::string(const std::string&)> generate);
template <typename T>
struct GetObjResult {
@@ -68,7 +62,7 @@ public:
GetObjResult<T> get_obj_or_load(NameT name) {
auto res = this->get_or_load(name);
if (res.file->data->size() != sizeof(T)) {
throw runtime_error("cached string size is incorrect");
throw std::runtime_error("cached string size is incorrect");
}
return {*reinterpret_cast<const T*>(res.file->data->data()), res.file, res.generate_called};
}
@@ -76,7 +70,7 @@ public:
GetObjResult<T> get_obj_or_throw(NameT name) {
auto res = this->get_or_throw(name);
if (res.file->data->size() != sizeof(T)) {
throw runtime_error("cached string size is incorrect");
throw std::runtime_error("cached string size is incorrect");
}
return {*reinterpret_cast<const T*>(res.file->data->data()), res.file, res.generate_called};
}
@@ -86,12 +80,12 @@ public:
try {
auto& f = this->name_to_file.at(name);
if (f->data->size() != sizeof(T)) {
throw runtime_error("cached string size is incorrect");
throw std::runtime_error("cached string size is incorrect");
}
if (this->ttl_usecs && (t - f->load_time < this->ttl_usecs)) {
return {*reinterpret_cast<const T*>(f->data->data()), f, false};
}
} catch (const out_of_range& e) {
} catch (const std::out_of_range& e) {
}
T value = generate(name);
auto ret = this->replace_obj(name, value);
+118
View File
@@ -0,0 +1,118 @@
#include "GVMEncoder.hh"
#include <phosg/Encoding.hh>
#include <phosg/Image.hh>
#include <phosg/Strings.hh>
#include "Text.hh"
using namespace std;
static uint16_t encode_rgb565(uint8_t r, uint8_t g, uint8_t b) {
return ((r << 8) & 0xF800) | ((g << 3) & 0x07E0) | ((b >> 3) & 0x001F);
}
static uint16_t encode_rgb5a3(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
if ((a & 0xE0) == 0xE0) {
return 0x8000 | ((r << 7) & 0x7C00) | ((g << 2) & 0x03E0) | ((b >> 3) & 0x001F);
} else {
return ((a << 7) & 0x7000) | ((r << 4) & 0x0F00) | (g & 0x00F0) | ((b >> 4) & 0x000F);
}
}
static uint32_t encode_argb8888(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
return (a << 24) | (r << 16) | (g << 8) | b;
}
struct GVMFileEntry {
be_uint16_t file_num;
ptext<char, 28> name;
parray<be_uint32_t, 2> unknown_a1;
} __attribute__((packed));
struct GVMFileHeader {
be_uint32_t magic; // 'GVMH'
le_uint32_t header_size;
be_uint16_t flags;
be_uint16_t num_files;
} __attribute__((packed));
struct GVRHeader {
be_uint32_t magic; // 'GVRT'
le_uint32_t data_size;
be_uint16_t unknown;
uint8_t format_flags; // High 4 bits are pixel format, low 4 are data flags
GVRDataFormat data_format;
be_uint16_t width;
be_uint16_t height;
} __attribute__((packed));
string encode_gvm(const Image& img, GVRDataFormat data_format) {
if (img.get_width() > 0xFFFF) {
throw runtime_error("image is too wide to be encoded as a GVR texture");
}
if (img.get_height() > 0xFFFF) {
throw runtime_error("image is too tall to be encoded as a GVR texture");
}
if (img.get_width() & 3) {
throw runtime_error("image width is not a multiple of 4");
}
if (img.get_height() & 3) {
throw runtime_error("image height is not a multiple of 4");
}
size_t pixel_count = img.get_width() * img.get_height();
size_t pixel_bytes = 0;
switch (data_format) {
case GVRDataFormat::RGB565:
case GVRDataFormat::RGB5A3:
pixel_bytes = pixel_count * 2;
break;
case GVRDataFormat::ARGB8888:
pixel_bytes = pixel_count * 2;
break;
default:
throw invalid_argument("cannot encode pixel format");
}
StringWriter w;
w.put<GVMFileHeader>({.magic = 0x47564D48, .header_size = 0x48, .flags = 0x010F, .num_files = 1});
GVMFileEntry file_entry;
file_entry.file_num = 0;
file_entry.name = "img";
file_entry.unknown_a1.clear(0);
w.put(file_entry);
w.extend_to(0x50, 0x00);
w.put<GVRHeader>({.magic = 0x47565254,
.data_size = pixel_bytes + 8,
.unknown = 0,
.format_flags = 0,
.data_format = data_format,
.width = img.get_width(),
.height = img.get_height()});
for (size_t y = 0; y < img.get_height(); y += 4) {
for (size_t x = 0; x < img.get_width(); x += 4) {
for (size_t yy = 0; yy < 4; yy++) {
for (size_t xx = 0; xx < 4; xx++) {
uint64_t a, r, g, b;
img.read_pixel(x + xx, y + yy, &r, &g, &b, &a);
switch (data_format) {
case GVRDataFormat::RGB565:
w.put_u16b(encode_rgb565(r, g, b));
break;
case GVRDataFormat::RGB5A3:
w.put_u16b(encode_rgb5a3(r, g, b, a));
break;
case GVRDataFormat::ARGB8888:
w.put_u32b(encode_argb8888(r, g, b, a));
break;
default:
throw logic_error("cannot encode pixel format");
}
}
}
}
}
return std::move(w.str());
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <phosg/Encoding.hh>
#include <phosg/Image.hh>
#include <phosg/Strings.hh>
#include "Text.hh"
enum class GVRDataFormat : uint8_t {
INTENSITY_4 = 0x00,
INTENSITY_8 = 0x01,
INTENSITY_A4 = 0x02,
INTENSITY_A8 = 0x03,
RGB565 = 0x04,
RGB5A3 = 0x05,
ARGB8888 = 0x06,
INDEXED_4 = 0x08,
INDEXED_8 = 0x09,
DXT1 = 0x0E,
};
std::string encode_gvm(const Image& img, GVRDataFormat data_format);
+22 -2
View File
@@ -4,9 +4,11 @@
#include <phosg/Encoding.hh>
#include "Text.hh"
struct EthernetHeader {
uint8_t dest_mac[6];
uint8_t src_mac[6];
parray<uint8_t, 6> dest_mac;
parray<uint8_t, 6> src_mac;
be_uint16_t protocol;
} __attribute__((packed));
@@ -61,6 +63,24 @@ struct TCPHeader {
be_uint16_t urgent_ptr;
} __attribute__((packed));
struct DHCPHeader {
uint8_t opcode = 0;
uint8_t hardware_type = 1; // 1 = Ethernet
uint8_t hardware_address_length = 6; // 6 for Ethernet
uint8_t hops = 0;
be_uint32_t transaction_id = 0;
be_uint16_t seconds_elapsed = 0;
be_uint16_t flags = 0;
be_uint32_t client_ip_address = 0;
be_uint32_t your_ip_address = 0;
be_uint32_t server_ip_address = 0;
be_uint32_t gateway_ip_address = 0;
parray<uint8_t, 0x10> client_hardware_address;
parray<uint8_t, 0xC0> unused_bootp_legacy;
be_uint32_t magic = 0x63825363;
// Options follow here, terminated with FF
} __attribute__((packed));
struct FrameInfo {
// This is always valid
const EthernetHeader* ether;
+270 -89
View File
@@ -62,13 +62,13 @@ string IPStackSimulator::str_for_tcp_connection(shared_ptr<const IPClient> c,
}
IPStackSimulator::IPStackSimulator(
std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state)
shared_ptr<struct event_base> base,
shared_ptr<ServerState> state)
: base(base),
state(state),
pcap_text_log_file(state->ip_stack_debug ? fopen("IPStackSimulator-Log.txt", "wt") : nullptr) {
memset(this->host_mac_address_bytes, 0x90, 6);
memset(this->broadcast_mac_address_bytes, 0xFF, 6);
this->host_mac_address_bytes.clear(0x90);
this->broadcast_mac_address_bytes.clear(0xFF);
}
IPStackSimulator::~IPStackSimulator() {
@@ -77,20 +77,29 @@ IPStackSimulator::~IPStackSimulator() {
}
}
void IPStackSimulator::listen(const std::string& socket_path) {
this->add_socket(::listen(socket_path, 0, SOMAXCONN));
void IPStackSimulator::listen(const string& name, const string& socket_path) {
int fd = ::listen(socket_path, 0, SOMAXCONN);
ip_stack_simulator_log.info("Listening on Unix socket %s on fd %d as %s", socket_path.c_str(), fd, name.c_str());
this->add_socket(name, fd);
}
void IPStackSimulator::listen(const std::string& addr, int port) {
this->add_socket(::listen(addr, port, SOMAXCONN));
void IPStackSimulator::listen(const string& name, const string& addr, int port) {
if (port == 0) {
this->listen(name, addr);
} else {
int fd = ::listen(addr, port, SOMAXCONN);
string netloc_str = render_netloc(addr, port);
ip_stack_simulator_log.info("Listening on TCP interface %s on fd %d as %s", netloc_str.c_str(), fd, name.c_str());
this->add_socket(name, fd);
}
}
void IPStackSimulator::listen(int port) {
this->add_socket(::listen("", port, SOMAXCONN));
void IPStackSimulator::listen(const string& name, int port) {
this->listen(name, "", port);
}
void IPStackSimulator::add_socket(int fd) {
this->listeners.emplace(
void IPStackSimulator::add_socket(const string& name, int fd) {
unique_listener l(
evconnlistener_new(
this->base.get(),
IPStackSimulator::dispatch_on_listen_accept,
@@ -99,6 +108,7 @@ void IPStackSimulator::add_socket(int fd) {
0,
fd),
evconnlistener_free);
this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd), forward_as_tuple(name, std::move(l)));
}
uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_addr) {
@@ -112,10 +122,28 @@ uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_ad
}
}
IPStackSimulator::IPClient::IPClient(struct bufferevent* bev)
: bev(bev, bufferevent_free),
ipv4_addr(0) {
memset(this->mac_addr, 0, 6);
IPStackSimulator::IPClient::IPClient(shared_ptr<IPStackSimulator> sim, struct bufferevent* bev)
: sim(sim),
bev(bev, bufferevent_free),
mac_addr(0),
ipv4_addr(0),
idle_timeout_event(event_new(sim->base.get(), -1, EV_TIMEOUT, &IPStackSimulator::IPClient::dispatch_on_idle_timeout, this), event_free) {
struct timeval tv = usecs_to_timeval(60 * 1000 * 1000);
event_add(this->idle_timeout_event.get(), &tv);
}
void IPStackSimulator::IPClient::dispatch_on_idle_timeout(evutil_socket_t, short, void* ctx) {
reinterpret_cast<IPStackSimulator::IPClient*>(ctx)->on_idle_timeout();
}
void IPStackSimulator::IPClient::on_idle_timeout() {
auto sim = this->sim.lock();
if (sim) {
ip_stack_simulator_log.info("Idle timeout expired on virtual network %d", bufferevent_getfd(this->bev.get()));
sim->disconnect_client(this->bev.get());
} else {
ip_stack_simulator_log.info("Idle timeout expired on virtual network %d, but simulator is missing", bufferevent_getfd(this->bev.get()));
}
}
static void flush_and_free_bufferevent(struct bufferevent* bev) {
@@ -139,6 +167,11 @@ IPStackSimulator::IPClient::TCPConnection::TCPConnection()
bytes_received(0),
bytes_sent(0) {}
void IPStackSimulator::disconnect_client(struct bufferevent* bev) {
ip_stack_simulator_log.info("Virtual network %d disconnected", bufferevent_getfd(bev));
this->bev_to_client.erase(bev);
}
void IPStackSimulator::dispatch_on_listen_accept(
struct evconnlistener* listener, evutil_socket_t fd,
struct sockaddr* address, int socklen, void* ctx) {
@@ -149,12 +182,21 @@ 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);
ip_stack_simulator_log.info("Virtual network fd %d connected via fd %d", fd, listen_fd);
const ListeningSocket* listening_socket;
try {
listening_socket = &this->listening_sockets.at(listen_fd);
} catch (const out_of_range&) {
ip_stack_simulator_log.info("Virtual network %d connected via unknown listener %d; disconnecting", fd, listen_fd);
close(fd);
return;
}
ip_stack_simulator_log.info("Virtual network %d connected via %s", 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<IPClient> c(new IPClient(bev));
c->sim = this;
shared_ptr<IPClient> c(new IPClient(this->shared_from_this(), bev));
this->bev_to_client.emplace(make_pair(bev, c));
bufferevent_setcb(bev, &IPStackSimulator::dispatch_on_client_input, nullptr,
@@ -193,6 +235,9 @@ void IPStackSimulator::on_client_input(struct bufferevent* bev) {
return;
}
struct timeval tv = usecs_to_timeval(60 * 1000 * 1000);
event_add(c->idle_timeout_event.get(), &tv);
while (evbuffer_get_length(buf) >= 2) {
uint16_t frame_size;
evbuffer_copyout(buf, &frame_size, 2);
@@ -218,31 +263,29 @@ void IPStackSimulator::dispatch_on_client_error(
struct bufferevent* bev, short events, void* ctx) {
reinterpret_cast<IPStackSimulator*>(ctx)->on_client_error(bev, events);
}
void IPStackSimulator::on_client_error(struct bufferevent* bev,
short events) {
void IPStackSimulator::on_client_error(struct bufferevent* bev, short events) {
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
ip_stack_simulator_log.warning("Virtual network caused error %d (%s)", err,
evutil_socket_error_to_string(err));
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
ip_stack_simulator_log.info("Virtual network fd %d disconnected", bufferevent_getfd(bev));
this->bev_to_client.erase(bev);
this->disconnect_client(bev);
}
}
void IPStackSimulator::on_client_frame(
shared_ptr<IPClient> c, const string& frame) {
if (ip_stack_simulator_log.info("Virtual network sent frame")) {
if (ip_stack_simulator_log.debug("Virtual network sent frame")) {
print_data(stderr, frame);
fputc('\n', stderr);
}
this->log_frame(frame);
FrameInfo fi(frame);
if (ip_stack_simulator_log.should_log(LogLevel::INFO)) {
if (ip_stack_simulator_log.should_log(LogLevel::DEBUG)) {
string fi_header = fi.header_str();
ip_stack_simulator_log.info("Frame header: %s", fi_header.c_str());
ip_stack_simulator_log.debug("Frame header: %s", fi_header.c_str());
}
if (fi.arp) {
@@ -255,10 +298,14 @@ void IPStackSimulator::on_client_frame(
"IPv4 header checksum is incorrect (%04hX expected, %04hX received)",
expected_ipv4_checksum, fi.ipv4->checksum.load()));
}
if (memcmp(fi.ether->src_mac, c->mac_addr, 6)) {
// Populate the client's addresses if needed
if (c->mac_addr.is_filled_with(0)) {
c->mac_addr = fi.ether->src_mac;
} else if ((fi.ether->src_mac != c->mac_addr) && (fi.ether->src_mac != this->broadcast_mac_address_bytes)) {
throw runtime_error("client sent IPv4 packet from different MAC address");
}
if (fi.ipv4->src_addr != c->ipv4_addr) {
if ((fi.ipv4->src_addr != c->ipv4_addr) && (fi.ipv4->src_addr != 0)) {
throw runtime_error("client sent IPv4 packet from different IPv4 address");
}
@@ -301,18 +348,14 @@ void IPStackSimulator::on_client_arp_frame(
throw runtime_error("ARP payload too small");
}
// Populate the client's addresses if needed
if (!memcmp(c->mac_addr, "\0\0\0\0\0\0", 6)) {
memcpy(c->mac_addr, fi.ether->src_mac, 6);
}
if (c->ipv4_addr == 0) {
c->ipv4_addr = *reinterpret_cast<const be_uint32_t*>(
reinterpret_cast<const uint8_t*>(fi.payload) + 6);
}
EthernetHeader r_ether;
memcpy(r_ether.dest_mac, fi.ether->src_mac, 6);
memcpy(r_ether.src_mac, this->host_mac_address_bytes, 6);
r_ether.dest_mac = fi.ether->src_mac;
r_ether.src_mac = this->host_mac_address_bytes;
r_ether.protocol = fi.ether->protocol;
ARPHeader r_arp;
@@ -336,7 +379,7 @@ void IPStackSimulator::on_client_arp_frame(
const char* payload_bytes = reinterpret_cast<const char*>(fi.payload);
uint8_t r_payload[20];
memcpy(&r_payload[0], this->host_mac_address_bytes, 6);
memcpy(&r_payload[0], this->host_mac_address_bytes.data(), 6);
memcpy(&r_payload[6], payload_bytes + 16, 4);
memcpy(&r_payload[10], payload_bytes, 10);
@@ -348,7 +391,7 @@ void IPStackSimulator::on_client_arp_frame(
evbuffer_add(out_buf, &r_arp, sizeof(r_arp));
evbuffer_add(out_buf, r_payload, sizeof(r_payload));
ip_stack_simulator_log.info("Sending ARP response");
ip_stack_simulator_log.debug("Sending ARP response");
if (this->pcap_text_log_file) {
StringWriter w;
@@ -361,17 +404,13 @@ void IPStackSimulator::on_client_arp_frame(
void IPStackSimulator::on_client_udp_frame(
shared_ptr<IPClient> c, const FrameInfo& fi) {
// We only implement the DNS server here
if (fi.udp->dest_port != 53) {
throw runtime_error("UDP packet is not DNS");
}
if (fi.payload_size < 0x0C) {
throw runtime_error("DNS payload too small");
}
// We only implement DHCP and newserv's DNS server here
// Every received UDP packet will elicit exactly one UDP response from
// newserv, so we prepare the response headers in advance
EthernetHeader r_ether;
memcpy(r_ether.dest_mac, fi.ether->src_mac, 6);
memcpy(r_ether.src_mac, this->host_mac_address_bytes, 6);
r_ether.dest_mac = fi.ether->src_mac;
r_ether.src_mac = this->host_mac_address_bytes;
r_ether.protocol = fi.ether->protocol;
IPv4Header r_ipv4;
@@ -392,39 +431,164 @@ void IPStackSimulator::on_client_udp_frame(
// r_udp.size filled in later
// r_udp.checksum filled in later
uint32_t resolved_address = this->connect_address_for_remote_address(c->ipv4_addr);
string r_data;
if (fi.udp->dest_port == 67) { // DHCP
StringReader r(fi.payload, fi.payload_size);
const auto& dhcp = r.get<DHCPHeader>();
if (dhcp.hardware_type != 1) {
throw runtime_error("unknown DHCP hardware type");
}
if (dhcp.hardware_address_length != 6) {
throw runtime_error("unknown DHCP hardware address length");
}
if (dhcp.magic != 0x63825363) {
throw runtime_error("incorrect DHCP magic cookie");
}
if (dhcp.opcode != 1) { // Request
throw runtime_error("DHCP packet is not a request");
}
string r_data = DNSServer::response_for_query(
fi.payload, fi.payload_size, resolved_address);
unordered_map<uint8_t, string> option_data;
for (;;) {
uint8_t option = r.get_u8();
if (option == 0xFF) {
break;
}
uint8_t size = r.get_u8();
option_data.emplace(option, r.read(size));
}
r_ipv4.size = sizeof(IPv4Header) + sizeof(UDPHeader) + r_data.size();
r_udp.size = sizeof(UDPHeader) + r_data.size();
r_ipv4.checksum = FrameInfo::computed_ipv4_header_checksum(r_ipv4);
r_udp.checksum = FrameInfo::computed_udp4_checksum(
r_ipv4, r_udp, r_data.data(), r_data.size());
uint8_t command = 0;
try {
command = option_data.at(53).at(0);
} catch (const out_of_range&) {
throw runtime_error("client did not send a DHCP command option");
}
struct evbuffer* out_buf = bufferevent_get_output(c->bev.get());
if (command == 7) {
// Release IP address (we just ignore these)
if (ip_stack_simulator_log.should_log(LogLevel::INFO)) {
string remote_str = this->str_for_ipv4_netloc(fi.ipv4->src_addr, fi.udp->src_port);
ip_stack_simulator_log.info("Sending DNS response to %s", remote_str.c_str());
print_data(stderr, r_data);
} else if ((command == 1) || (command == 3)) {
// Populate the client's addresses
c->mac_addr = dhcp.client_hardware_address.data();
c->ipv4_addr = 0x0A000105; // 10.0.1.5
// In this case, the client doesn't know its IPv4 address or ours yet,
// so we overwrite the existing fields with the appropriate addresses.
r_ipv4.src_addr = 0x0A000101; // 10.0.1.1
r_ipv4.dest_addr = c->ipv4_addr;
if ((command != 1) && (command != 3)) {
throw runtime_error("client sent unknown DHCP command option");
}
StringWriter w;
DHCPHeader r_dhcp;
r_dhcp.opcode = 2; // Response
r_dhcp.hardware_type = 1; // Ethernet
r_dhcp.hardware_address_length = 6; // Ethernet
r_dhcp.hops = 0;
r_dhcp.transaction_id = dhcp.transaction_id;
r_dhcp.seconds_elapsed = 0;
r_dhcp.flags = 0;
r_dhcp.client_ip_address = 0;
r_dhcp.your_ip_address = r_ipv4.dest_addr;
r_dhcp.server_ip_address = r_ipv4.src_addr;
r_dhcp.gateway_ip_address = 0;
r_dhcp.client_hardware_address = c->mac_addr;
r_dhcp.unused_bootp_legacy.clear(0);
r_dhcp.magic = 0x63825363;
w.put(r_dhcp);
// DHCP message type option
w.put_u8(53);
w.put_u8(1);
w.put_u8(static_cast<uint8_t>((command == 3) ? 5 : 2)); // Offer or ack
// DHCP server ID option
w.put_u8(54);
w.put_u8(4);
w.put_u32b(0x0A000101); // 10.0.1.1
// Lease time option
w.put_u8(51);
w.put_u8(4);
w.put_u32b(60 * 60 * 24 * 7); // 1 week
// Renewal time option
w.put_u8(58);
w.put_u8(4);
w.put_u32b(60 * 60 * 24 * 7); // 1 week
// Rebind time option
w.put_u8(59);
w.put_u8(4);
w.put_u32b(60 * 60 * 24 * 7); // 1 week
// Subnet mask option
w.put_u8(1);
w.put_u8(4);
w.put_u32b(0xFFFFFF00); // 255.255.255.0
// Broadcast IP option
w.put_u8(28);
w.put_u8(4);
w.put_u32b(c->ipv4_addr | 0x000000FF);
// DNS server option
w.put_u8(6);
w.put_u8(4);
w.put_u32b(0x0A000101); // 10.0.1.1
// Domain name option
w.put_u8(15);
w.put_u8(7);
w.write("newserv");
// Default gateway option
w.put_u8(3);
w.put_u8(4);
w.put_u32b(0x0A000101); // 10.0.1.1
// End option list
w.put_u8(0xFF);
r_data = std::move(w.str());
} else {
throw runtime_error("client sent unknown DHCP command");
}
} else if (fi.udp->dest_port == 53) { // DNS
if (fi.payload_size < 0x0C) {
throw runtime_error("DNS payload too small");
}
uint32_t resolved_address = this->connect_address_for_remote_address(c->ipv4_addr);
r_data = DNSServer::response_for_query(fi.payload, fi.payload_size, resolved_address);
} else { // Not DHCP or DNS
throw runtime_error("UDP packet is not DHCP or DNS");
}
uint16_t frame_size = sizeof(r_ether) + sizeof(r_ipv4) + sizeof(r_udp) + r_data.size();
evbuffer_add(out_buf, &frame_size, 2);
evbuffer_add(out_buf, &r_ether, sizeof(r_ether));
evbuffer_add(out_buf, &r_ipv4, sizeof(r_ipv4));
evbuffer_add(out_buf, &r_udp, sizeof(r_udp));
evbuffer_add(out_buf, r_data.data(), r_data.size());
if (!r_data.empty()) {
r_ipv4.size = sizeof(IPv4Header) + sizeof(UDPHeader) + r_data.size();
r_udp.size = sizeof(UDPHeader) + r_data.size();
r_ipv4.checksum = FrameInfo::computed_ipv4_header_checksum(r_ipv4);
r_udp.checksum = FrameInfo::computed_udp4_checksum(
r_ipv4, r_udp, r_data.data(), r_data.size());
if (this->pcap_text_log_file) {
StringWriter w;
w.write(&r_ether, sizeof(r_ether));
w.write(&r_ipv4, sizeof(r_ipv4));
w.write(&r_udp, sizeof(r_udp));
w.write(r_data.data(), r_data.size());
this->log_frame(w.str());
struct evbuffer* out_buf = bufferevent_get_output(c->bev.get());
if (ip_stack_simulator_log.should_log(LogLevel::DEBUG)) {
string remote_str = this->str_for_ipv4_netloc(fi.ipv4->src_addr, fi.udp->src_port);
ip_stack_simulator_log.debug("Sending UDP response to %s", remote_str.c_str());
print_data(stderr, r_data);
}
uint16_t frame_size = sizeof(r_ether) + sizeof(r_ipv4) + sizeof(r_udp) + r_data.size();
evbuffer_add(out_buf, &frame_size, 2);
evbuffer_add(out_buf, &r_ether, sizeof(r_ether));
evbuffer_add(out_buf, &r_ipv4, sizeof(r_ipv4));
evbuffer_add(out_buf, &r_udp, sizeof(r_udp));
evbuffer_add(out_buf, r_data.data(), r_data.size());
if (this->pcap_text_log_file) {
StringWriter w;
w.write(&r_ether, sizeof(r_ether));
w.write(&r_ipv4, sizeof(r_ipv4));
w.write(&r_udp, sizeof(r_udp));
w.write(r_data.data(), r_data.size());
this->log_frame(w.str());
}
}
}
@@ -451,7 +615,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) {
ip_stack_simulator_log.info("Virtual network sent TCP frame (seq=%08" PRIX32 ", ack=%08" PRIX32 ")",
ip_stack_simulator_log.debug("Virtual network sent TCP frame (seq=%08" PRIX32 ", ack=%08" PRIX32 ")",
fi.tcp->seq_num.load(), fi.tcp->ack_num.load());
if (fi.tcp->flags & (TCPHeader::Flag::NS | TCPHeader::Flag::CWR | TCPHeader::Flag::ECE | TCPHeader::Flag::URG)) {
@@ -541,13 +705,13 @@ 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);
ip_stack_simulator_log.info("Client resent SYN for TCP connection %s",
ip_stack_simulator_log.debug("Client resent SYN for TCP connection %s",
conn_str.c_str());
}
// Send a SYN+ACK (send_tcp_frame always adds the ACK flag)
this->send_tcp_frame(c, conn, TCPHeader::Flag::SYN);
ip_stack_simulator_log.info("Sent SYN+ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")",
ip_stack_simulator_log.debug("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);
} else {
@@ -562,7 +726,7 @@ void IPStackSimulator::on_client_tcp_frame(
bool conn_valid = true;
if (fi.tcp->flags & TCPHeader::Flag::ACK) {
ip_stack_simulator_log.info("Client sent ACK %08" PRIX32, fi.tcp->ack_num.load());
ip_stack_simulator_log.debug("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) {
throw runtime_error("first ack_num was not acked_server_seq + 1");
@@ -572,7 +736,7 @@ void IPStackSimulator::on_client_tcp_frame(
} else {
if (seq_num_greater(fi.tcp->ack_num, conn->acked_server_seq)) {
ip_stack_simulator_log.info("Advancing acked_server_seq from %08" PRIX32, conn->acked_server_seq);
ip_stack_simulator_log.debug("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());
if (pending_bytes < ack_delta) {
@@ -584,7 +748,7 @@ void IPStackSimulator::on_client_tcp_frame(
conn->resend_push_usecs = DEFAULT_RESEND_PUSH_USECS;
conn->next_push_max_frame_size = conn->max_frame_size;
ip_stack_simulator_log.info("Removed %08" PRIX32 " bytes from pending buffer and advanced acked_server_seq to %08" PRIX32,
ip_stack_simulator_log.debug("Removed %08" PRIX32 " bytes from pending buffer and advanced acked_server_seq to %08" PRIX32,
ack_delta, conn->acked_server_seq);
} else if (seq_num_less(fi.tcp->ack_num, conn->acked_server_seq)) {
@@ -662,10 +826,10 @@ void IPStackSimulator::on_client_tcp_frame(
bool was_logged;
if (payload_skip_bytes) {
was_logged = ip_stack_simulator_log.info("Client sent data on TCP connection %s, overlapping existing ack'ed data (0x%zX bytes ignored)",
was_logged = ip_stack_simulator_log.debug("Client sent data on TCP connection %s, overlapping existing ack'ed data (0x%zX bytes ignored)",
conn_str.c_str(), payload_skip_bytes);
} else {
was_logged = ip_stack_simulator_log.info("Client sent data on TCP connection %s",
was_logged = ip_stack_simulator_log.debug("Client sent data on TCP connection %s",
conn_str.c_str());
}
if (was_logged) {
@@ -688,7 +852,7 @@ void IPStackSimulator::on_client_tcp_frame(
// Send an ACK
this->send_tcp_frame(c, *conn);
ip_stack_simulator_log.info("Sent PSH ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ", bytes_received=0x%zX)",
ip_stack_simulator_log.debug("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);
}
@@ -758,7 +922,7 @@ void IPStackSimulator::send_pending_push_frame(
size_t bytes_to_send = min<size_t>(pending_bytes, conn.next_push_max_frame_size);
ip_stack_simulator_log.info("Sending PSH frame with seq_num %08" PRIX32 ", 0x%zX/0x%zX data bytes",
ip_stack_simulator_log.debug("Sending PSH frame with seq_num %08" PRIX32 ", 0x%zX/0x%zX data bytes",
conn.acked_server_seq, bytes_to_send, pending_bytes);
this->send_tcp_frame(c, conn, TCPHeader::Flag::PSH, conn.pending_data.get(),
@@ -790,8 +954,8 @@ void IPStackSimulator::send_tcp_frame(
}
EthernetHeader ether;
memcpy(ether.dest_mac, c->mac_addr, 6);
memcpy(ether.src_mac, this->host_mac_address_bytes, 6);
ether.dest_mac = c->mac_addr;
ether.src_mac = this->host_mac_address_bytes;
ether.protocol = 0x0800; // IPv4
IPv4Header ipv4;
@@ -850,7 +1014,12 @@ void IPStackSimulator::dispatch_on_resend_push(evutil_socket_t, short, void* ctx
if (!c.get()) {
ip_stack_simulator_log.warning("Resend push event triggered for deleted client; ignoring");
} else {
c->sim->on_resend_push(c, *conn);
auto sim = c->sim.lock();
if (!sim) {
ip_stack_simulator_log.warning("Resend push event triggered for client on deleted simulator; ignoring");
} else {
sim->on_resend_push(c, *conn);
}
}
}
@@ -864,15 +1033,23 @@ void IPStackSimulator::dispatch_on_server_input(struct bufferevent*, void* ctx)
if (!c.get()) {
ip_stack_simulator_log.warning("Server input event triggered for deleted client; ignoring");
} else {
c->sim->on_server_input(c, *conn);
auto sim = c->sim.lock();
if (!sim) {
ip_stack_simulator_log.warning("Server input event triggered for client on deleted simulator; ignoring");
} else {
sim->on_server_input(c, *conn);
}
}
}
void IPStackSimulator::on_server_input(shared_ptr<IPClient> c, IPClient::TCPConnection& conn) {
struct evbuffer* buf = bufferevent_get_input(conn.server_bev.get());
ip_stack_simulator_log.info("Server input event: 0x%zX bytes to read",
ip_stack_simulator_log.debug("Server input event: 0x%zX bytes to read",
evbuffer_get_length(buf));
struct timeval tv = usecs_to_timeval(60 * 1000 * 1000);
event_add(c->idle_timeout_event.get(), &tv);
evbuffer_add_buffer(conn.pending_data.get(), buf);
this->send_pending_push_frame(c, conn);
}
@@ -884,7 +1061,12 @@ void IPStackSimulator::dispatch_on_server_error(
if (!c.get()) {
ip_stack_simulator_log.warning("Server error event triggered for deleted client; ignoring");
} else {
c->sim->on_server_error(c, *conn, events);
auto sim = c->sim.lock();
if (!sim) {
ip_stack_simulator_log.warning("Server error event triggered for client on deleted simulator; ignoring");
} else {
sim->on_server_error(c, *conn, events);
}
}
}
@@ -904,8 +1086,7 @@ 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);
ip_stack_simulator_log.info("Server closed TCP connection %s",
conn_str.c_str());
ip_stack_simulator_log.info("Server closed TCP connection %s", conn_str.c_str());
c->tcp_connections.erase(this->tcp_conn_key_for_connection(conn));
}
}
+37 -26
View File
@@ -10,18 +10,19 @@
#include "ProxyServer.hh"
#include "Server.hh"
#include "ServerState.hh"
#include "Text.hh"
class IPStackSimulator {
class IPStackSimulator : public std::enable_shared_from_this<IPStackSimulator> {
public:
IPStackSimulator(
std::shared_ptr<struct event_base> base,
std::shared_ptr<ServerState> state);
~IPStackSimulator();
void listen(const std::string& socket_path);
void listen(const std::string& addr, int port);
void listen(int port);
void add_socket(int fd);
void listen(const std::string& name, const std::string& socket_path);
void listen(const std::string& name, const std::string& addr, int port);
void listen(const std::string& name, int port);
void add_socket(const std::string& name, int fd);
static uint32_t connect_address_for_remote_address(uint32_t remote_addr);
@@ -35,10 +36,10 @@ private:
using unique_event = std::unique_ptr<struct event, void (*)(struct event*)>;
struct IPClient {
IPStackSimulator* sim;
std::weak_ptr<IPStackSimulator> sim;
unique_bufferevent bev;
uint8_t mac_addr[6];
parray<uint8_t, 6> mac_addr;
uint32_t ipv4_addr;
struct TCPConnection {
@@ -72,38 +73,51 @@ private:
};
std::unordered_map<uint64_t, TCPConnection> tcp_connections;
IPClient(struct bufferevent* bev);
unique_event idle_timeout_event;
IPClient(std::shared_ptr<IPStackSimulator> sim, struct bufferevent* bev);
static void dispatch_on_idle_timeout(evutil_socket_t fd, short events, void* ctx);
void on_idle_timeout();
};
std::unordered_set<unique_listener> listeners;
struct ListeningSocket {
std::string name;
unique_listener listener;
ListeningSocket(const std::string& name, unique_listener&& l)
: name(name),
listener(std::move(l)) {}
};
std::unordered_map<int, ListeningSocket> listening_sockets;
std::unordered_map<struct bufferevent*, std::shared_ptr<IPClient>> bev_to_client;
uint8_t host_mac_address_bytes[6];
uint8_t broadcast_mac_address_bytes[6];
parray<uint8_t, 6> host_mac_address_bytes;
parray<uint8_t, 6> broadcast_mac_address_bytes;
FILE* pcap_text_log_file;
static uint64_t tcp_conn_key_for_connection(
const IPClient::TCPConnection& conn);
static uint64_t tcp_conn_key_for_client_frame(
const IPv4Header& ipv4, const TCPHeader& tcp);
void disconnect_client(struct bufferevent* bev);
static uint64_t tcp_conn_key_for_connection(const IPClient::TCPConnection& conn);
static uint64_t tcp_conn_key_for_client_frame(const IPv4Header& ipv4, const TCPHeader& tcp);
static uint64_t tcp_conn_key_for_client_frame(const FrameInfo& fi);
static std::string str_for_ipv4_netloc(uint32_t addr, uint16_t port);
static std::string str_for_tcp_connection(std::shared_ptr<const IPClient> c,
const IPClient::TCPConnection& conn);
const IPClient::TCPConnection& conn);
static void dispatch_on_listen_accept(struct evconnlistener* listener,
evutil_socket_t fd, struct sockaddr* address, int socklen, void* ctx);
evutil_socket_t fd, struct sockaddr* address, int socklen, void* ctx);
void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd,
struct sockaddr* address, int socklen);
struct sockaddr* address, int socklen);
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
void on_listen_error(struct evconnlistener* listener);
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
void on_client_input(struct bufferevent* bev);
static void dispatch_on_client_error(struct bufferevent* bev, short events,
void* ctx);
static void dispatch_on_client_error(struct bufferevent* bev, short events, void* ctx);
void on_client_error(struct bufferevent* bev, short events);
void on_client_frame(std::shared_ptr<IPClient> c, const std::string& frame);
@@ -111,18 +125,15 @@ private:
void on_client_udp_frame(std::shared_ptr<IPClient> c, const FrameInfo& fi);
void on_client_tcp_frame(std::shared_ptr<IPClient> c, const FrameInfo& fi);
static void dispatch_on_resend_push(evutil_socket_t fd, short events,
void* ctx);
static void dispatch_on_resend_push(evutil_socket_t fd, short events, void* ctx);
void on_resend_push(std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn);
static void dispatch_on_server_input(struct bufferevent* bev, void* ctx);
void on_server_input(std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn);
static void dispatch_on_server_error(struct bufferevent* bev, short events,
void* ctx);
static void dispatch_on_server_error(struct bufferevent* bev, short events, void* ctx);
void on_server_error(std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn, short events);
void send_pending_push_frame(
std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn);
void send_pending_push_frame(std::shared_ptr<IPClient> c, IPClient::TCPConnection& conn);
void send_tcp_frame(
std::shared_ptr<IPClient> c,
IPClient::TCPConnection& conn,
+47 -92
View File
@@ -21,7 +21,7 @@ ItemCreator::ItemCreator(
uint8_t difficulty,
uint8_t section_id,
uint32_t random_seed,
shared_ptr<const Restrictions> restrictions)
shared_ptr<const BattleRules> restrictions)
: log("[ItemCreator] "),
episode(episode),
mode(mode),
@@ -50,7 +50,7 @@ bool ItemCreator::are_rare_drops_allowed() const {
}
uint8_t ItemCreator::normalize_area_number(uint8_t area) const {
if (!this->item_drop_sub || (area < 0x10) || (area > 0x11)) {
if (!this->restrictions || (this->restrictions->box_drop_area == 0) || (area < 0x10) || (area > 0x11)) {
switch (this->episode) {
case Episode::EP1:
if (area >= 15) {
@@ -102,7 +102,7 @@ uint8_t ItemCreator::normalize_area_number(uint8_t area) const {
}
} else {
return this->item_drop_sub->override_area;
return this->restrictions->box_drop_area;
}
}
@@ -111,8 +111,7 @@ ItemData ItemCreator::on_box_item_drop(uint8_t area) {
}
ItemData ItemCreator::on_monster_item_drop(uint32_t enemy_type, uint8_t area) {
return this->on_monster_item_drop_with_norm_area(
enemy_type, normalize_area_number(area) - 1);
return this->on_monster_item_drop_with_norm_area(enemy_type, normalize_area_number(area) - 1);
}
ItemData ItemCreator::on_box_item_drop_with_norm_area(uint8_t area_norm) {
@@ -152,10 +151,9 @@ ItemData ItemCreator::on_box_item_drop_with_norm_area(uint8_t area_norm) {
return item;
}
ItemData ItemCreator::on_monster_item_drop_with_norm_area(
uint32_t enemy_type, uint8_t norm_area) {
ItemData ItemCreator::on_monster_item_drop_with_norm_area(uint32_t enemy_type, uint8_t norm_area) {
if (enemy_type > 0x58) {
this->log.info("Invalid enemy type: %" PRIX32, enemy_type);
this->log.warning("Invalid enemy type: %" PRIX32, enemy_type);
return ItemData();
}
this->log.info("Enemy type: %" PRIX32, enemy_type);
@@ -224,8 +222,7 @@ ItemData ItemCreator::on_monster_item_drop_with_norm_area(
return item;
}
ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(
uint8_t area_norm) {
ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area_norm) {
ItemData item;
if (!this->are_rare_drops_allowed()) {
return item;
@@ -276,11 +273,9 @@ bool ItemCreator::should_allow_meseta_drops() const {
return (this->mode != GameMode::CHALLENGE);
}
ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(
uint32_t enemy_type) {
ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_type) {
ItemData item;
if (this->are_rare_drops_allowed() &&
(enemy_type > 0) && (enemy_type < 0x58)) {
if (this->are_rare_drops_allowed() && (enemy_type > 0) && (enemy_type < 0x58)) {
// Note: In the original implementation, enemies can only have one possible
// rare drop. In our implementation, they can have multiple rare drops if
// JSONRareItemSet is used (the other RareItemSet implementations never
@@ -441,12 +436,14 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const {
// (HP/Resurrection and TP/Resurrection) only exist on BB.
if (item.data1[0] == 1) {
if ((item.data1[1] == 3) && (((item.data1[2] >= 0x33) && (item.data1[2] <= 0x38)) || (item.data1[2] == 0x61) || (item.data1[2] == 0x62))) {
this->log.info("Restricted: restore items not allowed in Challenge mode");
this->log.info("Restricted: restore units not allowed in Challenge mode");
item.clear();
return;
}
} else if (item.data1[0] == 4) {
this->log.info("Restricted: meseta not allowed in Challenge mode");
item.clear();
return;
}
}
@@ -455,16 +452,16 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const {
case 0:
case 1:
switch (this->restrictions->weapon_and_armor_mode) {
case Restrictions::WeaponAndArmorMode::ALL_ON:
case Restrictions::WeaponAndArmorMode::ONLY_PICKING:
case BattleRules::WeaponAndArmorMode::ALLOW:
case BattleRules::WeaponAndArmorMode::CLEAR_AND_ALLOW:
break;
case Restrictions::WeaponAndArmorMode::NO_RARE:
case BattleRules::WeaponAndArmorMode::FORBID_RARES:
if (this->item_parameter_table->is_item_rare(item)) {
this->log.info("Restricted: rare items not allowed");
item.clear();
}
break;
case Restrictions::WeaponAndArmorMode::ALL_OFF:
case BattleRules::WeaponAndArmorMode::FORBID_ALL:
this->log.info("Restricted: weapons and armors not allowed");
item.clear();
break;
@@ -473,30 +470,30 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const {
}
break;
case 2:
if (this->restrictions->forbid_mags) {
if (this->restrictions->mag_mode == BattleRules::MagMode::FORBID_ALL) {
this->log.info("Restricted: mags not allowed");
item.clear();
}
break;
case 3:
if (this->restrictions->tool_mode == Restrictions::ToolMode::ALL_OFF) {
if (this->restrictions->tool_mode == BattleRules::ToolMode::FORBID_ALL) {
this->log.info("Restricted: tools not allowed");
item.clear();
} else if (item.data1[1] == 2) {
switch (this->restrictions->tech_disk_mode) {
case Restrictions::TechDiskMode::ON:
case BattleRules::TechDiskMode::ALLOW:
break;
case Restrictions::TechDiskMode::OFF:
case BattleRules::TechDiskMode::FORBID_ALL:
this->log.info("Restricted: tech disks not allowed");
item.clear();
break;
case Restrictions::TechDiskMode::LIMIT_LEVEL:
case BattleRules::TechDiskMode::LIMIT_LEVEL:
this->log.info("Restricted: tech disk level limited to %hhu",
static_cast<uint8_t>(this->restrictions->max_tech_disk_level + 1));
if (this->restrictions->max_tech_disk_level == 0) {
static_cast<uint8_t>(this->restrictions->max_tech_level + 1));
if (this->restrictions->max_tech_level == 0) {
item.data1[2] = 0;
} else {
item.data1[2] %= this->restrictions->max_tech_disk_level;
item.data1[2] %= this->restrictions->max_tech_level;
}
break;
default:
@@ -508,7 +505,7 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const {
}
break;
case 4:
if (this->restrictions->meseta_drop_mode == Restrictions::MesetaDropMode::OFF) {
if (this->restrictions->meseta_mode == BattleRules::MesetaMode::FORBID_ALL) {
this->log.info("Restricted: meseta not allowed");
item.clear();
}
@@ -519,8 +516,7 @@ void ItemCreator::clear_item_if_restricted(ItemData& item) const {
}
}
void ItemCreator::generate_common_item_variances(
uint32_t norm_area, ItemData& item) {
void ItemCreator::generate_common_item_variances(uint32_t norm_area, ItemData& item) {
switch (item.data1[0]) {
case 0:
this->generate_common_weapon_variances(norm_area, item);
@@ -529,14 +525,12 @@ void ItemCreator::generate_common_item_variances(
if (item.data1[1] == 3) {
float f1 = 1.0 + this->pt->unit_maxes[norm_area];
float f2 = this->rand_float_0_1_from_crypt();
this->generate_common_unit_variances(
static_cast<uint32_t>(f1 * f2) & 0xFF, item);
this->generate_common_unit_variances(static_cast<uint32_t>(f1 * f2) & 0xFF, item);
if (item.data1[2] == 0xFF) {
item.clear();
}
} else {
this->generate_common_armor_or_shield_type_and_variances(
norm_area, item);
this->generate_common_armor_or_shield_type_and_variances(norm_area, item);
}
break;
case 2:
@@ -546,9 +540,7 @@ void ItemCreator::generate_common_item_variances(
this->generate_common_tool_variances(norm_area, item);
break;
case 4:
item.data2d = this->choose_meseta_amount(
this->pt->box_meseta_ranges, norm_area) &
0xFFFF;
item.data2d = this->choose_meseta_amount(this->pt->box_meseta_ranges, norm_area) & 0xFFFF;
break;
default:
// Note: The original code does the following here:
@@ -584,19 +576,16 @@ void ItemCreator::generate_common_armor_slots_and_bonuses(ItemData& item) {
this->generate_common_armor_slot_count(item);
}
const auto& def = this->item_parameter_table->get_armor_or_shield(
item.data1[1], item.data1[2]);
const auto& def = this->item_parameter_table->get_armor_or_shield(item.data1[1], item.data1[2]);
item.set_armor_or_shield_defense_bonus(def.dfp_range * this->rand_float_0_1_from_crypt());
item.set_common_armor_evasion_bonus(def.evp_range * this->rand_float_0_1_from_crypt());
}
void ItemCreator::generate_common_armor_slot_count(ItemData& item) {
item.data1[5] = this->get_rand_from_weighted_tables_1d(
this->pt->armor_slot_count_prob_table);
item.data1[5] = this->get_rand_from_weighted_tables_1d(this->pt->armor_slot_count_prob_table);
}
void ItemCreator::generate_common_tool_variances(
uint32_t area_norm, ItemData& item) {
void ItemCreator::generate_common_tool_variances(uint32_t area_norm, ItemData& item) {
item.clear();
uint8_t tool_class = this->get_rand_from_weighted_tables_2d_vertical(
@@ -615,8 +604,7 @@ void ItemCreator::generate_common_tool_variances(
this->set_tool_item_amount_to_1(item);
}
uint8_t ItemCreator::generate_tech_disk_level(
uint32_t tech_num, uint32_t area_norm) {
uint8_t ItemCreator::generate_tech_disk_level(uint32_t tech_num, uint32_t area_norm) {
uint8_t min = this->pt->technique_level_ranges[tech_num][area_norm].min;
uint8_t max = this->pt->technique_level_ranges[tech_num][area_norm].max;
@@ -628,8 +616,7 @@ uint8_t ItemCreator::generate_tech_disk_level(
return min;
}
void ItemCreator::generate_common_tool_type(
uint8_t tool_class, ItemData& item) const {
void ItemCreator::generate_common_tool_type(uint8_t tool_class, ItemData& item) const {
auto data = this->item_parameter_table->find_tool_by_class(tool_class);
item.data1[0] = 0x03;
item.data1[1] = data.first;
@@ -643,8 +630,7 @@ void ItemCreator::generate_common_mag_variances(ItemData& item) const {
}
}
void ItemCreator::generate_common_weapon_variances(
uint8_t area_norm, ItemData& item) {
void ItemCreator::generate_common_weapon_variances(uint8_t area_norm, ItemData& item) {
item.clear();
item.data1[0] = 0x00;
@@ -843,8 +829,7 @@ IntT ItemCreator::get_rand_from_weighted_tables(
}
template <typename IntT, size_t X>
IntT ItemCreator::get_rand_from_weighted_tables_1d(
const parray<IntT, X>& tables) {
IntT ItemCreator::get_rand_from_weighted_tables_1d(const parray<IntT, X>& tables) {
return ItemCreator::get_rand_from_weighted_tables<IntT>(tables.data(), 0, X, 1);
}
@@ -981,8 +966,7 @@ void ItemCreator::generate_armor_shop_armors(
}
}
void ItemCreator::generate_armor_shop_shields(
vector<ItemData>& shop, size_t player_level) {
void ItemCreator::generate_armor_shop_shields(vector<ItemData>& shop, size_t player_level) {
size_t num_items;
if (player_level < 11) {
num_items = 4;
@@ -1025,8 +1009,7 @@ void ItemCreator::generate_armor_shop_shields(
}
}
void ItemCreator::generate_armor_shop_units(
vector<ItemData>& shop, size_t player_level) {
void ItemCreator::generate_armor_shop_units(vector<ItemData>& shop, size_t player_level) {
size_t num_items;
if (player_level < 11) {
return; // num_items = 0
@@ -1172,8 +1155,7 @@ void ItemCreator::generate_rare_tool_shop_recovery_items(
}
}
void ItemCreator::generate_tool_shop_tech_disks(
vector<ItemData>& shop, size_t player_level) {
void ItemCreator::generate_tool_shop_tech_disks(vector<ItemData>& shop, size_t player_level) {
size_t num_items;
if (player_level < 11) {
num_items = 4;
@@ -1206,8 +1188,7 @@ void ItemCreator::generate_tool_shop_tech_disks(
item.data1[0] = 3;
item.data1[1] = 2;
item.data1[4] = tech_num_map.at(tech_num_index);
this->choose_tech_disk_level_for_tool_shop(
item, player_level, tech_num_index);
this->choose_tech_disk_level_for_tool_shop(item, player_level, tech_num_index);
if (this->shop_does_not_contain_duplicate_tech_disk(shop, item)) {
shop.emplace_back(std::move(item));
items_generated++;
@@ -1435,8 +1416,7 @@ vector<ItemData> ItemCreator::generate_weapon_shop_contents(size_t player_level)
return shop;
}
void ItemCreator::generate_weapon_shop_item_grind(
ItemData& item, size_t player_level) {
void ItemCreator::generate_weapon_shop_item_grind(ItemData& item, size_t player_level) {
size_t table_index;
if (player_level < 4) {
table_index = 0;
@@ -1464,8 +1444,7 @@ void ItemCreator::generate_weapon_shop_item_grind(
this->rand_int(range->max + 1), range->min, weapon_def.max_grind);
}
void ItemCreator::generate_weapon_shop_item_special(
ItemData& item, size_t player_level) {
void ItemCreator::generate_weapon_shop_item_special(ItemData& item, size_t player_level) {
ProbabilityTable<uint8_t, 100> pt;
size_t table_index;
@@ -1513,27 +1492,7 @@ void ItemCreator::generate_weapon_shop_item_special(
}
static const array<int8_t, 20> bonus_values = {
-50,
-45,
-40,
-35,
-30,
-25,
-20,
-15,
-10,
-5,
5,
10,
15,
20,
25,
30,
35,
40,
45,
50,
};
-50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50};
void ItemCreator::generate_weapon_shop_item_bonus1(
ItemData& item, size_t player_level) {
@@ -1558,8 +1517,7 @@ void ItemCreator::generate_weapon_shop_item_bonus1(
table_index = 8;
}
const auto* type_table = this->weapon_random_set->get_bonus_type_table(
0, table_index);
const auto* type_table = this->weapon_random_set->get_bonus_type_table(0, table_index);
ProbabilityTable<uint8_t, 100> pt;
for (size_t z = 0; z < type_table->size(); z++) {
const auto& e = type_table->at(z);
@@ -1581,8 +1539,7 @@ void ItemCreator::generate_weapon_shop_item_bonus1(
}
}
void ItemCreator::generate_weapon_shop_item_bonus2(
ItemData& item, size_t player_level) {
void ItemCreator::generate_weapon_shop_item_bonus2(ItemData& item, size_t player_level) {
size_t table_index;
if (player_level < 6) {
table_index = 0;
@@ -1604,8 +1561,7 @@ void ItemCreator::generate_weapon_shop_item_bonus2(
table_index = 8;
}
const auto* type_table = this->weapon_random_set->get_bonus_type_table(
1, table_index);
const auto* type_table = this->weapon_random_set->get_bonus_type_table(1, table_index);
ProbabilityTable<uint8_t, 100> pt;
for (size_t z = 0; z < type_table->size(); z++) {
const auto& e = type_table->at(z);
@@ -1676,8 +1632,7 @@ ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) {
throw runtime_error("tekker deltas can only be applied to weapons");
}
static const array<int8_t, 11> delta_table = {
-10, -5, -3, -2, -1, 0, 1, 2, 3, 5, 10};
static const array<int8_t, 11> delta_table = {-10, -5, -3, -2, -1, 0, 1, 2, 3, 5, 10};
bool favored = item.data1[1] == favored_weapon_by_section_id[section_id];
ssize_t luck = 0;
+7 -43
View File
@@ -5,51 +5,12 @@
#include "CommonItemSet.hh"
#include "ItemParameterTable.hh"
#include "PSOEncryption.hh"
#include "PlayerSubordinates.hh"
#include "RareItemSet.hh"
#include "StaticGameData.hh"
struct ItemDropSub {
uint8_t override_area;
};
class ItemCreator {
public:
struct Restrictions {
// Note: In the original code, this is actually the battle rules structure.
// We omit some fields here because the item creator doesn't need them.
enum class TechDiskMode {
ON = 0,
OFF = 1,
LIMIT_LEVEL = 2,
};
enum class WeaponAndArmorMode {
// Note: These names match the value names in TPlyPKEditor
ALL_ON = 0,
ONLY_PICKING = 1,
ALL_OFF = 2,
NO_RARE = 3,
};
enum class ToolMode {
// Note: These names match the value names in TPlyPKEditor
ALL_ON = 0,
ONLY_PICKING = 1,
ALL_OFF = 2,
};
enum class MesetaDropMode {
// Note: These names match the value names in TPlyPKEditor
ON = 0,
OFF = 1,
ONLY_PICKING = 2,
};
TechDiskMode tech_disk_mode;
WeaponAndArmorMode weapon_and_armor_mode;
bool forbid_mags;
ToolMode tool_mode;
MesetaDropMode meseta_drop_mode;
bool forbid_scape_dolls;
uint8_t max_tech_disk_level; // 0xFF = no maximum
};
ItemCreator(
std::shared_ptr<const CommonItemSet> common_item_set,
std::shared_ptr<const RareItemSet> rare_item_set,
@@ -63,7 +24,7 @@ public:
uint8_t difficulty,
uint8_t section_id,
uint32_t random_seed,
std::shared_ptr<const Restrictions> restrictions = nullptr);
std::shared_ptr<const BattleRules> restrictions = nullptr);
~ItemCreator() = default;
ItemData on_monster_item_drop(uint32_t enemy_type, uint8_t area);
@@ -78,6 +39,10 @@ public:
// See the comments in TekkerAdjustmentSet for what this value means.
ssize_t apply_tekker_deltas(ItemData& item, uint8_t section_id);
inline void set_restrictions(std::shared_ptr<const BattleRules> restrictions) {
this->restrictions = restrictions;
}
private:
PrefixedLogger log;
Episode episode;
@@ -92,9 +57,8 @@ private:
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set;
std::shared_ptr<const ItemParameterTable> item_parameter_table;
const CommonItemSet::Table<true>* pt;
std::shared_ptr<const Restrictions> restrictions;
std::shared_ptr<const BattleRules> restrictions;
std::shared_ptr<ItemDropSub> item_drop_sub;
parray<uint8_t, 0x88> unit_weights_table1;
parray<int8_t, 0x0D> unit_weights_table2;
+83 -9
View File
@@ -1,5 +1,7 @@
#include "ItemData.hh"
#include <map>
#include "StaticGameData.hh"
using namespace std;
@@ -79,12 +81,6 @@ void ItemData::clear() {
this->data2d = 0;
}
void ItemData::bswap_data2_if_mag() {
if (this->data1[0] == 0x02) {
this->data2d = bswap32(this->data2d);
}
}
bool ItemData::empty() const {
return (this->data1d[0] == 0) &&
(this->data1d[1] == 0) &&
@@ -123,6 +119,27 @@ bool ItemData::is_wrapped() const {
}
}
void ItemData::wrap() {
switch (this->data1[0]) {
case 0:
case 1:
this->data1[4] |= 0x40;
break;
case 2:
this->data2[2] |= 0x40;
break;
case 3:
if (!this->is_stackable()) {
this->data1[3] |= 0x40;
}
break;
case 4:
break;
default:
throw runtime_error("invalid item data");
}
}
void ItemData::unwrap() {
switch (this->data1[0]) {
case 0:
@@ -300,6 +317,61 @@ void ItemData::add_mag_photon_blast(uint8_t pb_num) {
}
}
void ItemData::decode_if_mag(GameVersion from_version) {
if (this->data1[0] != 2) {
return;
}
if (from_version == GameVersion::GC) {
// PSO GC erroneously byteswaps the data2d field, even though it's actually
// just four individual bytes, so we correct for that here.
this->data2d = bswap32(this->data2d);
} else if (from_version == GameVersion::DC || from_version == GameVersion::PC) {
// PSO PC encodes mags in a tediously annoying manner. The first four bytes are the same, but then...
// V2: pHHHHHHHHHHHHHHc pIIIIIIIIIIIIIIc JJJJJJJJJJJJJJJc KKKKKKKKKKKKKKKc QQQQQQQQ QQQQQQQQ YYYYYYYY pYYYYYYY
// V3: HHHHHHHHHHHHHHHH IIIIIIIIIIIIIIII JJJJJJJJJJJJJJJJ KKKKKKKKKKKKKKKK YYYYYYYY QQQQQQQQ PPPPPPPP CCCCCCCC
// c = color in V2 (4 bits; low bit first)
// C = color in V3
// p = PB flag bits in V2 (3 bits; ordered 1, 2, 0)
// P = PB flag bits in V3
// H, I, J, K = DEF, POW, DEX, MIND
// Q = IQ (little-endian in V2)
// Y = synchro (little-endian in V2)
// Order is important; data2[0] must not be written before data2w[0] is read
this->data2[1] = this->data2w[0]; // IQ
this->data2[0] = this->data2w[1] & 0x7FFF; // Synchro
this->data2[2] = ((this->data2[3] >> 7) & 1) | ((this->data1w[2] >> 14) & 2) | ((this->data1w[3] >> 13) & 4); // PB flags
this->data2[3] = (this->data1w[2] & 1) | ((this->data1w[3] & 1) << 1) | ((this->data1w[4] & 1) << 2) | ((this->data1w[5] & 1) << 3); // Color
this->data1w[2] &= 0x7FFE;
this->data1w[3] &= 0x7FFE;
this->data1w[4] &= 0xFFFE;
this->data1w[5] &= 0xFFFE;
}
}
void ItemData::encode_if_mag(GameVersion to_version) {
if (this->data1[0] != 2) {
return;
}
// This function is the inverse of decode_v2_mag; see that function for a
// description of what's going on here.
if (to_version == GameVersion::GC) {
this->data2d = bswap32(this->data2d);
} else if (to_version == GameVersion::DC || to_version == GameVersion::PC) {
this->data1w[2] = (this->data1w[2] & 0x7FFE) | ((this->data2[2] << 14) & 0x8000) | (this->data2[3] & 1);
this->data1w[3] = (this->data1w[3] & 0x7FFE) | ((this->data2[2] << 13) & 0x8000) | ((this->data2[3] >> 1) & 1);
this->data1w[4] = (this->data1w[4] & 0xFFFE) | ((this->data2[3] >> 2) & 1);
this->data1w[5] = (this->data1w[5] & 0xFFFE) | ((this->data2[3] >> 3) & 1);
// Order is important; data2w[0] must not be written before data2[0] is read
this->data2w[1] = this->data2[0] | ((this->data2[2] << 15) & 0x8000);
this->data2w[0] = this->data2[1];
}
}
uint16_t ItemData::get_sealed_item_kill_count() const {
return ((this->data1[10] << 8) | this->data1[11]) & 0x7FFF;
}
@@ -1803,7 +1875,7 @@ string ItemData::name(bool include_color_codes) const {
}
} else { // Not S-rank (extended name bits not set)
uint8_t percentages[5] = {0, 0, 0, 0, 0};
parray<uint8_t, 5> percentages(0);
for (size_t x = 0; x < 3; x++) {
uint8_t which = this->data1[6 + 2 * x];
uint8_t value = this->data1[7 + 2 * x];
@@ -1816,8 +1888,10 @@ string ItemData::name(bool include_color_codes) const {
percentages[which - 1] = value;
}
}
ret_tokens.emplace_back(string_printf("%hhu/%hhu/%hhu/%hhu/%hhu",
percentages[0], percentages[1], percentages[2], percentages[3], percentages[4]));
if (!percentages.is_filled_with(0)) {
ret_tokens.emplace_back(string_printf("%hhu/%hhu/%hhu/%hhu/%hhu",
percentages[0], percentages[1], percentages[2], percentages[3], percentages[4]));
}
}
// For armors, add the slots, unit modifiers, and/or DEF/EVP bonuses
+8 -2
View File
@@ -4,6 +4,7 @@
#include <string>
#include "Text.hh"
#include "Version.hh"
constexpr uint32_t MESETA_IDENTIFIER = 0x00040000;
@@ -82,6 +83,10 @@ struct ItemData { // 0x14 bytes
// makes it incompatible with little-endian versions of PSO (i.e. all other
// versions). We manually byteswap data2 upon receipt and immediately before
// sending where needed.
// Related note: PSO V2 has an annoyingly complicated format for mags that
// doesn't match the above table. We decode this upon receipt and encode it
// imemdiately before sending when interacting with V2 clients; see the
// implementation of decode_if_mag() for details.
union {
parray<uint8_t, 12> data1;
@@ -107,13 +112,12 @@ struct ItemData { // 0x14 bytes
void clear();
void bswap_data2_if_mag();
std::string hex() const;
std::string name(bool include_color_codes) const;
uint32_t primary_identifier() const;
bool is_wrapped() const;
void wrap();
void unwrap();
bool is_stackable() const;
@@ -130,6 +134,8 @@ struct ItemData { // 0x14 bytes
uint8_t mag_photon_blast_for_slot(uint8_t slot) const;
bool mag_has_photon_blast_in_any_slot(uint8_t pb_num) const;
void add_mag_photon_blast(uint8_t pb_num);
void decode_if_mag(GameVersion version);
void encode_if_mag(GameVersion version);
uint16_t get_sealed_item_kill_count() const;
void set_sealed_item_kill_count(uint16_t v);
+27 -16
View File
@@ -8,7 +8,9 @@
using namespace std;
void player_use_item(shared_ptr<ServerState> s, shared_ptr<Client> c, size_t item_index) {
void player_use_item(shared_ptr<Client> c, size_t item_index) {
auto s = c->require_server_state();
// On PC (and presumably DC), the client sends a 6x29 after this to delete the
// used item. On GC and later versions, this does not happen, so we should
// delete the item here.
@@ -26,45 +28,51 @@ void player_use_item(shared_ptr<ServerState> s, shared_ptr<Client> c, size_t ite
if (item.data.data1[2] > max_level) {
throw runtime_error("technique level too high");
}
player->disp.technique_levels.data()[item.data.data1[4]] = item.data.data1[2];
player->set_technique_level(item.data.data1[4], item.data.data1[2]);
} else if ((item_identifier & 0xFFFF00) == 0x030A00) { // Grinder
if (item.data.data1[2] > 2) {
throw invalid_argument("incorrect grinder value");
throw runtime_error("incorrect grinder value");
}
auto& weapon = player->inventory.items[player->inventory.find_equipped_weapon()];
auto weapon_def = s->item_parameter_table->get_weapon(
weapon.data.data1[1], weapon.data.data1[2]);
auto weapon_def = s->item_parameter_table->get_weapon(weapon.data.data1[1], weapon.data.data1[2]);
if (weapon.data.data1[3] >= weapon_def.max_grind) {
throw runtime_error("weapon already at maximum grind");
}
weapon.data.data1[3] += (item.data.data1[2] + 1);
} else if ((item_identifier & 0xFFFF00) == 0x030B00) { // Material
auto p = c->game_data.player();
using Type = SavedPlayerDataBB::MaterialType;
switch (item.data.data1[2]) {
case 0: // Power Material
c->game_data.player()->disp.stats.char_stats.atp += 2;
p->set_material_usage(Type::POWER, p->get_material_usage(Type::POWER) + 1);
p->disp.stats.char_stats.atp += 2;
break;
case 1: // Mind Material
c->game_data.player()->disp.stats.char_stats.mst += 2;
p->set_material_usage(Type::MIND, p->get_material_usage(Type::MIND) + 1);
p->disp.stats.char_stats.mst += 2;
break;
case 2: // Evade Material
c->game_data.player()->disp.stats.char_stats.evp += 2;
p->set_material_usage(Type::EVADE, p->get_material_usage(Type::EVADE) + 1);
p->disp.stats.char_stats.evp += 2;
break;
case 3: // HP Material
c->game_data.player()->inventory.hp_materials_used += 2;
p->set_material_usage(Type::HP, p->get_material_usage(Type::HP) + 1);
break;
case 4: // TP Material
c->game_data.player()->inventory.tp_materials_used += 2;
p->set_material_usage(Type::TP, p->get_material_usage(Type::TP) + 1);
break;
case 5: // Def Material
c->game_data.player()->disp.stats.char_stats.dfp += 2;
p->set_material_usage(Type::DEF, p->get_material_usage(Type::DEF) + 1);
p->disp.stats.char_stats.dfp += 2;
break;
case 6: // Luck Material
c->game_data.player()->disp.stats.char_stats.lck += 2;
p->set_material_usage(Type::LUCK, p->get_material_usage(Type::LUCK) + 1);
p->disp.stats.char_stats.lck += 2;
break;
default:
throw invalid_argument("unknown material used");
throw runtime_error("unknown material used");
}
} else if ((item_identifier & 0xFFFF00) == 0x030F00) { // AddSlot
@@ -152,8 +160,10 @@ void player_use_item(shared_ptr<ServerState> s, shared_ptr<Client> c, size_t ite
item.data.data1.clear_after(3);
should_delete_item = false;
auto l = s->find_lobby(c->lobby_id);
send_create_inventory_item(l, c, item.data);
auto l = c->lobby.lock();
if (l) {
send_create_inventory_item(c, item.data);
}
break;
}
}
@@ -218,7 +228,7 @@ void player_use_item(shared_ptr<ServerState> s, shared_ptr<Client> c, size_t ite
}
}
void player_feed_mag(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index) {
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index) {
static const unordered_map<uint32_t, size_t> result_index_for_fed_item({
{0x030000, 0}, // Monomate
{0x030001, 1}, // Dimate
@@ -233,6 +243,7 @@ void player_feed_mag(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c,
{0x030500, 10}, // Star Atomizer
});
auto s = c->require_server_state();
auto player = c->game_data.player();
auto& fed_item = player->inventory.items[fed_item_index];
auto& mag_item = player->inventory.items[mag_item_index];
+2 -2
View File
@@ -9,5 +9,5 @@
#include "ServerState.hh"
#include "StaticGameData.hh"
void player_use_item(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c, size_t item_index);
void player_feed_mag(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index);
void player_use_item(std::shared_ptr<Client> c, size_t item_index);
void player_feed_mag(std::shared_ptr<Client> c, size_t mag_item_index, size_t fed_item_index);
+23 -1
View File
@@ -8,6 +8,28 @@
using namespace std;
void PlayerStats::reset_to_base(uint8_t char_class, shared_ptr<const LevelTable> level_table) {
this->level = 0;
this->char_stats = level_table->base_stats_for_class(char_class);
}
void PlayerStats::advance_to_level(uint8_t char_class, uint32_t level, shared_ptr<const LevelTable> level_table) {
for (; this->level < level; this->level++) {
const auto& level_stats = level_table->stats_delta_for_level(char_class, this->level + 1);
// The original code clamps the resulting stat values to [0, max_stat]; we
// don't have max_stat handy so we just allow them to be unbounded
this->char_stats.atp += level_stats.atp;
this->char_stats.mst += level_stats.mst;
this->char_stats.evp += level_stats.evp;
this->char_stats.hp += level_stats.hp;
this->char_stats.dfp += level_stats.dfp;
this->char_stats.ata += level_stats.ata;
// Note: It is not a bug that lck is ignored here; the original code
// ignores it too.
this->experience = level_stats.experience;
}
}
LevelTable::LevelTable(shared_ptr<const string> data, bool compressed) {
if (compressed) {
this->data.reset(new string(prs_decompress(*data)));
@@ -28,7 +50,7 @@ const CharacterStats& LevelTable::base_stats_for_class(uint8_t char_class) const
return this->table->base_stats[char_class];
}
const LevelTable::LevelStats& LevelTable::stats_for_level(
const LevelTable::LevelStats& LevelTable::stats_delta_for_level(
uint8_t char_class, uint8_t level) const {
if (char_class >= 12) {
throw invalid_argument("invalid character class");
+49 -1
View File
@@ -6,6 +6,8 @@
#include <phosg/Encoding.hh>
#include <string>
class LevelTable;
struct CharacterStats {
le_uint16_t atp = 0;
le_uint16_t mst = 0;
@@ -16,6 +18,20 @@ struct CharacterStats {
le_uint16_t lck = 0;
} __attribute__((packed));
struct PlayerStats {
/* 00 */ CharacterStats char_stats;
/* 0E */ le_uint16_t unknown_a1 = 0;
/* 10 */ le_float unknown_a2 = 0.0;
/* 14 */ le_float unknown_a3 = 0.0;
/* 18 */ le_uint32_t level = 0;
/* 1C */ le_uint32_t experience = 0;
/* 20 */ le_uint32_t meseta = 0;
/* 24 */
void reset_to_base(uint8_t char_class, std::shared_ptr<const LevelTable> level_table);
void advance_to_level(uint8_t char_class, uint32_t level, std::shared_ptr<const LevelTable> level_table);
} __attribute__((packed));
class LevelTable { // from PlyLevelTbl.prs
public:
struct LevelStats {
@@ -41,9 +57,41 @@ public:
LevelTable(std::shared_ptr<const std::string> data, bool compressed);
const CharacterStats& base_stats_for_class(uint8_t char_class) const;
const LevelStats& stats_for_level(uint8_t char_class, uint8_t level) const;
const LevelStats& stats_delta_for_level(uint8_t char_class, uint8_t level) const;
private:
// TODO: Currently we only support the BB version of this file. It'd be nice
// to support non-BB versions, but their formats are very different:
//
// BB:
// root:
// u32 offset:
// u32[12] unknown
// u32 offset:
// u32[12] offsets:
// LevelStats[200] level_stats
// u32 offset:
// CharacterStats[12] base_stats
// GC:
// root:
// u32 offset:
// u32[12] offsets:
// LevelStats[200] level_stats
// PC:
// root:
// u32 offset:
// u32 offset[9]:
// LevelStats[200] level_stats
// u32 offset:
// (0x18 bytes)
// u32 offset:
// PlayerStats[9] max_stats
// u32 offset:
// PlayerStats[9] level100_stats
// u32 offset:
// u32 offset[9]:
// CharacterStats level1_stats
// (11 more pointers)
std::shared_ptr<const std::string> data;
const Table* table;
};
+150 -160
View File
@@ -10,103 +10,166 @@
using namespace std;
License::License()
License::License(const JSON& json)
: serial_number(0),
privileges(0),
ban_end_time(0) {}
flags(0),
ban_end_time(0),
ep3_current_meseta(0),
ep3_total_meseta_earned(0) {
this->serial_number = json.get_int("SerialNumber");
this->access_key = json.get_string("AccessKey", "");
this->gc_password = json.get_string("GCPassword", "");
this->bb_username = json.get_string("BBUsername", "");
this->bb_password = json.get_string("BBPassword", "");
this->flags = json.get_int("Flags", 0);
this->ban_end_time = json.get_int("BanEndTime", 0);
this->ep3_current_meseta = json.get_int("Ep3CurrentMeseta", 0);
this->ep3_total_meseta_earned = json.get_int("Ep3TotalMesetaEarned", 0);
}
JSON License::json() const {
return JSON::dict({
{"SerialNumber", this->serial_number},
{"AccessKey", this->access_key},
{"GCPassword", this->gc_password},
{"BBUsername", this->bb_username},
{"BBPassword", this->bb_password},
{"Flags", this->flags},
{"BanEndTime", this->ban_end_time},
{"Ep3CurrentMeseta", this->ep3_current_meseta},
{"Ep3TotalMesetaEarned", this->ep3_total_meseta_earned},
});
}
void License::save() const {
auto json = this->json();
string json_data = json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS);
string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->serial_number);
save_file(filename, json_data);
}
void License::delete_file() const {
string filename = string_printf("system/licenses/%010" PRIu32 ".json", this->serial_number);
remove(filename.c_str());
}
string License::str() const {
string ret = string_printf("License(serial_number=%" PRIu32, this->serial_number);
if (!this->username.empty()) {
ret += ", username=";
ret += this->username;
}
if (!this->bb_password.empty()) {
ret += ", bb-password=";
ret += this->bb_password;
}
vector<string> tokens;
tokens.emplace_back(string_printf("serial_number=%010" PRIu32 "/%08" PRIX32, this->serial_number, this->serial_number));
if (!this->access_key.empty()) {
ret += ", access-key=";
ret += this->access_key;
tokens.emplace_back("access_key=" + this->access_key);
}
if (!this->gc_password.empty()) {
ret += ", gc-password=";
ret += this->gc_password;
tokens.emplace_back("gc_password=" + this->gc_password);
}
ret += string_printf(", privileges=%" PRIu32, this->privileges);
if (!this->bb_username.empty()) {
tokens.emplace_back("bb_username=" + this->bb_username);
}
if (!this->bb_password.empty()) {
tokens.emplace_back("bb_password=" + this->bb_password);
}
tokens.emplace_back(string_printf("flags=%08" PRIX32, this->flags));
if (this->ban_end_time) {
ret += string_printf(", banned-until=%" PRIu64, this->ban_end_time);
tokens.emplace_back(string_printf("ban_end_time=%016" PRIX64, this->ban_end_time));
}
return ret + ")";
if (this->ep3_current_meseta) {
tokens.emplace_back(string_printf("ep3_current_meseta=%" PRIu32, this->ep3_current_meseta));
}
if (this->ep3_total_meseta_earned) {
tokens.emplace_back(string_printf("ep3_total_meseta_earned=%" PRIu32, this->ep3_total_meseta_earned));
}
return "[License: " + join(tokens, ", ") + "]";
}
LicenseManager::LicenseManager()
: filename(""),
autosave(false) {}
struct BinaryLicense {
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.
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)
} __attribute__((packed));
LicenseManager::LicenseManager(const string& filename)
: filename(filename),
autosave(true) {
try {
auto licenses = load_vector_file<License>(this->filename);
for (const auto& read_license : licenses) {
shared_ptr<License> license(new License(read_license));
LicenseIndex::LicenseIndex() {
if (!isdir("system/licenses")) {
mkdir("system/licenses", 0755);
}
// 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);
// Convert binary licenses to JSON licenses and save them
if (isfile("system/licenses.nsi")) {
auto bin_licenses = load_vector_file<BinaryLicense>("system/licenses.nsi");
for (const auto& bin_license : bin_licenses) {
// Only add licenses from the binary file if there isn't a JSON version of
// the same license
try {
this->get(bin_license.serial_number);
} catch (const missing_license&) {
License license;
license.serial_number = bin_license.serial_number;
license.access_key = bin_license.access_key;
license.gc_password = bin_license.gc_password;
license.bb_username = bin_license.username;
license.bb_password = bin_license.bb_password;
license.flags = bin_license.privileges;
license.ban_end_time = bin_license.ban_end_time;
license.ep3_current_meseta = 0;
license.ep3_total_meseta_earned = 0;
license.save();
}
}
::remove("system/licenses.nsi");
}
} catch (const cannot_open_file&) {
license_log.warning("File %s does not exist; no licenses are registered",
this->filename.c_str());
for (const auto& item : list_directory("system/licenses")) {
if (ends_with(item, ".json")) {
JSON json = JSON::parse(load_file("system/licenses/" + item));
shared_ptr<License> license(new License(json));
this->add(license);
}
}
}
void LicenseManager::save() const {
if (this->filename.empty()) {
throw logic_error("license manager has no filename; cannot save");
size_t LicenseIndex::count() const {
return this->serial_number_to_license.size();
}
shared_ptr<License> LicenseIndex::get(uint32_t serial_number) const {
try {
return this->serial_number_to_license.at(serial_number);
} catch (const out_of_range&) {
throw missing_license();
}
auto f = fopen_unique(this->filename, "wb");
}
vector<shared_ptr<License>> LicenseIndex::all() const {
vector<shared_ptr<License>> ret;
ret.reserve(this->serial_number_to_license.size());
for (const auto& it : this->serial_number_to_license) {
if (it.second->privileges & Privilege::TEMPORARY) {
continue;
}
fwritex(f.get(), it.second.get(), sizeof(License));
ret.emplace_back(it.second);
}
return ret;
}
void LicenseIndex::add(shared_ptr<License> l) {
this->serial_number_to_license[l->serial_number] = l;
if (!l->bb_username.empty()) {
this->bb_username_to_license[l->bb_username] = l;
}
}
void LicenseManager::set_autosave(bool autosave) {
this->autosave = autosave;
}
shared_ptr<const License> LicenseManager::verify_pc(uint32_t serial_number,
const string& access_key) const {
try {
auto& license = this->serial_number_to_license.at(serial_number);
if (!license->access_key.eq_n(access_key, 8)) {
throw incorrect_access_key();
}
if (license->ban_end_time && (license->ban_end_time >= now())) {
throw invalid_argument("user is banned");
}
return license;
} catch (const out_of_range&) {
throw missing_license();
void LicenseIndex::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->bb_username.empty()) {
this->bb_username_to_license.erase(l->bb_username);
}
}
shared_ptr<const License> LicenseManager::verify_gc(uint32_t serial_number,
const string& access_key) const {
shared_ptr<License> LicenseIndex::verify_v1_v2(uint32_t serial_number, const string& access_key) const {
try {
auto& license = this->serial_number_to_license.at(serial_number);
if (!license->access_key.eq_n(access_key, 12)) {
if (license->access_key.compare(0, 8, access_key) != 0) {
throw incorrect_access_key();
}
if (license->ban_end_time && (license->ban_end_time >= now())) {
@@ -118,11 +181,25 @@ shared_ptr<const License> LicenseManager::verify_gc(uint32_t serial_number,
}
}
shared_ptr<const License> LicenseManager::verify_gc(uint32_t serial_number,
const string& access_key, const string& password) const {
shared_ptr<License> LicenseIndex::verify_gc(uint32_t serial_number, const string& access_key) const {
try {
auto& license = this->serial_number_to_license.at(serial_number);
if (!license->access_key.eq_n(access_key, 12)) {
if (license->access_key != access_key) {
throw incorrect_access_key();
}
if (license->ban_end_time && (license->ban_end_time >= now())) {
throw invalid_argument("user is banned");
}
return license;
} catch (const out_of_range&) {
throw missing_license();
}
}
shared_ptr<License> LicenseIndex::verify_gc(uint32_t serial_number, const string& access_key, const string& password) const {
try {
auto& license = this->serial_number_to_license.at(serial_number);
if (license->access_key != access_key) {
throw incorrect_access_key();
}
if (license->gc_password != password) {
@@ -137,14 +214,12 @@ shared_ptr<const License> LicenseManager::verify_gc(uint32_t serial_number,
}
}
shared_ptr<const License> LicenseManager::verify_bb(const string& username,
const string& password) const {
shared_ptr<License> LicenseIndex::verify_bb(const string& username, const string& password) const {
try {
auto& license = this->bb_username_to_license.at(username);
if (license->bb_password != password) {
throw incorrect_password();
}
if (license->ban_end_time && (license->ban_end_time >= now())) {
throw invalid_argument("user is banned");
}
@@ -153,88 +228,3 @@ shared_ptr<const License> LicenseManager::verify_bb(const string& username,
throw missing_license();
}
}
size_t LicenseManager::count() const {
return this->serial_number_to_license.size();
}
void LicenseManager::ban_until(uint32_t serial_number, uint64_t end_time) {
this->serial_number_to_license.at(serial_number)->ban_end_time = end_time;
if (this->autosave) {
this->save();
}
}
shared_ptr<const License> LicenseManager::get(uint32_t serial_number) const {
try {
return this->serial_number_to_license.at(serial_number);
} catch (const out_of_range&) {
throw missing_license();
}
}
void LicenseManager::add(shared_ptr<License> l) {
this->serial_number_to_license[l->serial_number] = l;
if (!l->username.empty()) {
this->bb_username_to_license[l->username] = l;
}
if (this->autosave) {
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.empty()) {
this->bb_username_to_license.erase(l->username);
}
if (this->autosave) {
this->save();
}
}
vector<License> LicenseManager::snapshot() const {
vector<License> ret;
for (auto it : this->serial_number_to_license) {
ret.emplace_back(*it.second);
}
return ret;
}
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;
l->access_key = access_key;
if (temporary) {
l->privileges |= Privilege::TEMPORARY;
}
return l;
}
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;
l->access_key = access_key;
l->gc_password = password;
if (temporary) {
l->privileges |= Privilege::TEMPORARY;
}
return l;
}
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;
l->username = username;
l->bb_password = password;
if (temporary) {
l->privileges |= Privilege::TEMPORARY;
}
return l;
}
+60 -75
View File
@@ -1,105 +1,90 @@
#pragma once
#include <memory>
#include <phosg/JSON.hh>
#include <string>
#include <unordered_map>
#include <vector>
#include "Text.hh"
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,
DEBUG = 0x01000000,
MODERATOR = 0x00000007,
ADMINISTRATOR = 0x0000003F,
ROOT = 0x7FFFFFFF,
TEMPORARY = 0x80000000,
};
enum LicenseVerifyAction {
BB = 0x00,
GC = 0x01,
PC = 0x02,
SERIAL_NUMBER = 0x03,
};
class LicenseIndex;
struct License {
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.
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)
enum Flag : uint32_t {
// clang-format off
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,
DEBUG = 0x01000000,
MODERATOR = 0x00000007,
ADMINISTRATOR = 0x000000FF,
ROOT = 0x010000FF,
License();
std::string str() const;
} __attribute__((packed));
UNUSED_BITS = 0xFEFFFF00,
// clang-format on
};
class incorrect_password : public std::invalid_argument {
public:
incorrect_password() : invalid_argument("incorrect password") {}
};
uint32_t serial_number = 0;
std::string access_key;
std::string gc_password;
std::string bb_username;
std::string bb_password;
class incorrect_access_key : public std::invalid_argument {
public:
incorrect_access_key() : invalid_argument("incorrect access key") {}
};
uint32_t flags = 0;
uint64_t ban_end_time = 0; // 0 = not banned
class missing_license : public std::invalid_argument {
public:
missing_license() : invalid_argument("missing license") {}
};
uint32_t ep3_current_meseta = 0;
uint32_t ep3_total_meseta_earned = 0;
class LicenseManager {
public:
LicenseManager();
explicit LicenseManager(const std::string& filename);
~LicenseManager() = default;
License() = default;
explicit License(const JSON& json);
JSON json() const;
void save() const;
void set_autosave(bool autosave);
void delete_file() const;
std::shared_ptr<const License> verify_pc(uint32_t serial_number,
const std::string& access_key) const;
std::shared_ptr<const License> verify_gc(uint32_t serial_number,
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);
std::string str() const;
};
class LicenseIndex {
public:
class incorrect_password : public std::invalid_argument {
public:
incorrect_password() : invalid_argument("incorrect password") {}
};
class incorrect_access_key : public std::invalid_argument {
public:
incorrect_access_key() : invalid_argument("incorrect access key") {}
};
class missing_license : public std::invalid_argument {
public:
missing_license() : invalid_argument("missing license") {}
};
LicenseIndex();
~LicenseIndex() = default;
size_t count() const;
std::shared_ptr<License> get(uint32_t serial_number) const;
std::vector<std::shared_ptr<License>> all() const;
std::shared_ptr<const License> get(uint32_t serial_number) const;
void add(std::shared_ptr<License> l);
void remove(uint32_t serial_number);
std::vector<License> snapshot() const;
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);
std::shared_ptr<License> verify_v1_v2(uint32_t serial_number, const std::string& access_key) const;
std::shared_ptr<License> verify_gc(uint32_t serial_number, const std::string& access_key) const;
std::shared_ptr<License> verify_gc(uint32_t serial_number, const std::string& access_key, const std::string& password) const;
std::shared_ptr<License> verify_bb(const std::string& username, const std::string& password) const;
protected:
std::string filename;
bool autosave;
std::unordered_map<std::string, std::shared_ptr<License>> bb_username_to_license;
std::unordered_map<uint32_t, std::shared_ptr<License>> serial_number_to_license;
};
+87 -25
View File
@@ -10,13 +10,15 @@
using namespace std;
Lobby::Lobby(uint32_t id)
: log(string_printf("[Lobby/%" PRIX32 "] ", id), lobby_log.min_level),
Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id)
: server_state(s),
log(string_printf("[Lobby/%" PRIX32 "] ", id), lobby_log.min_level),
lobby_id(id),
min_level(0),
max_level(0xFFFFFFFF),
next_game_item_id(0x00810000),
version(GameVersion::GC),
base_version(GameVersion::GC),
allowed_versions(0xFFFF),
section_id(0),
episode(Episode::NONE),
mode(GameMode::NORMAL),
@@ -25,7 +27,6 @@ Lobby::Lobby(uint32_t id)
random_seed(random_object<uint32_t>()),
event(0),
block(0),
type(0),
leader_id(0),
max_clients(12),
flags(0) {
@@ -34,6 +35,62 @@ Lobby::Lobby(uint32_t id)
}
}
shared_ptr<ServerState> Lobby::require_server_state() const {
auto s = this->server_state.lock();
if (!s) {
throw logic_error("server is deleted");
}
return s;
}
void Lobby::create_item_creator() {
auto s = this->require_server_state();
shared_ptr<const RareItemSet> rare_item_set;
if (this->base_version == GameVersion::BB) {
rare_item_set = s->rare_item_sets.at("default-v4");
} else if (this->base_version == GameVersion::GC || this->base_version == GameVersion::XB) {
rare_item_set = s->rare_item_sets.at("default-v3");
} else {
// TODO: Should there be a separate table for V1 eventually?
rare_item_set = s->rare_item_sets.at("default-v2");
}
this->item_creator.reset(new ItemCreator(
s->common_item_set,
rare_item_set,
s->armor_random_set,
s->tool_random_set,
s->weapon_random_sets.at(this->difficulty),
s->tekker_adjustment_set,
s->item_parameter_table,
this->episode,
(this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode,
this->difficulty,
this->section_id,
this->random_seed));
}
void Lobby::create_ep3_server() {
auto s = this->require_server_state();
if (!this->ep3_server) {
this->log.info("Creating Episode 3 server state");
} else {
this->log.info("Recreating Episode 3 server state");
}
auto tourn = this->tournament_match ? this->tournament_match->tournament.lock() : nullptr;
bool is_trial = (this->flags & Lobby::Flag::IS_EP3_TRIAL);
Episode3::Server::Options options = {
.card_index = is_trial ? s->ep3_card_index_trial : s->ep3_card_index,
.map_index = s->ep3_map_index,
.behavior_flags = s->ep3_behavior_flags,
.random_crypt = this->random_crypt,
.tournament = tourn,
.trap_card_ids = s->ep3_trap_card_ids,
};
this->ep3_server = make_shared<Episode3::Server>(this->shared_from_this(), std::move(options));
this->ep3_server->init();
}
void Lobby::reassign_leader_on_client_departure(size_t leaving_client_index) {
for (size_t x = 0; x < this->max_clients; x++) {
if (x == leaving_client_index) {
@@ -103,7 +160,7 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
}
c->lobby_client_id = index;
c->lobby_id = this->lobby_id;
c->lobby = this->weak_from_this();
// If there's no one else in the lobby, set the leader id as well
size_t leader_index;
@@ -119,24 +176,27 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_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;
auto p = c->game_data.player();
auto& inv = p->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);
p->print_inventory(stderr);
}
// If the lobby is recording a battle record, add the player join event
if (this->battle_record) {
auto p = c->game_data.player();
PlayerLobbyDataDCGC lobby_data;
lobby_data.player_tag = 0x00010000;
lobby_data.guild_card = c->license->serial_number;
lobby_data.name = encode_sjis(c->game_data.player()->disp.name);
lobby_data.name = encode_sjis(p->disp.name);
this->battle_record->add_player(
lobby_data,
c->game_data.player()->inventory,
c->game_data.player()->disp.to_dcpcv3());
p->inventory,
p->disp.to_dcpcv3(),
c->game_data.ep3_config ? (c->game_data.ep3_config->online_clv_exp / 100) : 0);
}
// Send spectator count notifications if needed
@@ -144,10 +204,10 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
if (this->flags & Lobby::Flag::IS_SPECTATOR_TEAM) {
auto watched_l = this->watched_lobby.lock();
if (watched_l) {
send_ep3_update_spectator_count(watched_l);
send_ep3_update_game_metadata(watched_l);
}
} else {
send_ep3_update_spectator_count(this->shared_from_this());
send_ep3_update_game_metadata(this->shared_from_this());
}
}
}
@@ -160,14 +220,16 @@ void Lobby::remove_client(shared_ptr<Client> c) {
c->lobby_client_id,
static_cast<uint8_t>(other_c ? other_c->lobby_client_id : 0xFF)));
}
this->clients[c->lobby_client_id] = nullptr;
// 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) {
c->lobby_id = 0;
// Unassign the client's lobby if it matches the current lobby (it may not
// match if the client was already added to another lobby - this can happen
// during the lobby change procedure)
{
auto c_lobby = c->lobby.lock();
if (c_lobby.get() == this) {
c->lobby.reset();
}
}
this->reassign_leader_on_client_departure(c->lobby_client_id);
@@ -182,10 +244,10 @@ void Lobby::remove_client(shared_ptr<Client> c) {
if (this->flags & Lobby::Flag::IS_SPECTATOR_TEAM) {
auto watched_l = this->watched_lobby.lock();
if (watched_l) {
send_ep3_update_spectator_count(watched_l);
send_ep3_update_game_metadata(watched_l);
}
} else {
send_ep3_update_spectator_count(this->shared_from_this());
send_ep3_update_game_metadata(this->shared_from_this());
}
}
}
@@ -245,20 +307,20 @@ uint8_t Lobby::game_event_for_lobby_event(uint8_t lobby_event) {
return lobby_event;
}
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;
void Lobby::add_item(const ItemData& data, uint8_t area, float x, float z) {
auto& fi = this->item_id_to_floor_item[data.id];
fi.data = data;
fi.area = area;
fi.x = x;
fi.z = z;
}
PlayerInventoryItem Lobby::remove_item(uint32_t item_id) {
ItemData 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");
}
PlayerInventoryItem ret = std::move(item_it->second.inv_item);
ItemData ret = item_it->second.data;
this->item_id_to_floor_item.erase(item_it);
return ret;
}
+35 -12
View File
@@ -11,21 +11,22 @@
#include <vector>
#include "Client.hh"
#include "CommandFormats.hh"
#include "Episode3/BattleRecord.hh"
#include "Episode3/Server.hh"
#include "ItemCreator.hh"
#include "Map.hh"
#include "Player.hh"
#include "Quest.hh"
#include "RareItemSet.hh"
#include "StaticGameData.hh"
#include "Text.hh"
struct ServerState;
struct Lobby : public std::enable_shared_from_this<Lobby> {
enum Flag {
GAME = 0x00000001,
NON_V1_ONLY = 0x00000002, // DC NTE and DCv1 not allowed
PERSISTENT = 0x00000004,
PERSISTENT = 0x00000002,
// Flags used only for games
CHEATS_ENABLED = 0x00000100,
@@ -38,12 +39,16 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
START_BATTLE_PLAYER_IMMEDIATELY = 0x00008000,
DROPS_ENABLED = 0x00010000, // Does not affect BB
IS_EP3_TRIAL = 0x00020000,
USE_SERVER_RARE_TABLE = 0x00040000, // Does not affect BB
// Flags used only for lobbies
PUBLIC = 0x01000000,
DEFAULT = 0x02000000,
V2_AND_LATER = 0x04000000, // Lobby does not appear on v1
IS_OVERFLOW = 0x08000000,
};
std::weak_ptr<ServerState> server_state;
PrefixedLogger log;
uint32_t lobby_id;
@@ -53,7 +58,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
// Item info
struct FloorItem {
PlayerInventoryItem inv_item;
ItemData data;
float x;
float z;
uint8_t area;
@@ -65,7 +70,11 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
parray<le_uint32_t, 0x20> variations;
// Game config
GameVersion version;
GameVersion base_version;
// Bits in allowed_versions specify who is allowed to join this game. The
// bits are indexed as (1 << version), where version is a value from the
// QuestScriptVersion enum.
uint16_t allowed_versions;
uint8_t section_id;
Episode episode;
GameMode mode;
@@ -89,18 +98,17 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
// Types 2 and 3 may be distinguished by the presence of the battle_record
// field - in replay games, it will be present; in watcher games it will be
// absent.
std::shared_ptr<Episode3::ServerBase> ep3_server_base; // Only used in primary games
std::shared_ptr<Episode3::Server> ep3_server; // Only used in primary games
std::weak_ptr<Lobby> watched_lobby; // Only used in watcher games
std::unordered_set<shared_ptr<Lobby>> watcher_lobbies; // Only used in primary games
std::unordered_set<std::shared_ptr<Lobby>> watcher_lobbies; // Only used in primary games
std::shared_ptr<Episode3::BattleRecord> battle_record; // Not used in watcher games
std::shared_ptr<Episode3::BattleRecord> prev_battle_record; // Only used in primary games
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player; // Only used in replay games
std::shared_ptr<Episode3::Tournament::Match> tournament_match;
std::shared_ptr<const G_SetEXResultValues_GC_Ep3_6xB4x4B> ep3_ex_result_values;
// Lobby stuff
uint8_t event;
uint8_t block;
uint8_t type; // number to give to PSO for the lobby number
uint8_t leader_id;
uint8_t max_clients;
uint32_t flags;
@@ -109,7 +117,15 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
// Keys in this map are client_id
std::unordered_map<size_t, std::weak_ptr<Client>> clients_to_add;
explicit Lobby(uint32_t id);
Lobby(std::shared_ptr<ServerState> s, uint32_t id);
Lobby(const Lobby&) = delete;
Lobby(Lobby&&) = delete;
Lobby& operator=(const Lobby&) = delete;
Lobby& operator=(Lobby&&) = delete;
std::shared_ptr<ServerState> require_server_state() const;
void create_item_creator();
void create_ep3_server();
inline bool is_game() const {
return this->flags & Flag::GAME;
@@ -118,6 +134,13 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
return this->episode == Episode::EP3;
}
inline bool version_is_allowed(QuestScriptVersion v) const {
return this->allowed_versions & (1 << static_cast<size_t>(v));
}
inline void allow_version(QuestScriptVersion v) {
this->allowed_versions |= (1 << static_cast<size_t>(v));
}
void reassign_leader_on_client_departure(size_t leaving_client_id);
size_t count_clients() const;
bool any_client_loading() const;
@@ -134,8 +157,8 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
const std::u16string* identifier = nullptr,
uint64_t serial_number = 0);
void add_item(const PlayerInventoryItem& item, uint8_t area, float x, float z);
PlayerInventoryItem remove_item(uint32_t item_id);
void add_item(const ItemData& item, uint8_t area, float x, float z);
ItemData remove_item(uint32_t item_id);
size_t find_item(uint32_t item_id);
uint32_t generate_item_id(uint8_t client_id);
-2
View File
@@ -12,7 +12,6 @@ PrefixedLogger config_log("[Config] ", LogLevel::USE_DEFAULT);
PrefixedLogger dns_server_log("[DNSServer] ", LogLevel::USE_DEFAULT);
PrefixedLogger function_compiler_log("[FunctionCompiler] ", LogLevel::USE_DEFAULT);
PrefixedLogger ip_stack_simulator_log("[IPStackSimulator] ", LogLevel::USE_DEFAULT);
PrefixedLogger license_log("[LicenseManager] ", LogLevel::USE_DEFAULT);
PrefixedLogger lobby_log("", LogLevel::USE_DEFAULT);
PrefixedLogger patch_index_log("[PatchFileIndex] ", LogLevel::USE_DEFAULT);
PrefixedLogger player_data_log("", LogLevel::USE_DEFAULT);
@@ -39,7 +38,6 @@ void set_log_levels_from_json(const JSON& json) {
set_log_level_from_json(dns_server_log, json, "DNSServer");
set_log_level_from_json(function_compiler_log, json, "FunctionCompiler");
set_log_level_from_json(ip_stack_simulator_log, json, "IPStackSimulator");
set_log_level_from_json(license_log, json, "LicenseManager");
set_log_level_from_json(lobby_log, json, "Lobbies");
set_log_level_from_json(patch_index_log, json, "PatchFileIndex");
set_log_level_from_json(player_data_log, json, "PlayerData");
-1
View File
@@ -11,7 +11,6 @@ extern PrefixedLogger config_log;
extern PrefixedLogger dns_server_log;
extern PrefixedLogger function_compiler_log;
extern PrefixedLogger ip_stack_simulator_log;
extern PrefixedLogger license_log;
extern PrefixedLogger lobby_log;
extern PrefixedLogger patch_index_log;
extern PrefixedLogger player_data_log;
+561 -130
View File
File diff suppressed because it is too large Load Diff
+11 -4
View File
@@ -480,6 +480,13 @@ void Map::add_enemies_from_map_data(
}
}
struct DATSectionHeader {
le_uint32_t type; // 1 = objects, 2 = enemies. There are other types too
le_uint32_t section_size; // Includes this header
le_uint32_t area;
le_uint32_t data_size;
} __attribute__((packed));
void Map::add_enemies_from_quest_data(
Episode episode,
uint8_t difficulty,
@@ -488,7 +495,7 @@ void Map::add_enemies_from_quest_data(
size_t size) {
StringReader r(data, size);
while (!r.eof()) {
const auto& header = r.get<Quest::DATSectionHeader>();
const auto& header = r.get<DATSectionHeader>();
if (header.type == 0 && header.section_size == 0) {
break;
}
@@ -780,11 +787,11 @@ vector<string> map_filenames_for_variation(
vector<string> ret;
if (is_solo) {
// Try both _offe.dat and e_s.dat suffixes
// Try both _offe.dat and e_s.dat suffixes first before falling back to
// non-solo version
ret.emplace_back(filename + "_offe.dat");
ret.emplace_back(filename + "e_s.dat");
} else {
ret.emplace_back(filename + "e.dat");
}
ret.emplace_back(filename + "e.dat");
return ret;
}
+23 -15
View File
@@ -20,6 +20,7 @@ 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 REDIRECT_DESTINATIONS = 0x78000078;
constexpr uint32_t PROGRAMS = 0x88000088;
constexpr uint32_t PATCHES = 0x99000099;
constexpr uint32_t PROXY_OPTIONS = 0xAA0000AA;
@@ -33,6 +34,7 @@ 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 REDIRECT_DESTINATIONS = 0x11565611;
constexpr uint32_t PATCHES = 0x11666611;
constexpr uint32_t PROGRAMS = 0x11777711;
constexpr uint32_t DISCONNECT = 0x11888811;
@@ -48,6 +50,11 @@ constexpr uint32_t GO_BACK = 0x77FFFF77;
constexpr uint32_t OPTIONS = 0x77EEEE77;
} // namespace ProxyDestinationsMenuItemID
namespace RedirectDestinationsMenuItemID {
constexpr uint32_t GO_BACK = 0x78FFFF78;
constexpr uint32_t OPTIONS = 0x78EEEE78;
} // namespace RedirectDestinationsMenuItemID
namespace ProgramsMenuItemID {
constexpr uint32_t GO_BACK = 0x88FFFF88;
}
@@ -58,21 +65,22 @@ constexpr uint32_t GO_BACK = 0x99FFFF99;
namespace ProxyOptionsMenuItemID {
constexpr uint32_t GO_BACK = 0xAAFFFFAA;
constexpr uint32_t CHAT_COMMANDS = 0xAA0000AA;
constexpr uint32_t CHAT_FILTER = 0xAA1111AA;
constexpr uint32_t PLAYER_NOTIFICATIONS = 0xAA2222AA;
constexpr uint32_t BLOCK_PINGS = 0xAA3333AA;
constexpr uint32_t INFINITE_HP = 0xAA4444AA;
constexpr uint32_t INFINITE_TP = 0xAA5555AA;
constexpr uint32_t SWITCH_ASSIST = 0xAA6666AA;
constexpr uint32_t BLOCK_EVENTS = 0xAA7777AA;
constexpr uint32_t BLOCK_PATCHES = 0xAA8888AA;
constexpr uint32_t SAVE_FILES = 0xAA9999AA;
constexpr uint32_t RED_NAME = 0xAAAAAAAA;
constexpr uint32_t BLANK_NAME = 0xAABBBBAA;
constexpr uint32_t SUPPRESS_LOGIN = 0xAACCCCAA;
constexpr uint32_t SKIP_CARD = 0xAADDDDAA;
constexpr uint32_t EP3_INFINITE_MESETA = 0xAAEEEEAA;
constexpr uint32_t CHAT_COMMANDS = 0xAA0101AA;
constexpr uint32_t CHAT_FILTER = 0xAA0202AA;
constexpr uint32_t PLAYER_NOTIFICATIONS = 0xAA0303AA;
constexpr uint32_t BLOCK_PINGS = 0xAA0404AA;
constexpr uint32_t INFINITE_HP = 0xAA0505AA;
constexpr uint32_t INFINITE_TP = 0xAA0606AA;
constexpr uint32_t SWITCH_ASSIST = 0xAA0707AA;
constexpr uint32_t BLOCK_EVENTS = 0xAA0808AA;
constexpr uint32_t BLOCK_PATCHES = 0xAA0909AA;
constexpr uint32_t SAVE_FILES = 0xAA0A0AAA;
constexpr uint32_t RED_NAME = 0xAA0B0BAA;
constexpr uint32_t BLANK_NAME = 0xAA0C0CAA;
constexpr uint32_t SUPPRESS_LOGIN = 0xAA0D0DAA;
constexpr uint32_t SKIP_CARD = 0xAA0E0EAA;
constexpr uint32_t EP3_INFINITE_MESETA = 0xAA0F0FAA;
constexpr uint32_t EP3_INFINITE_TIME = 0xAA1010AA;
} // namespace ProxyOptionsMenuItemID
struct MenuItem {
+1 -4
View File
@@ -68,10 +68,7 @@ map<string, uint32_t> get_local_addresses() {
bool is_local_address(uint32_t addr) {
uint8_t net = (addr >> 24) & 0xFF;
if ((net != 127) && (net != 172) && (net != 10) && (net != 192)) {
return false;
}
return true;
return ((net == 127) || (net == 172) || (net == 10) || (net == 192));
}
bool is_local_address(const sockaddr_storage& daddr) {
+34 -12
View File
@@ -23,7 +23,8 @@ PSOLFGEncryption::PSOLFGEncryption(
: stream(stream_length, 0),
offset(0),
end_offset(end_offset),
seed(seed) {}
initial_seed(seed),
cycles(0) {}
uint32_t PSOLFGEncryption::next(bool advance) {
if (this->offset == this->end_offset) {
@@ -40,36 +41,44 @@ template <bool IsBigEndian>
void PSOLFGEncryption::encrypt_t(void* vdata, size_t size, bool advance) {
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
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;
size_t uint32_count = size >> 2;
size_t extra_bytes = size & 3;
U32T* data = reinterpret_cast<U32T*>(vdata);
for (size_t x = 0; x < size; x++) {
for (size_t x = 0; x < uint32_count; x++) {
data[x] ^= this->next(advance);
}
if (extra_bytes) {
U32T last = 0;
memcpy(&last, &data[uint32_count], extra_bytes);
last ^= this->next(advance);
memcpy(&data[uint32_count], &last, extra_bytes);
}
}
template <bool IsBigEndian>
void PSOLFGEncryption::encrypt_minus_t(void* vdata, size_t size, bool advance) {
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
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;
size_t uint32_count = size >> 2;
size_t extra_bytes = size & 3;
U32T* data = reinterpret_cast<U32T*>(vdata);
for (size_t x = 0; x < size; x++) {
for (size_t x = 0; x < uint32_count; x++) {
data[x] = this->next(advance) - data[x];
}
if (extra_bytes) {
U32T last = 0;
memcpy(&last, &data[uint32_count], extra_bytes);
last = this->next(advance) - last;
memcpy(&data[uint32_count], &last, extra_bytes);
}
}
void PSOLFGEncryption::encrypt(void* vdata, size_t size, bool advance) {
@@ -111,7 +120,7 @@ PSOV2Encryption::PSOV2Encryption(uint32_t seed)
: PSOLFGEncryption(seed, this->STREAM_LENGTH + 1, this->STREAM_LENGTH) {
uint32_t esi, ebx, edi, eax, edx, var1;
esi = 1;
ebx = this->seed;
ebx = this->initial_seed;
edi = 0x15;
this->stream[56] = ebx;
this->stream[55] = ebx;
@@ -128,6 +137,7 @@ PSOV2Encryption::PSOV2Encryption(uint32_t seed)
for (size_t x = 0; x < 5; x++) {
this->update_stream();
}
this->cycles = 0;
}
void PSOV2Encryption::update_stream() {
@@ -153,6 +163,7 @@ void PSOV2Encryption::update_stream() {
edx--;
}
this->offset = 1;
this->cycles++;
}
PSOEncryption::Type PSOV2Encryption::type() const {
@@ -190,6 +201,7 @@ PSOV3Encryption::PSOV3Encryption(uint32_t seed)
for (size_t x = 0; x < 4; x++) {
this->update_stream();
}
this->cycles = 0;
}
void PSOV3Encryption::update_stream() {
@@ -207,6 +219,7 @@ void PSOV3Encryption::update_stream() {
}
this->offset = 0;
this->cycles++;
}
PSOEncryption::Type PSOV3Encryption::type() const {
@@ -975,3 +988,12 @@ std::u16string decrypt_challenge_rank_text(const std::u16string& data) {
std::u16string encrypt_challenge_rank_text(const std::u16string& data) {
return encrypt_challenge_rank_text(data.data(), data.size());
}
string decrypt_v2_registry_value(const void* data, size_t size) {
string ret(reinterpret_cast<const char*>(data), size);
PSOV2Encryption crypt(0x66);
for (size_t z = 0; z < size; z++) {
ret[z] ^= (crypt.next() & 0x7F);
}
return ret;
}
+56 -4
View File
@@ -52,21 +52,28 @@ public:
uint32_t next(bool advance = true);
inline uint32_t seed() const {
return this->initial_seed;
}
uint32_t absolute_offset() const {
return (this->cycles * this->end_offset) + this->offset;
}
protected:
explicit PSOLFGEncryption(uint32_t seed, size_t stream_length, size_t end_offset);
PSOLFGEncryption(uint32_t seed, size_t stream_length, size_t end_offset);
virtual void update_stream() = 0;
std::vector<uint32_t> stream;
size_t offset;
size_t end_offset;
uint32_t seed;
uint32_t initial_seed;
size_t cycles;
};
class PSOV2Encryption : public PSOLFGEncryption {
public:
explicit PSOV2Encryption(uint32_t seed);
virtual Type type() const;
protected:
@@ -78,7 +85,6 @@ protected:
class PSOV3Encryption : public PSOLFGEncryption {
public:
explicit PSOV3Encryption(uint32_t key);
virtual Type type() const;
protected:
@@ -272,3 +278,49 @@ template <size_t Size>
std::u16string encrypt_challenge_rank_text(const ptext<char16_t, Size>& data) {
return encrypt_challenge_rank_text(data.data(), data.size());
}
std::string decrypt_v2_registry_value(const void* data, size_t size);
struct DecryptedPR2 {
std::string compressed_data;
size_t decompressed_size;
};
template <bool IsBigEndian>
DecryptedPR2 decrypt_pr2_data(const std::string& data) {
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
if (data.size() < 8) {
throw std::runtime_error("not enough data for PR2 header");
}
StringReader r(data);
DecryptedPR2 ret = {
.compressed_data = data.substr(8),
.decompressed_size = r.get<U32T>()};
PSOV2Encryption crypt(r.get<U32T>());
if (IsBigEndian) {
crypt.encrypt_big_endian(ret.compressed_data.data(), ret.compressed_data.size());
} else {
crypt.decrypt(ret.compressed_data.data(), ret.compressed_data.size());
}
return ret;
}
template <bool IsBigEndian>
std::string encrypt_pr2_data(const std::string& data, size_t decompressed_size, uint32_t seed) {
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
StringWriter w;
w.put<U32T>(decompressed_size);
w.put<U32T>(seed);
w.write(data);
std::string ret = std::move(w.str());
PSOV2Encryption crypt(seed);
if (IsBigEndian) {
crypt.encrypt_big_endian(ret.data() + 8, ret.size() - 8);
} else {
crypt.decrypt(ret.data() + 8, ret.size() - 8);
}
return ret;
}
+178 -518
View File
@@ -24,173 +24,6 @@ using namespace std;
static const string ACCOUNT_FILE_SIGNATURE =
"newserv account file format; 7 sections present; sequential;";
static FileContentsCache player_files_cache(300 * 1000 * 1000);
PlayerStats::PlayerStats() noexcept
: level(0),
experience(0),
meseta(0) {}
PlayerVisualConfig::PlayerVisualConfig() noexcept
: unknown_a2(0),
name_color(0),
extra_model(0),
unknown_a3(0),
section_id(0),
char_class(0),
v2_flags(0),
version(0),
v1_flags(0),
costume(0),
skin(0),
face(0),
head(0),
hair(0),
hair_r(0),
hair_g(0),
hair_b(0),
proportion_x(0),
proportion_y(0) {}
void PlayerDispDataDCPCV3::enforce_v2_limits() {
// V1/V2 have fewer classes, so we'll substitute some here
if (this->visual.char_class == 11) {
this->visual.char_class = 0; // FOmar -> HUmar
} else if (this->visual.char_class == 10) {
this->visual.char_class = 1; // RAmarl -> HUnewearl
} else if (this->visual.char_class == 9) {
this->visual.char_class = 5; // HUcaseal -> RAcaseal
}
// If the player is somehow still not a valid class, make them appear as the
// "ninja" NPC
if (this->visual.char_class > 8) {
this->visual.extra_model = 0;
this->visual.v2_flags |= 2;
}
this->visual.version = 2;
}
PlayerDispDataBB PlayerDispDataDCPCV3::to_bb() const {
PlayerDispDataBB bb;
bb.stats = this->stats;
bb.visual = this->visual;
bb.visual.name = " 0";
bb.name = add_language_marker(this->visual.name, 'J');
bb.config = this->config;
bb.technique_levels = this->v1_technique_levels;
return bb;
}
PlayerDispDataBB::PlayerDispDataBB() noexcept
: play_time(0),
unknown_a3(0) {}
PlayerDispDataDCPCV3 PlayerDispDataBB::to_dcpcv3() const {
PlayerDispDataDCPCV3 ret;
ret.stats = this->stats;
ret.visual = this->visual;
ret.visual.name = remove_language_marker(this->name);
ret.config = this->config;
ret.v1_technique_levels = this->technique_levels;
return ret;
}
PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const {
PlayerDispDataBBPreview pre;
pre.level = this->stats.level;
pre.experience = this->stats.experience;
pre.visual = this->visual;
pre.name = this->name;
pre.play_time = this->play_time;
return pre;
}
void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) {
this->stats.level = pre.level;
this->stats.experience = pre.experience;
this->visual = pre.visual;
this->name = pre.name;
}
void PlayerDispDataBB::apply_dressing_room(const PlayerDispDataBBPreview& pre) {
this->visual.name_color = pre.visual.name_color;
this->visual.extra_model = pre.visual.extra_model;
this->visual.unknown_a3 = pre.visual.unknown_a3;
this->visual.section_id = pre.visual.section_id;
this->visual.char_class = pre.visual.char_class;
this->visual.v2_flags = pre.visual.v2_flags;
this->visual.version = pre.visual.version;
this->visual.v1_flags = pre.visual.v1_flags;
this->visual.costume = pre.visual.costume;
this->visual.skin = pre.visual.skin;
this->visual.face = pre.visual.face;
this->visual.head = pre.visual.head;
this->visual.hair = pre.visual.hair;
this->visual.hair_r = pre.visual.hair_r;
this->visual.hair_g = pre.visual.hair_g;
this->visual.hair_b = pre.visual.hair_b;
this->visual.proportion_x = pre.visual.proportion_x;
this->visual.proportion_y = pre.visual.proportion_y;
this->name = pre.name;
}
PlayerDispDataBBPreview::PlayerDispDataBBPreview() noexcept
: experience(0),
level(0),
play_time(0) {}
GuildCardV3::GuildCardV3() noexcept
: player_tag(0),
guild_card_number(0),
present(0),
language(0),
section_id(0),
char_class(0) {}
GuildCardBB::GuildCardBB() noexcept
: guild_card_number(0),
present(0),
language(0),
section_id(0),
char_class(0) {}
void GuildCardBB::clear() {
this->guild_card_number = 0;
this->name.clear(0);
this->team_name.clear(0);
this->description.clear(0);
this->present = 0;
this->language = 0;
this->section_id = 0;
this->char_class = 0;
}
void GuildCardEntryBB::clear() {
this->data.clear();
this->unknown_a1.clear(0);
}
uint32_t GuildCardFileBB::checksum() const {
return crc32(this, sizeof(*this));
}
void PlayerBank::load(const string& filename) {
*this = player_files_cache.get_obj_or_load<PlayerBank>(filename).obj;
for (uint32_t x = 0; x < this->num_items; x++) {
this->items[x].data.id = 0x0F010000 + x;
}
}
void PlayerBank::save(const string& filename, bool save_to_filesystem) const {
player_files_cache.replace(filename, this, sizeof(*this));
if (save_to_filesystem) {
save_file(filename, this, sizeof(*this));
}
}
////////////////////////////////////////////////////////////////////////////////
ClientGameData::ClientGameData()
: last_play_time_update(0),
guild_card_number(0),
@@ -209,8 +42,100 @@ ClientGameData::~ClientGameData() {
}
}
shared_ptr<SavedAccountDataBB> ClientGameData::account(bool should_load) {
if (!this->account_data.get() && should_load) {
void ClientGameData::create_battle_overlay(shared_ptr<const BattleRules> rules, shared_ptr<const LevelTable> level_table) {
this->overlay_player_data.reset(new SavedPlayerDataBB(*this->player(true, false)));
if (rules->weapon_and_armor_mode != BattleRules::WeaponAndArmorMode::ALLOW) {
this->overlay_player_data->inventory.remove_all_items_of_type(0);
this->overlay_player_data->inventory.remove_all_items_of_type(1);
}
if (rules->mag_mode == BattleRules::MagMode::FORBID_ALL) {
this->overlay_player_data->inventory.remove_all_items_of_type(2);
}
if (rules->tool_mode != BattleRules::ToolMode::ALLOW) {
this->overlay_player_data->inventory.remove_all_items_of_type(3);
}
if (rules->replace_char) {
// TODO: Shouldn't we clear other material usage here? It looks like the
// original code doesn't, but that seems wrong.
this->overlay_player_data->inventory.hp_materials_used = 0;
this->overlay_player_data->inventory.tp_materials_used = 0;
uint32_t target_level = clamp<uint32_t>(rules->char_level, 0, 199);
uint8_t char_class = this->overlay_player_data->disp.visual.char_class;
auto& stats = this->overlay_player_data->disp.stats;
stats.reset_to_base(char_class, level_table);
stats.advance_to_level(char_class, target_level, level_table);
stats.unknown_a1 = 40;
stats.meseta = 300;
}
if (rules->tech_disk_mode == BattleRules::TechDiskMode::LIMIT_LEVEL) {
// TODO: Verify this is what the game actually does.
for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) {
uint8_t existing_level = this->overlay_player_data->get_technique_level(tech_num);
if ((existing_level != 0xFF) && (existing_level > rules->max_tech_level)) {
this->overlay_player_data->set_technique_level(tech_num, rules->max_tech_level);
}
}
} else if (rules->tech_disk_mode == BattleRules::TechDiskMode::FORBID_ALL) {
for (uint8_t tech_num = 0; tech_num < 0x13; tech_num++) {
this->overlay_player_data->set_technique_level(tech_num, 0xFF);
}
}
if (rules->meseta_mode != BattleRules::MesetaMode::ALLOW) {
this->overlay_player_data->disp.stats.meseta = 0;
}
if (rules->forbid_scape_dolls) {
this->overlay_player_data->inventory.remove_all_items_of_type(3, 9);
}
}
void ClientGameData::create_challenge_overlay(size_t template_index, shared_ptr<const LevelTable> level_table) {
const auto& tpl = get_challenge_template_definition(this->player(true, false)->disp.visual.class_flags, template_index);
this->overlay_player_data.reset(new SavedPlayerDataBB(*this->player(true, false)));
auto overlay = this->overlay_player_data;
for (size_t z = 0; z < overlay->inventory.items.size(); z++) {
auto& i = overlay->inventory.items[z];
i.present = 0;
i.extension_data1 = 0;
i.extension_data2 = 0;
i.flags = 0;
i.data = ItemData();
}
overlay->inventory.items[13].extension_data2 = 1;
overlay->disp.stats.reset_to_base(overlay->disp.visual.char_class, level_table);
overlay->disp.stats.advance_to_level(overlay->disp.visual.char_class, tpl.level, level_table);
overlay->disp.stats.unknown_a1 = 40;
overlay->disp.stats.unknown_a3 = 10.0;
overlay->disp.stats.experience = level_table->stats_delta_for_level(overlay->disp.visual.char_class, overlay->disp.stats.level).experience;
overlay->disp.stats.meseta = 0;
overlay->clear_all_material_usage();
for (size_t z = 0; z < 0x13; z++) {
overlay->set_technique_level(z, 0xFF);
}
for (size_t z = 0; z < tpl.items.size(); z++) {
auto& inv_item = overlay->inventory.items[z];
inv_item.present = tpl.items[z].present;
inv_item.flags = tpl.items[z].flags;
inv_item.data = tpl.items[z].data;
}
overlay->inventory.num_items = tpl.items.size();
for (const auto& tech_level : tpl.tech_levels) {
overlay->set_technique_level(tech_level.tech_num, tech_level.level);
}
}
shared_ptr<SavedAccountDataBB> ClientGameData::account(bool allow_load) {
if (!this->account_data.get() && allow_load) {
if (this->bb_username.empty()) {
this->account_data.reset(new SavedAccountDataBB());
this->account_data->signature = ACCOUNT_FILE_SIGNATURE;
@@ -221,8 +146,11 @@ shared_ptr<SavedAccountDataBB> ClientGameData::account(bool should_load) {
return this->account_data;
}
shared_ptr<SavedPlayerDataBB> ClientGameData::player(bool should_load) {
if (!this->player_data.get() && should_load) {
shared_ptr<SavedPlayerDataBB> ClientGameData::player(bool allow_load, bool allow_overlay) {
if (this->overlay_player_data && allow_overlay) {
return this->overlay_player_data;
}
if (!this->player_data.get() && allow_load) {
if (this->bb_username.empty()) {
this->player_data.reset(new SavedPlayerDataBB());
} else {
@@ -232,15 +160,18 @@ shared_ptr<SavedPlayerDataBB> ClientGameData::player(bool should_load) {
return this->player_data;
}
shared_ptr<const SavedAccountDataBB> ClientGameData::account() const {
if (!this->account_data.get()) {
shared_ptr<const SavedAccountDataBB> ClientGameData::account(bool allow_load) const {
if (!this->account_data.get() && allow_load) {
throw runtime_error("account data is not loaded");
}
return this->account_data;
}
shared_ptr<const SavedPlayerDataBB> ClientGameData::player() const {
if (!this->player_data.get()) {
shared_ptr<const SavedPlayerDataBB> ClientGameData::player(bool allow_load, bool allow_overlay) const {
if (allow_overlay && this->overlay_player_data) {
return this->overlay_player_data;
}
if (!this->player_data.get() && allow_load) {
throw runtime_error("player data is not loaded");
}
return this->player_data;
@@ -366,193 +297,6 @@ void ClientGameData::save_player_data() {
}
}
void PlayerLobbyDataPC::clear() {
this->player_tag = 0;
this->guild_card = 0;
this->ip_address = 0;
this->client_id = 0;
ptext<char16_t, 0x10> name;
}
void PlayerLobbyDataDCGC::clear() {
this->player_tag = 0;
this->guild_card = 0;
this->ip_address = 0;
this->client_id = 0;
ptext<char, 0x10> name;
}
void XBNetworkLocation::clear() {
this->internal_ipv4_address = 0;
this->external_ipv4_address = 0;
this->port = 0;
this->mac_address.clear(0);
this->unknown_a1.clear(0);
this->account_id = 0;
this->unknown_a2.clear(0);
}
void PlayerLobbyDataXB::clear() {
this->player_tag = 0;
this->guild_card = 0;
this->netloc.clear();
this->client_id = 0;
this->name.clear(0);
}
void PlayerLobbyDataBB::clear() {
this->player_tag = 0;
this->guild_card = 0;
this->ip_address = 0;
this->unknown_a1.clear(0);
this->client_id = 0;
this->name.clear(0);
this->unknown_a2 = 0;
}
PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec)
: title_color(rec.title_color),
unknown_u0(rec.unknown_u0),
times_ep1_online(rec.times_ep1_online),
times_ep2_online(0),
times_ep1_offline(0),
unknown_g3(rec.unknown_g3),
grave_deaths(rec.grave_deaths),
unknown_u4(0),
grave_coords_time(rec.grave_coords_time),
grave_team(rec.grave_team),
grave_message(rec.grave_message),
unknown_m5(0),
unknown_t6(0),
rank_title(encrypt_challenge_rank_text(decode_sjis(decrypt_challenge_rank_text(rec.rank_title)))),
unknown_l7(0) {}
PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec)
: title_color(rec.title_color),
unknown_u0(rec.unknown_u0),
times_ep1_online(rec.times_ep1_online),
times_ep2_online(0),
times_ep1_offline(0),
unknown_g3(rec.unknown_g3),
grave_deaths(rec.grave_deaths),
unknown_u4(0),
grave_coords_time(rec.grave_coords_time),
grave_team(rec.grave_team),
grave_message(rec.grave_message),
unknown_m5(0),
unknown_t6(0),
rank_title(rec.rank_title),
unknown_l7(0) {}
PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge<false>& rec)
: title_color(rec.title_color),
unknown_u0(rec.unknown_u0),
times_ep1_online(rec.times_ep1_online),
times_ep2_online(rec.times_ep2_online),
times_ep1_offline(rec.times_ep1_offline),
unknown_g3(rec.unknown_g3),
grave_deaths(rec.grave_deaths),
unknown_u4(rec.unknown_u4),
grave_coords_time(rec.grave_coords_time),
grave_team(rec.grave_team),
grave_message(rec.grave_message),
unknown_m5(rec.unknown_m5),
unknown_t6(rec.unknown_t6),
rank_title(encrypt_challenge_rank_text(decode_sjis(decrypt_challenge_rank_text(rec.rank_title)))),
unknown_l7(rec.unknown_l7) {}
PlayerRecordsBB_Challenge::operator PlayerRecordsDC_Challenge() const {
PlayerRecordsDC_Challenge ret;
ret.title_color = this->title_color;
ret.unknown_u0 = this->unknown_u0;
ret.rank_title = encrypt_challenge_rank_text(encode_sjis(decrypt_challenge_rank_text(this->rank_title)));
ret.times_ep1_online = this->times_ep1_online;
ret.unknown_g3 = 0;
ret.grave_deaths = this->grave_deaths;
ret.grave_coords_time = this->grave_coords_time;
ret.grave_team = this->grave_team;
ret.grave_message = this->grave_message;
ret.times_ep1_offline = this->times_ep1_offline;
ret.unknown_l4.clear(0);
return ret;
}
PlayerRecordsBB_Challenge::operator PlayerRecordsPC_Challenge() const {
PlayerRecordsPC_Challenge ret;
ret.title_color = this->title_color;
ret.unknown_u0 = this->unknown_u0;
ret.rank_title = this->rank_title;
ret.times_ep1_online = this->times_ep1_online;
ret.unknown_g3 = 0;
ret.grave_deaths = this->grave_deaths;
ret.grave_coords_time = this->grave_coords_time;
ret.grave_team = this->grave_team;
ret.grave_message = this->grave_message;
ret.times_ep1_offline = this->times_ep1_offline;
ret.unknown_l4.clear(0);
return ret;
}
PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge<false>() const {
PlayerRecordsV3_Challenge<false> ret;
ret.title_color = this->title_color;
ret.unknown_u0 = this->unknown_u0;
ret.times_ep1_online = this->times_ep1_online;
ret.times_ep2_online = this->times_ep2_online;
ret.times_ep1_offline = this->times_ep1_offline;
ret.unknown_g3 = this->unknown_g3;
ret.grave_deaths = this->grave_deaths;
ret.unknown_u4 = this->unknown_u4;
ret.grave_coords_time = this->grave_coords_time;
ret.grave_team = this->grave_team;
ret.grave_message = this->grave_message;
ret.unknown_m5 = this->unknown_m5;
ret.unknown_t6 = this->unknown_t6;
ret.rank_title = encrypt_challenge_rank_text(encode_sjis(decrypt_challenge_rank_text(this->rank_title)));
ret.unknown_l7 = this->unknown_l7;
return ret;
}
PlayerInventoryItem::PlayerInventoryItem() {
this->clear();
}
PlayerInventoryItem::PlayerInventoryItem(const PlayerBankItem& src)
: present(1),
extension_data1(0),
extension_data2(0),
flags(0),
data(src.data) {}
void PlayerInventoryItem::clear() {
this->present = 0x0000;
this->extension_data1 = 0x00;
this->extension_data2 = 0x00;
this->flags = 0x00000000;
this->data.clear();
}
PlayerBankItem::PlayerBankItem() {
this->clear();
}
PlayerBankItem::PlayerBankItem(const PlayerInventoryItem& src)
: data(src.data),
amount(this->data.stack_size()),
show_flags(1) {}
void PlayerBankItem::clear() {
this->data.clear();
this->amount = 0;
this->show_flags = 0;
}
PlayerInventory::PlayerInventory()
: num_items(0),
hp_materials_used(0),
tp_materials_used(0),
language(0) {}
void SavedPlayerDataBB::update_to_latest_version() {
if (this->signature == PLAYER_FILE_SIGNATURE_V0) {
this->signature = PLAYER_FILE_SIGNATURE_V1;
@@ -567,31 +311,31 @@ void SavedPlayerDataBB::update_to_latest_version() {
// TODO: Eliminate duplication between this function and the parallel function
// in PlayerBank
void SavedPlayerDataBB::add_item(const PlayerInventoryItem& item) {
uint32_t pid = item.data.primary_identifier();
void SavedPlayerDataBB::add_item(const ItemData& item) {
uint32_t pid = item.primary_identifier();
// Annoyingly, meseta is in the disp data, not in the inventory struct. If the
// item is meseta, we have to modify disp instead.
if (pid == MESETA_IDENTIFIER) {
this->add_meseta(item.data.data2d);
this->add_meseta(item.data2d);
return;
}
// Handle combinable items
size_t combine_max = item.data.max_stack_size();
size_t combine_max = item.max_stack_size();
if (combine_max > 1) {
// Get the item index if there's already a stack of the same item in the
// player's inventory
size_t y;
for (y = 0; y < this->inventory.num_items; y++) {
if (this->inventory.items[y].data.primary_identifier() == item.data.primary_identifier()) {
if (this->inventory.items[y].data.primary_identifier() == item.primary_identifier()) {
break;
}
}
// If we found an existing stack, add it to the total and return
if (y < this->inventory.num_items) {
this->inventory.items[y].data.data1[5] += item.data.data1[5];
this->inventory.items[y].data.data1[5] += item.data1[5];
if (this->inventory.items[y].data.data1[5] > combine_max) {
this->inventory.items[y].data.data1[5] = combine_max;
}
@@ -604,59 +348,24 @@ void SavedPlayerDataBB::add_item(const PlayerInventoryItem& item) {
if (this->inventory.num_items >= 30) {
throw runtime_error("inventory is full");
}
this->inventory.items[this->inventory.num_items] = item;
auto& inv_item = this->inventory.items[this->inventory.num_items];
inv_item.present = 1;
inv_item.flags = 0;
inv_item.data = item;
this->inventory.num_items++;
}
void PlayerBank::add_item(const PlayerBankItem& item) {
uint32_t pid = item.data.primary_identifier();
if (pid == MESETA_IDENTIFIER) {
this->meseta += item.data.data2d;
if (this->meseta > 999999) {
this->meseta = 999999;
}
return;
}
size_t combine_max = item.data.max_stack_size();
if (combine_max > 1) {
size_t y;
for (y = 0; y < this->num_items; y++) {
if (this->items[y].data.primary_identifier() == item.data.primary_identifier()) {
break;
}
}
if (y < this->num_items) {
this->items[y].data.data1[5] += item.data.data1[5];
if (this->items[y].data.data1[5] > combine_max) {
this->items[y].data.data1[5] = combine_max;
}
this->items[y].amount = this->items[y].data.data1[5];
return;
}
}
if (this->num_items >= 200) {
throw runtime_error("bank is full");
}
this->items[this->num_items] = item;
this->num_items++;
}
// TODO: Eliminate code duplication between this function and the parallel
// function in PlayerBank
PlayerInventoryItem SavedPlayerDataBB::remove_item(
uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft) {
PlayerInventoryItem ret;
ItemData SavedPlayerDataBB::remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft) {
ItemData ret;
// If we're removing meseta (signaled by an invalid item ID), then create a
// meseta item.
if (item_id == 0xFFFFFFFF) {
this->remove_meseta(amount, allow_meseta_overdraft);
ret.data.data1[0] = 0x04;
ret.data.data2d = amount;
ret.data1[0] = 0x04;
ret.data2d = amount;
return ret;
}
@@ -669,9 +378,9 @@ PlayerInventoryItem SavedPlayerDataBB::remove_item(
// applies if amount is nonzero.
if (amount && (inventory_item.data.stack_size() > 1) &&
(amount < inventory_item.data.data1[5])) {
ret = inventory_item;
ret.data.data1[5] = amount;
ret.data.id = 0xFFFFFFFF;
ret = inventory_item.data;
ret.data1[5] = amount;
ret.id = 0xFFFFFFFF;
inventory_item.data.data1[5] -= amount;
return ret;
}
@@ -679,12 +388,15 @@ PlayerInventoryItem SavedPlayerDataBB::remove_item(
// If we get here, then it's not meseta, and either it's not a combine item or
// we're removing the entire stack. Delete the item from the inventory slot
// and return the deleted item.
ret = inventory_item;
ret = inventory_item.data;
this->inventory.num_items--;
for (size_t x = index; x < this->inventory.num_items; x++) {
this->inventory.items[x] = this->inventory.items[x + 1];
}
this->inventory.items[this->inventory.num_items] = PlayerInventoryItem();
auto& last_item = this->inventory.items[this->inventory.num_items];
last_item.present = 0;
last_item.flags = 0;
last_item.data.clear();
return ret;
}
@@ -702,120 +414,68 @@ void SavedPlayerDataBB::remove_meseta(uint32_t amount, bool allow_overdraft) {
}
}
PlayerBankItem PlayerBank::remove_item(uint32_t item_id, uint32_t amount) {
PlayerBankItem ret;
if (item_id == 0xFFFFFFFF) {
if (amount > this->meseta) {
throw out_of_range("player does not have enough meseta");
}
ret.data.data1[0] = 0x04;
ret.data.data2d = amount;
this->meseta -= amount;
return ret;
}
size_t index = this->find_item(item_id);
auto& bank_item = this->items[index];
if (amount && (bank_item.data.stack_size() > 1) &&
(amount < bank_item.data.data1[5])) {
ret = bank_item;
ret.data.data1[5] = amount;
ret.amount = amount;
bank_item.data.data1[5] -= amount;
bank_item.amount -= amount;
return ret;
}
ret = bank_item;
this->num_items--;
for (size_t x = index; x < this->num_items; x++) {
this->items[x] = this->items[x + 1];
}
this->items[this->num_items] = PlayerBankItem();
return ret;
uint8_t SavedPlayerDataBB::get_technique_level(uint8_t which) const {
return (this->disp.technique_levels_v1[which] == 0xFF)
? 0xFF
: (this->disp.technique_levels_v1[which] + this->inventory.items[which].extension_data1);
}
size_t PlayerInventory::find_item(uint32_t item_id) const {
for (size_t x = 0; x < this->num_items; x++) {
if (this->items[x].data.id == item_id) {
return x;
}
void SavedPlayerDataBB::set_technique_level(uint8_t which, uint8_t level) {
if (level == 0xFF) {
this->disp.technique_levels_v1[which] = 0xFF;
this->inventory.items[which].extension_data1 = 0x00;
} else if (level <= 0x0E) {
this->disp.technique_levels_v1[which] = level;
this->inventory.items[which].extension_data1 = 0x00;
} else {
this->disp.technique_levels_v1[which] = 0x0E;
this->inventory.items[which].extension_data1 = level - 0x0E;
}
throw out_of_range("item not present");
}
size_t PlayerInventory::find_equipped_weapon() const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
if (!(this->items[y].flags & 0x00000008)) {
continue;
}
if (this->items[y].data.data1[0] != 0) {
continue;
}
if (ret < 0) {
ret = y;
} else {
throw runtime_error("multiple weapons are equipped");
}
uint8_t SavedPlayerDataBB::get_material_usage(MaterialType which) const {
switch (which) {
case MaterialType::HP:
return this->inventory.hp_materials_used;
case MaterialType::TP:
return this->inventory.tp_materials_used;
case MaterialType::POWER:
case MaterialType::MIND:
case MaterialType::EVADE:
case MaterialType::DEF:
case MaterialType::LUCK:
return this->inventory.items[8 + static_cast<uint8_t>(which)].extension_data2;
default:
throw logic_error("invalid material type");
}
if (ret < 0) {
throw out_of_range("no weapon is equipped");
}
return ret;
}
size_t PlayerInventory::find_equipped_armor() const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
if (!(this->items[y].flags & 0x00000008)) {
continue;
}
if (this->items[y].data.data1[0] != 1 || this->items[y].data.data1[1] != 1) {
continue;
}
if (ret < 0) {
ret = y;
} else {
throw runtime_error("multiple armors are equipped");
}
void SavedPlayerDataBB::set_material_usage(MaterialType which, uint8_t usage) {
switch (which) {
case MaterialType::HP:
this->inventory.hp_materials_used = usage;
break;
case MaterialType::TP:
this->inventory.tp_materials_used = usage;
break;
case MaterialType::POWER:
case MaterialType::MIND:
case MaterialType::EVADE:
case MaterialType::DEF:
case MaterialType::LUCK:
this->inventory.items[8 + static_cast<uint8_t>(which)].extension_data2 = usage;
break;
default:
throw logic_error("invalid material type");
}
if (ret < 0) {
throw out_of_range("no armor is equipped");
}
return ret;
}
size_t PlayerInventory::find_equipped_mag() const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
if (!(this->items[y].flags & 0x00000008)) {
continue;
}
if (this->items[y].data.data1[0] != 2) {
continue;
}
if (ret < 0) {
ret = y;
} else {
throw runtime_error("multiple mags are equipped");
}
void SavedPlayerDataBB::clear_all_material_usage() {
this->inventory.hp_materials_used = 0;
this->inventory.tp_materials_used = 0;
for (size_t z = 0; z < 5; z++) {
this->inventory.items[z + 8].extension_data2 = 0;
}
if (ret < 0) {
throw out_of_range("no mag is equipped");
}
return ret;
}
size_t PlayerBank::find_item(uint32_t item_id) {
for (size_t x = 0; x < this->num_items; x++) {
if (this->items[x].data.id == item_id) {
return x;
}
}
throw out_of_range("item not present");
}
void SavedPlayerDataBB::print_inventory(FILE* stream) const {
+40 -442
View File
@@ -10,92 +10,12 @@
#include <vector>
#include "Episode3/DataIndexes.hh"
#include "ItemData.hh"
#include "ItemCreator.hh"
#include "LevelTable.hh"
#include "PlayerSubordinates.hh"
#include "Text.hh"
#include "Version.hh"
struct PlayerBankItem;
// PSO V2 stored some extra data in the character structs in a format that I'm
// sure Sega thought was very clever for backward compatibility, but for us is
// just plain annoying. Specifically, they used the third and fourth bytes of
// the InventoryItem struct to store some things not present in V1. The game
// stores arrays of bytes striped across these structures. In newserv, we call
// those fields extension_data. They contain:
// items[0].extension_data1 through items[19].extension_data1:
// Extended technique levels. The values in the v1_technique_levels array
// only go up to 14 (tech level 15); if the player has a technique above
// level 15, the corresponding extension_data1 field holds the remaining
// levels (so a level 20 tech would have 14 in v1_technique_levels and 5
// in the corresponding item's extension_data1 field).
// items[0].extension_data2 through items[3].extension_data2:
// The value known as unknown_a1 in the PSOGCCharacterFile::Character
// struct. See SaveFileFormats.hh.
// items[4].extension_data2 through items[7].extension_data2:
// The timestamp when the character was last saved, in seconds since
// January 1, 2000. Stored little-endian, so items[4] contains the LSB.
// items[8].extension_data2 through items[12].extension_data2:
// Number of power materials, mind materials, evade materials, def
// materials, and luck materials (respectively) used by the player.
// items[13].extension_data2 through items[15].extension_data2:
// Unknown. These are not an array, but do appear to be related.
struct PlayerInventoryItem { // 0x1C bytes
le_uint16_t present;
// See note above about these fields
uint8_t extension_data1;
uint8_t extension_data2;
le_uint32_t flags; // 8 = equipped
ItemData data;
PlayerInventoryItem();
PlayerInventoryItem(const PlayerBankItem&);
void clear();
} __attribute__((packed));
struct PlayerBankItem { // 0x18 bytes
ItemData data;
le_uint16_t amount;
le_uint16_t show_flags;
PlayerBankItem();
PlayerBankItem(const PlayerInventoryItem&);
void clear();
} __attribute__((packed));
struct PlayerInventory { // 0x34C bytes
uint8_t num_items;
uint8_t hp_materials_used;
uint8_t tp_materials_used;
uint8_t language;
PlayerInventoryItem items[30];
PlayerInventory();
size_t find_item(uint32_t item_id) const;
size_t find_equipped_weapon() const;
size_t find_equipped_armor() const;
size_t find_equipped_mag() const;
} __attribute__((packed));
struct PlayerBank { // 0x12C8 bytes
le_uint32_t num_items;
le_uint32_t meseta;
PlayerBankItem items[200];
void load(const std::string& filename);
void save(const std::string& filename, bool save_to_filesystem) const;
bool switch_with_file(const std::string& save_filename,
const std::string& load_filename);
void add_item(const PlayerBankItem& item);
PlayerBankItem remove_item(uint32_t item_id, uint32_t amount);
size_t find_item(uint32_t item_id);
} __attribute__((packed));
struct PendingItemTrade {
uint8_t other_client_id;
bool confirmed; // true if client has sent a D2 command
@@ -108,325 +28,6 @@ struct PendingCardTrade {
std::vector<std::pair<uint32_t, uint32_t>> card_to_count;
};
struct PlayerDispDataBB;
struct PlayerStats {
/* 00 */ CharacterStats char_stats;
/* 0E */ le_uint16_t unknown_a1;
/* 10 */ le_float unknown_a2;
/* 14 */ le_float unknown_a3;
/* 18 */ le_uint32_t level;
/* 1C */ le_uint32_t experience;
/* 20 */ le_uint32_t meseta;
/* 24 */
PlayerStats() noexcept;
} __attribute__((packed));
struct PlayerVisualConfig {
/* 00 */ ptext<char, 0x10> name;
/* 10 */ le_uint64_t unknown_a2; // Note: This is probably not actually a 64-bit int.
/* 18 */ le_uint32_t name_color; // RGBA
/* 1C */ uint8_t extra_model;
/* 1D */ parray<uint8_t, 0x0F> unused;
/* 2C */ le_uint32_t unknown_a3;
/* 30 */ uint8_t section_id;
/* 31 */ uint8_t char_class;
/* 32 */ uint8_t v2_flags;
/* 33 */ uint8_t version;
/* 34 */ le_uint32_t v1_flags;
/* 38 */ le_uint16_t costume;
/* 3A */ le_uint16_t skin;
/* 3C */ le_uint16_t face;
/* 3E */ le_uint16_t head;
/* 40 */ le_uint16_t hair;
/* 42 */ le_uint16_t hair_r;
/* 44 */ le_uint16_t hair_g;
/* 46 */ le_uint16_t hair_b;
/* 48 */ le_float proportion_x;
/* 4C */ le_float proportion_y;
/* 50 */
PlayerVisualConfig() noexcept;
} __attribute__((packed));
struct PlayerDispDataDCPCV3 {
/* 00 */ PlayerStats stats;
/* 24 */ PlayerVisualConfig visual;
/* 74 */ parray<uint8_t, 0x48> config;
/* BC */ parray<uint8_t, 0x14> v1_technique_levels;
/* D0 */
// 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.
PlayerDispDataDCPCV3() noexcept = default;
void enforce_v2_limits();
PlayerDispDataBB to_bb() const;
} __attribute__((packed));
struct PlayerDispDataBBPreview {
/* 00 */ le_uint32_t experience;
/* 04 */ le_uint32_t level;
// The name field in this structure is used for the player's Guild Card
// number, apparently (possibly because it's a char array and this is BB)
/* 08 */ PlayerVisualConfig visual;
/* 58 */ ptext<char16_t, 0x10> name;
/* 78 */ uint32_t play_time;
/* 7C */
PlayerDispDataBBPreview() noexcept;
} __attribute__((packed));
// BB player appearance and stats data
struct PlayerDispDataBB {
/* 0000 */ PlayerStats stats;
/* 0024 */ PlayerVisualConfig visual;
/* 0074 */ ptext<char16_t, 0x0C> name;
/* 008C */ le_uint32_t play_time;
/* 0090 */ uint32_t unknown_a3;
/* 0094 */ parray<uint8_t, 0xE8> config;
/* 017C */ parray<uint8_t, 0x14> technique_levels;
/* 0190 */
PlayerDispDataBB() noexcept;
inline void enforce_v2_limits() {}
PlayerDispDataDCPCV3 to_dcpcv3() const;
PlayerDispDataBBPreview to_preview() const;
void apply_preview(const PlayerDispDataBBPreview&);
void apply_dressing_room(const PlayerDispDataBBPreview&);
} __attribute__((packed));
// TODO: Is this the same for XB as it is for GC? (This struct is based on the
// GC format)
struct GuildCardV3 {
/* 00 */ le_uint32_t player_tag;
/* 04 */ le_uint32_t guild_card_number;
/* 08 */ ptext<char, 0x18> name;
/* 20 */ ptext<char, 0x6C> description;
/* 8C */ uint8_t present; // should be 1
/* 8D */ uint8_t language;
/* 8E */ uint8_t section_id;
/* 8F */ uint8_t char_class;
/* 90 */
GuildCardV3() noexcept;
} __attribute__((packed));
// BB guild card format
struct GuildCardBB {
/* 0000 */ le_uint32_t guild_card_number;
/* 0004 */ ptext<char16_t, 0x18> name;
/* 0034 */ ptext<char16_t, 0x10> team_name;
/* 0054 */ ptext<char16_t, 0x58> description;
/* 0104 */ uint8_t present; // should be 1 if guild card entry exists
/* 0105 */ uint8_t language;
/* 0106 */ uint8_t section_id;
/* 0107 */ uint8_t char_class;
/* 0108 */
GuildCardBB() noexcept;
void clear();
} __attribute__((packed));
// an entry in the BB guild card file
struct GuildCardEntryBB {
GuildCardBB data;
ptext<char16_t, 0x58> comment;
parray<uint8_t, 0x4> unknown_a1;
void clear();
} __attribute__((packed));
// the format of the BB guild card file
struct GuildCardFileBB {
parray<uint8_t, 0x114> unknown_a1;
GuildCardBB blocked[0x1C];
parray<uint8_t, 0x180> unknown_a2;
GuildCardEntryBB entries[0x69];
uint32_t checksum() const;
} __attribute__((packed));
struct KeyAndTeamConfigBB {
parray<uint8_t, 0x0114> unknown_a1; // 0000
parray<uint8_t, 0x016C> key_config; // 0114
parray<uint8_t, 0x0038> joystick_config; // 0280
le_uint32_t guild_card_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 {
le_uint32_t player_tag = 0;
le_uint32_t guild_card = 0;
// There's a strange behavior (bug? "feature"?) in Episode 3 where the start
// button does nothing in the lobby (hence you can't "quit game") if the
// client's IP address is zero. So, we fill it in with a fake nonzero value to
// avoid this behavior, and to be consistent, we make IP addresses fake and
// nonzero on all other versions too.
be_uint32_t ip_address = 0x7F000001;
le_uint32_t client_id = 0;
ptext<char16_t, 0x10> name;
void clear();
} __attribute__((packed));
struct PlayerLobbyDataDCGC {
le_uint32_t player_tag = 0;
le_uint32_t guild_card = 0;
be_uint32_t ip_address = 0x7F000001;
le_uint32_t client_id = 0;
ptext<char, 0x10> name;
void clear();
} __attribute__((packed));
struct XBNetworkLocation {
le_uint32_t internal_ipv4_address = 0x0A0A0A0A;
le_uint32_t external_ipv4_address = 0x23232323;
le_uint16_t port = 9100;
parray<uint8_t, 6> mac_address = 0x77;
parray<le_uint32_t, 2> unknown_a1;
le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
parray<le_uint32_t, 4> unknown_a2;
void clear();
} __attribute__((packed));
struct PlayerLobbyDataXB {
le_uint32_t player_tag = 0;
le_uint32_t guild_card = 0;
XBNetworkLocation netloc;
le_uint32_t client_id = 0;
ptext<char, 0x10> name;
void clear();
} __attribute__((packed));
struct PlayerLobbyDataBB {
le_uint32_t player_tag = 0;
le_uint32_t guild_card = 0;
// This field is a guess; the official builds didn't use this, but all other
// versions have it
be_uint32_t ip_address = 0x7F000001;
parray<uint8_t, 0x10> unknown_a1;
le_uint32_t client_id = 0;
ptext<char16_t, 0x10> name;
le_uint32_t unknown_a2 = 0;
void clear();
} __attribute__((packed));
template <bool IsWideChar>
struct PlayerRecordsDCPC_Challenge {
using CharT = typename std::conditional<IsWideChar, char16_t, char>::type;
/* 00 */ le_uint16_t title_color = 0x7FFF;
/* 02 */ parray<uint8_t, 2> unknown_u0;
/* 04 */ ptext<CharT, 0x0C> rank_title; // Encrypted; see decrypt_challenge_rank_text
/* 10 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time. TODO: This might be offline times
/* 34 */ le_uint16_t unknown_g3 = 0;
/* 36 */ le_uint16_t grave_deaths = 0;
/* 38 */ parray<le_uint32_t, 5> grave_coords_time;
/* 4C */ ptext<CharT, 0x14> grave_team;
/* 60 */ ptext<CharT, 0x18> grave_message;
/* 78 */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times
/* 9C */ parray<uint8_t, 4> unknown_l4;
/* A0 */
} __attribute__((packed));
struct PlayerRecordsDC_Challenge : PlayerRecordsDCPC_Challenge<false> {
} __attribute__((packed));
struct PlayerRecordsPC_Challenge : PlayerRecordsDCPC_Challenge<true> {
} __attribute__((packed));
template <bool IsBigEndian>
struct PlayerRecordsV3_Challenge {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
// Offsets are (1) relative to start of C5 entry, and (2) relative to start
// of save file structure
/* 0000:001C */ U16T title_color = 0x7FFF; // XRGB1555
/* 0002:001E */ parray<uint8_t, 2> unknown_u0;
/* 0004:0020 */ parray<U32T, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time
/* 0028:0044 */ parray<U32T, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time
/* 003C:0058 */ parray<U32T, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time
/* 0060:007C */ parray<uint8_t, 4> unknown_g3;
/* 0064:0080 */ U16T grave_deaths = 0;
/* 0066:0082 */ parray<uint8_t, 2> unknown_u4;
/* 0068:0084 */ parray<U32T, 5> grave_coords_time;
/* 007C:0098 */ ptext<char, 0x14> grave_team;
/* 0090:00AC */ ptext<char, 0x20> grave_message;
/* 00B0:00CC */ parray<uint8_t, 4> unknown_m5;
/* 00B4:00D0 */ parray<U32T, 9> unknown_t6;
/* 00D8:00F4 */ ptext<char, 0x0C> rank_title; // Encrypted; see decrypt_challenge_rank_text
/* 00E4:0100 */ parray<uint8_t, 0x1C> unknown_l7;
/* 0100:011C */
} __attribute__((packed));
struct PlayerRecordsBB_Challenge {
/* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555
/* 0002 */ parray<uint8_t, 2> unknown_u0;
/* 0004 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time
/* 0028 */ parray<le_uint32_t, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time
/* 003C */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time
/* 0060 */ parray<uint8_t, 4> unknown_g3;
/* 0064 */ le_uint16_t grave_deaths = 0;
/* 0066 */ parray<uint8_t, 2> unknown_u4;
/* 0068 */ parray<le_uint32_t, 5> grave_coords_time;
/* 007C */ ptext<char16_t, 0x14> grave_team;
/* 00A4 */ ptext<char16_t, 0x20> grave_message;
/* 00E4 */ parray<uint8_t, 4> unknown_m5;
/* 00E8 */ parray<le_uint32_t, 9> unknown_t6;
/* 010C */ ptext<char16_t, 0x0C> rank_title; // Encrypted; see decrypt_challenge_rank_text
/* 0124 */ parray<uint8_t, 0x1C> unknown_l7;
/* 0140 */
PlayerRecordsBB_Challenge() = default;
PlayerRecordsBB_Challenge(const PlayerRecordsBB_Challenge& other) = default;
PlayerRecordsBB_Challenge& operator=(const PlayerRecordsBB_Challenge& other) = default;
PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec);
PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec);
PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge<false>& rec);
operator PlayerRecordsDC_Challenge() const;
operator PlayerRecordsPC_Challenge() const;
operator PlayerRecordsV3_Challenge<false>() const;
} __attribute__((packed));
template <bool IsBigEndian>
struct PlayerRecords_Battle {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
// On Episode 3, battle_place_counts[0] is win count and [1] is loss count
/* 00 */ parray<U16T, 4> place_counts;
/* 08 */ U16T disconnect_count;
/* 0A */ parray<uint8_t, 0x0E> unknown_a1;
/* 18 */
} __attribute__((packed));
template <typename ItemIDT>
struct ChoiceSearchConfig {
// 0 = enabled, 1 = disabled. Unused for command C3
le_uint32_t choice_search_disabled = 0;
struct Entry {
ItemIDT parent_category_id = 0;
ItemIDT category_id = 0;
} __attribute__((packed));
parray<Entry, 5> entries;
} __attribute__((packed));
constexpr uint64_t PLAYER_FILE_SIGNATURE_V0 = 0x6E65777365727620;
constexpr uint64_t PLAYER_FILE_SIGNATURE_V1 = 0xA904332D5CEF0296;
@@ -443,18 +44,34 @@ struct SavedPlayerDataBB { // .nsc file format
/* 185C */ ptext<char16_t, 0x00AC> info_board;
/* 19B4 */ PlayerInventory inventory;
/* 1D00 */ parray<uint8_t, 0x0208> quest_data1;
/* 1F08 */ parray<uint8_t, 0x0058> quest_data2;
/* 1F08 */ parray<le_uint32_t, 0x0016> quest_data2;
/* 1F60 */ parray<uint8_t, 0x0028> tech_menu_config;
/* 1F88 */
void update_to_latest_version();
void add_item(const PlayerInventoryItem& item);
PlayerInventoryItem remove_item(
uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft);
void add_item(const ItemData& item);
ItemData remove_item(uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft);
void add_meseta(uint32_t amount);
void remove_meseta(uint32_t amount, bool allow_overdraft);
uint8_t get_technique_level(uint8_t which) const; // Returns FF or 00-1D
void set_technique_level(uint8_t which, uint8_t level);
enum class MaterialType : int8_t {
HP = -2,
TP = -1,
POWER = 0,
MIND = 1,
EVADE = 2,
DEF = 3,
LUCK = 4,
};
uint8_t get_material_usage(MaterialType which) const;
void set_material_usage(MaterialType which, uint8_t usage);
void clear_all_material_usage();
void print_inventory(FILE* stream) const;
} __attribute__((packed));
@@ -477,6 +94,10 @@ struct SavedAccountDataBB { // .nsa file format
class ClientGameData {
private:
std::shared_ptr<SavedAccountDataBB> account_data;
// The overlay player data is used in battle and challenge modes, when player
// data is temporarily replaced in-game. In other play modes and in lobbies,
// overlay_player_data is null.
std::shared_ptr<SavedPlayerDataBB> overlay_player_data;
std::shared_ptr<SavedPlayerDataBB> player_data;
uint64_t last_play_time_update;
@@ -496,17 +117,26 @@ public:
// These are only used if the client is BB
std::string bb_username;
size_t bb_player_index;
PlayerInventoryItem identify_result;
ItemData identify_result;
std::array<std::vector<ItemData>, 3> shop_contents;
bool should_save;
ClientGameData();
~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;
void create_battle_overlay(std::shared_ptr<const BattleRules> rules, std::shared_ptr<const LevelTable> level_table);
void create_challenge_overlay(size_t template_index, std::shared_ptr<const LevelTable> level_table);
inline void delete_overlay() {
this->overlay_player_data.reset();
}
inline bool has_overlay() const {
return this->overlay_player_data.get() != nullptr;
}
std::shared_ptr<SavedAccountDataBB> account(bool allow_load = true);
std::shared_ptr<SavedPlayerDataBB> player(bool allow_load = true, bool allow_overlay = true);
std::shared_ptr<const SavedAccountDataBB> account(bool allow_load = true) const;
std::shared_ptr<const SavedPlayerDataBB> player(bool allow_load = true, bool allow_overlay = true) const;
std::string account_data_filename() const;
std::string player_data_filename() const;
@@ -522,35 +152,3 @@ public:
// Note: This function is not const because it updates the player's play time.
void save_player_data();
};
uint32_t compute_guild_card_checksum(const void* data, size_t size);
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 PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3>(
const PlayerDispDataDCPCV3& src) {
return src;
}
template <>
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(
const PlayerDispDataBB& src) {
return src.to_dcpcv3();
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB, PlayerDispDataDCPCV3>(
const PlayerDispDataDCPCV3& src) {
return src.to_bb();
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
const PlayerDispDataBB& src) {
return src;
}
+799
View File
@@ -0,0 +1,799 @@
#include "PlayerSubordinates.hh"
#include <stdio.h>
#include <string.h>
#include <wchar.h>
#include <phosg/Filesystem.hh>
#include <phosg/Hash.hh>
#include <stdexcept>
#include "ItemData.hh"
#include "Loggers.hh"
#include "PSOEncryption.hh"
#include "StaticGameData.hh"
#include "Text.hh"
#include "Version.hh"
using namespace std;
FileContentsCache player_files_cache(300 * 1000 * 1000);
void PlayerDispDataDCPCV3::enforce_lobby_join_limits(GameVersion target_version) {
if ((target_version == GameVersion::PC) || (target_version == GameVersion::DC)) {
// V1/V2 have fewer classes, so we'll substitute some here
if (this->visual.char_class == 9) {
this->visual.char_class = 5; // HUcaseal -> RAcaseal
} else if (this->visual.char_class == 10) {
this->visual.char_class = 0; // FOmar -> HUmar
} else if (this->visual.char_class == 11) {
this->visual.char_class = 1; // RAmarl -> HUnewearl
}
// V1/V2 has fewer costumes, so substitute them here too
this->visual.costume %= 9;
// If the player is somehow still not a valid class, make them appear as the
// "ninja" NPC
if (this->visual.char_class > 8) {
this->visual.extra_model = 0;
this->visual.v2_flags |= 2;
}
this->visual.version = 2;
}
}
void PlayerDispDataBB::enforce_lobby_join_limits(GameVersion) {
this->play_time = 0;
}
PlayerDispDataBB PlayerDispDataDCPCV3::to_bb() const {
PlayerDispDataBB bb;
bb.stats = this->stats;
bb.visual = this->visual;
bb.visual.name = " 0";
bb.name = this->visual.name;
bb.config = this->config;
bb.technique_levels_v1 = this->technique_levels_v1;
return bb;
}
PlayerDispDataDCPCV3 PlayerDispDataBB::to_dcpcv3() const {
PlayerDispDataDCPCV3 ret;
ret.stats = this->stats;
ret.visual = this->visual;
ret.visual.name = this->name;
remove_language_marker_inplace(ret.visual.name);
ret.config = this->config;
ret.technique_levels_v1 = this->technique_levels_v1;
return ret;
}
PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const {
PlayerDispDataBBPreview pre;
pre.level = this->stats.level;
pre.experience = this->stats.experience;
pre.visual = this->visual;
pre.name = this->name;
pre.play_time = this->play_time;
return pre;
}
void PlayerDispDataBB::apply_preview(const PlayerDispDataBBPreview& pre) {
this->stats.level = pre.level;
this->stats.experience = pre.experience;
this->visual = pre.visual;
this->name = pre.name;
}
void PlayerDispDataBB::apply_dressing_room(const PlayerDispDataBBPreview& pre) {
this->visual.name_color = pre.visual.name_color;
this->visual.extra_model = pre.visual.extra_model;
this->visual.unknown_a3 = pre.visual.unknown_a3;
this->visual.section_id = pre.visual.section_id;
this->visual.char_class = pre.visual.char_class;
this->visual.v2_flags = pre.visual.v2_flags;
this->visual.version = pre.visual.version;
this->visual.class_flags = pre.visual.class_flags;
this->visual.costume = pre.visual.costume;
this->visual.skin = pre.visual.skin;
this->visual.face = pre.visual.face;
this->visual.head = pre.visual.head;
this->visual.hair = pre.visual.hair;
this->visual.hair_r = pre.visual.hair_r;
this->visual.hair_g = pre.visual.hair_g;
this->visual.hair_b = pre.visual.hair_b;
this->visual.proportion_x = pre.visual.proportion_x;
this->visual.proportion_y = pre.visual.proportion_y;
this->name = pre.name;
}
void GuildCardBB::clear() {
this->guild_card_number = 0;
this->name.clear(0);
this->team_name.clear(0);
this->description.clear(0);
this->present = 0;
this->language = 0;
this->section_id = 0;
this->char_class = 0;
}
void GuildCardEntryBB::clear() {
this->data.clear();
this->unknown_a1.clear(0);
}
uint32_t GuildCardFileBB::checksum() const {
return crc32(this, sizeof(*this));
}
void PlayerBank::load(const string& filename) {
*this = player_files_cache.get_obj_or_load<PlayerBank>(filename).obj;
for (uint32_t x = 0; x < this->num_items; x++) {
this->items[x].data.id = 0x0F010000 + x;
}
}
void PlayerBank::save(const string& filename, bool save_to_filesystem) const {
player_files_cache.replace(filename, this, sizeof(*this));
if (save_to_filesystem) {
save_file(filename, this, sizeof(*this));
}
}
void PlayerLobbyDataPC::clear() {
this->player_tag = 0;
this->guild_card = 0;
this->ip_address = 0;
this->client_id = 0;
ptext<char16_t, 0x10> name;
}
void PlayerLobbyDataDCGC::clear() {
this->player_tag = 0;
this->guild_card = 0;
this->ip_address = 0;
this->client_id = 0;
ptext<char, 0x10> name;
}
void XBNetworkLocation::clear() {
this->internal_ipv4_address = 0;
this->external_ipv4_address = 0;
this->port = 0;
this->mac_address.clear(0);
this->unknown_a1.clear(0);
this->account_id = 0;
this->unknown_a2.clear(0);
}
void PlayerLobbyDataXB::clear() {
this->player_tag = 0;
this->guild_card = 0;
this->netloc.clear();
this->client_id = 0;
this->name.clear(0);
}
void PlayerLobbyDataBB::clear() {
this->player_tag = 0;
this->guild_card = 0;
this->ip_address = 0;
this->unknown_a1.clear(0);
this->client_id = 0;
this->name.clear(0);
this->unknown_a2 = 0;
}
PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec)
: title_color(rec.title_color),
unknown_u0(rec.unknown_u0),
times_ep1_online(rec.times_ep1_online),
times_ep2_online(0),
times_ep1_offline(0),
unknown_g3(rec.unknown_g3),
grave_deaths(rec.grave_deaths),
unknown_u4(0),
grave_coords_time(rec.grave_coords_time),
grave_team(rec.grave_team),
grave_message(rec.grave_message),
unknown_m5(0),
unknown_t6(0),
rank_title(encrypt_challenge_rank_text(decode_sjis(decrypt_challenge_rank_text(rec.rank_title)))),
unknown_l7(0) {}
PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec)
: title_color(rec.title_color),
unknown_u0(rec.unknown_u0),
times_ep1_online(rec.times_ep1_online),
times_ep2_online(0),
times_ep1_offline(0),
unknown_g3(rec.unknown_g3),
grave_deaths(rec.grave_deaths),
unknown_u4(0),
grave_coords_time(rec.grave_coords_time),
grave_team(rec.grave_team),
grave_message(rec.grave_message),
unknown_m5(0),
unknown_t6(0),
rank_title(rec.rank_title),
unknown_l7(0) {}
PlayerRecordsBB_Challenge::PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge<false>& rec)
: title_color(rec.stats.title_color),
unknown_u0(rec.stats.unknown_u0),
times_ep1_online(rec.stats.times_ep1_online),
times_ep2_online(rec.stats.times_ep2_online),
times_ep1_offline(rec.stats.times_ep1_offline),
unknown_g3(rec.stats.unknown_g3),
grave_deaths(rec.stats.grave_deaths),
unknown_u4(rec.stats.unknown_u4),
grave_coords_time(rec.stats.grave_coords_time),
grave_team(rec.stats.grave_team),
grave_message(rec.stats.grave_message),
unknown_m5(rec.stats.unknown_m5),
unknown_t6(rec.stats.unknown_t6),
rank_title(encrypt_challenge_rank_text(decode_sjis(decrypt_challenge_rank_text(rec.rank_title)))),
unknown_l7(rec.unknown_l7) {}
PlayerRecordsBB_Challenge::operator PlayerRecordsDC_Challenge() const {
PlayerRecordsDC_Challenge ret;
ret.title_color = this->title_color;
ret.unknown_u0 = this->unknown_u0;
ret.rank_title = encrypt_challenge_rank_text(encode_sjis(decrypt_challenge_rank_text(this->rank_title)));
ret.times_ep1_online = this->times_ep1_online;
ret.unknown_g3 = 0;
ret.grave_deaths = this->grave_deaths;
ret.grave_coords_time = this->grave_coords_time;
ret.grave_team = this->grave_team;
ret.grave_message = this->grave_message;
ret.times_ep1_offline = this->times_ep1_offline;
ret.unknown_l4.clear(0);
return ret;
}
PlayerRecordsBB_Challenge::operator PlayerRecordsPC_Challenge() const {
PlayerRecordsPC_Challenge ret;
ret.title_color = this->title_color;
ret.unknown_u0 = this->unknown_u0;
ret.rank_title = this->rank_title;
ret.times_ep1_online = this->times_ep1_online;
ret.unknown_g3 = 0;
ret.grave_deaths = this->grave_deaths;
ret.grave_coords_time = this->grave_coords_time;
ret.grave_team = this->grave_team;
ret.grave_message = this->grave_message;
ret.times_ep1_offline = this->times_ep1_offline;
ret.unknown_l4.clear(0);
return ret;
}
PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge<false>() const {
PlayerRecordsV3_Challenge<false> ret;
ret.stats.title_color = this->title_color;
ret.stats.unknown_u0 = this->unknown_u0;
ret.stats.times_ep1_online = this->times_ep1_online;
ret.stats.times_ep2_online = this->times_ep2_online;
ret.stats.times_ep1_offline = this->times_ep1_offline;
ret.stats.unknown_g3 = this->unknown_g3;
ret.stats.grave_deaths = this->grave_deaths;
ret.stats.unknown_u4 = this->unknown_u4;
ret.stats.grave_coords_time = this->grave_coords_time;
ret.stats.grave_team = this->grave_team;
ret.stats.grave_message = this->grave_message;
ret.stats.unknown_m5 = this->unknown_m5;
ret.stats.unknown_t6 = this->unknown_t6;
ret.rank_title = encrypt_challenge_rank_text(encode_sjis(decrypt_challenge_rank_text(this->rank_title)));
ret.unknown_l7 = this->unknown_l7;
return ret;
}
PlayerInventory::PlayerInventory()
: num_items(0),
hp_materials_used(0),
tp_materials_used(0),
language(0) {}
void PlayerBank::add_item(const ItemData& item) {
uint32_t pid = item.primary_identifier();
if (pid == MESETA_IDENTIFIER) {
this->meseta += item.data2d;
if (this->meseta > 999999) {
this->meseta = 999999;
}
return;
}
size_t combine_max = item.max_stack_size();
if (combine_max > 1) {
size_t y;
for (y = 0; y < this->num_items; y++) {
if (this->items[y].data.primary_identifier() == item.primary_identifier()) {
break;
}
}
if (y < this->num_items) {
this->items[y].data.data1[5] += item.data1[5];
if (this->items[y].data.data1[5] > combine_max) {
this->items[y].data.data1[5] = combine_max;
}
this->items[y].amount = this->items[y].data.data1[5];
return;
}
}
if (this->num_items >= 200) {
throw runtime_error("bank is full");
}
auto& last_item = this->items[this->num_items];
last_item.data = item;
last_item.amount = (item.max_stack_size() > 1) ? item.data1[5] : 1;
last_item.present = 1;
this->num_items++;
}
ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount) {
ItemData ret;
if (item_id == 0xFFFFFFFF) {
if (amount > this->meseta) {
throw out_of_range("player does not have enough meseta");
}
ret.data1[0] = 0x04;
ret.data2d = amount;
this->meseta -= amount;
return ret;
}
size_t index = this->find_item(item_id);
auto& bank_item = this->items[index];
if (amount && (bank_item.data.stack_size() > 1) && (amount < bank_item.data.data1[5])) {
ret = bank_item.data;
ret.data1[5] = amount;
bank_item.data.data1[5] -= amount;
bank_item.amount -= amount;
return ret;
}
ret = bank_item.data;
this->num_items--;
for (size_t x = index; x < this->num_items; x++) {
this->items[x] = this->items[x + 1];
}
auto& last_item = this->items[this->num_items];
last_item.amount = 0;
last_item.present = 0;
last_item.data.clear();
return ret;
}
size_t PlayerInventory::find_item(uint32_t item_id) const {
for (size_t x = 0; x < this->num_items; x++) {
if (this->items[x].data.id == item_id) {
return x;
}
}
throw out_of_range("item not present");
}
size_t PlayerInventory::find_item_by_primary_identifier(uint32_t primary_identifier) const {
for (size_t x = 0; x < this->num_items; x++) {
if (this->items[x].data.primary_identifier() == primary_identifier) {
return x;
}
}
throw out_of_range("item not present");
}
size_t PlayerInventory::find_equipped_weapon() const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
if (!(this->items[y].flags & 0x00000008)) {
continue;
}
if (this->items[y].data.data1[0] != 0) {
continue;
}
if (ret < 0) {
ret = y;
} else {
throw runtime_error("multiple weapons are equipped");
}
}
if (ret < 0) {
throw out_of_range("no weapon is equipped");
}
return ret;
}
size_t PlayerInventory::find_equipped_armor() const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
if (!(this->items[y].flags & 0x00000008)) {
continue;
}
if (this->items[y].data.data1[0] != 1 || this->items[y].data.data1[1] != 1) {
continue;
}
if (ret < 0) {
ret = y;
} else {
throw runtime_error("multiple armors are equipped");
}
}
if (ret < 0) {
throw out_of_range("no armor is equipped");
}
return ret;
}
size_t PlayerInventory::find_equipped_mag() const {
ssize_t ret = -1;
for (size_t y = 0; y < this->num_items; y++) {
if (!(this->items[y].flags & 0x00000008)) {
continue;
}
if (this->items[y].data.data1[0] != 2) {
continue;
}
if (ret < 0) {
ret = y;
} else {
throw runtime_error("multiple mags are equipped");
}
}
if (ret < 0) {
throw out_of_range("no mag is equipped");
}
return ret;
}
size_t PlayerInventory::remove_all_items_of_type(uint8_t data1_0, int16_t data1_1) {
size_t write_offset = 0;
for (size_t read_offset = 0; read_offset < this->num_items; read_offset++) {
bool should_delete = ((this->items[read_offset].data.data1[0] == data1_0) &&
((data1_1 < 0) || (this->items[read_offset].data.data1[1] == static_cast<uint8_t>(data1_1))));
if (!should_delete) {
if (read_offset != write_offset) {
this->items[write_offset].present = this->items[read_offset].present;
this->items[write_offset].flags = this->items[read_offset].flags;
this->items[write_offset].data = this->items[read_offset].data;
}
write_offset++;
}
}
size_t ret = this->num_items - write_offset;
this->num_items = write_offset;
return ret;
}
void PlayerInventory::decode_mags(GameVersion version) {
for (size_t z = 0; z < this->items.size(); z++) {
this->items[z].data.decode_if_mag(version);
}
}
void PlayerInventory::encode_mags(GameVersion version) {
for (size_t z = 0; z < this->items.size(); z++) {
this->items[z].data.encode_if_mag(version);
}
}
size_t PlayerBank::find_item(uint32_t item_id) {
for (size_t x = 0; x < this->num_items; x++) {
if (this->items[x].data.id == item_id) {
return x;
}
}
throw out_of_range("item not present");
}
BattleRules::BattleRules(const JSON& json) {
static const JSON empty_list = JSON::list();
this->tech_disk_mode = json.get_enum("tech_disk_mode", this->tech_disk_mode);
this->weapon_and_armor_mode = json.get_enum("weapon_and_armor_mode", this->weapon_and_armor_mode);
this->mag_mode = json.get_enum("mag_mode", this->mag_mode);
this->tool_mode = json.get_enum("tool_mode", this->tool_mode);
this->trap_mode = json.get_enum("trap_mode", this->trap_mode);
this->unused_F817 = json.get_int("unused_F817", this->unused_F817);
this->respawn_mode = json.get_int("respawn_mode", this->respawn_mode);
this->replace_char = json.get_int("replace_char", this->replace_char);
this->drop_weapon = json.get_int("drop_weapon", this->drop_weapon);
this->is_teams = json.get_int("is_teams", this->is_teams);
this->hide_target_reticle = json.get_int("hide_target_reticle", this->hide_target_reticle);
this->meseta_mode = json.get_enum("meseta_mode", this->meseta_mode);
this->death_level_up = json.get_int("death_level_up", this->death_level_up);
const JSON& trap_counts_json = json.get("trap_counts", empty_list);
for (size_t z = 0; z < trap_counts_json.size(); z++) {
this->trap_counts[z] = trap_counts_json.at(z).as_int();
}
this->enable_sonar = json.get_int("enable_sonar", this->enable_sonar);
this->sonar_count = json.get_int("sonar_count", this->sonar_count);
this->forbid_scape_dolls = json.get_int("forbid_scape_dolls", this->forbid_scape_dolls);
this->lives = json.get_int("lives", this->lives);
this->max_tech_level = json.get_int("max_tech_level", this->max_tech_level);
this->char_level = json.get_int("char_level", this->char_level);
this->time_limit = json.get_int("time_limit", this->time_limit);
this->death_tech_level_up = json.get_int("death_tech_level_up", this->death_tech_level_up);
this->box_drop_area = json.get_int("box_drop_area", this->box_drop_area);
}
JSON BattleRules::json() const {
return JSON::dict({
{"tech_disk_mode", this->tech_disk_mode},
{"weapon_and_armor_mode", this->weapon_and_armor_mode},
{"mag_mode", this->mag_mode},
{"tool_mode", this->tool_mode},
{"trap_mode", this->trap_mode},
{"unused_F817", this->unused_F817},
{"respawn_mode", this->respawn_mode},
{"replace_char", this->replace_char},
{"drop_weapon", this->drop_weapon},
{"is_teams", this->is_teams},
{"hide_target_reticle", this->hide_target_reticle},
{"meseta_mode", this->meseta_mode},
{"death_level_up", this->death_level_up},
{"trap_counts", JSON::list({this->trap_counts[0], this->trap_counts[1], this->trap_counts[2], this->trap_counts[3]})},
{"enable_sonar", this->enable_sonar},
{"sonar_count", this->sonar_count},
{"forbid_scape_dolls", this->forbid_scape_dolls},
{"lives", this->lives.load()},
{"max_tech_level", this->max_tech_level.load()},
{"char_level", this->char_level.load()},
{"time_limit", this->time_limit.load()},
{"death_tech_level_up", this->death_tech_level_up.load()},
{"box_drop_area", this->box_drop_area.load()},
});
}
template <>
const char* name_for_enum<BattleRules::TechDiskMode>(BattleRules::TechDiskMode v) {
switch (v) {
case BattleRules::TechDiskMode::ALLOW:
return "ALLOW";
case BattleRules::TechDiskMode::FORBID_ALL:
return "FORBID_ALL";
case BattleRules::TechDiskMode::LIMIT_LEVEL:
return "LIMIT_LEVEL";
default:
throw invalid_argument("invalid BattleRules::TechDiskMode value");
}
}
template <>
BattleRules::TechDiskMode enum_for_name<BattleRules::TechDiskMode>(const char* name) {
if (!strcmp(name, "ALLOW")) {
return BattleRules::TechDiskMode::ALLOW;
} else if (!strcmp(name, "FORBID_ALL")) {
return BattleRules::TechDiskMode::FORBID_ALL;
} else if (!strcmp(name, "LIMIT_LEVEL")) {
return BattleRules::TechDiskMode::LIMIT_LEVEL;
} else {
throw invalid_argument("invalid BattleRules::TechDiskMode name");
}
}
template <>
const char* name_for_enum<BattleRules::WeaponAndArmorMode>(BattleRules::WeaponAndArmorMode v) {
switch (v) {
case BattleRules::WeaponAndArmorMode::ALLOW:
return "ALLOW";
case BattleRules::WeaponAndArmorMode::CLEAR_AND_ALLOW:
return "CLEAR_AND_ALLOW";
case BattleRules::WeaponAndArmorMode::FORBID_ALL:
return "FORBID_ALL";
case BattleRules::WeaponAndArmorMode::FORBID_RARES:
return "FORBID_RARES";
default:
throw invalid_argument("invalid BattleRules::WeaponAndArmorMode value");
}
}
template <>
BattleRules::WeaponAndArmorMode enum_for_name<BattleRules::WeaponAndArmorMode>(const char* name) {
if (!strcmp(name, "ALLOW")) {
return BattleRules::WeaponAndArmorMode::ALLOW;
} else if (!strcmp(name, "CLEAR_AND_ALLOW")) {
return BattleRules::WeaponAndArmorMode::CLEAR_AND_ALLOW;
} else if (!strcmp(name, "FORBID_ALL")) {
return BattleRules::WeaponAndArmorMode::FORBID_ALL;
} else if (!strcmp(name, "FORBID_RARES")) {
return BattleRules::WeaponAndArmorMode::FORBID_RARES;
} else {
throw invalid_argument("invalid BattleRules::WeaponAndArmorMode name");
}
}
template <>
const char* name_for_enum<BattleRules::MagMode>(BattleRules::MagMode v) {
switch (v) {
case BattleRules::MagMode::ALLOW:
return "ALLOW";
case BattleRules::MagMode::FORBID_ALL:
return "FORBID_ALL";
default:
throw invalid_argument("invalid BattleRules::MagMode value");
}
}
template <>
BattleRules::MagMode enum_for_name<BattleRules::MagMode>(const char* name) {
if (!strcmp(name, "ALLOW")) {
return BattleRules::MagMode::ALLOW;
} else if (!strcmp(name, "FORBID_ALL")) {
return BattleRules::MagMode::FORBID_ALL;
} else {
throw invalid_argument("invalid BattleRules::MagMode name");
}
}
template <>
const char* name_for_enum<BattleRules::ToolMode>(BattleRules::ToolMode v) {
switch (v) {
case BattleRules::ToolMode::ALLOW:
return "ALLOW";
case BattleRules::ToolMode::CLEAR_AND_ALLOW:
return "CLEAR_AND_ALLOW";
case BattleRules::ToolMode::FORBID_ALL:
return "FORBID_ALL";
default:
throw invalid_argument("invalid BattleRules::ToolMode value");
}
}
template <>
BattleRules::ToolMode enum_for_name<BattleRules::ToolMode>(const char* name) {
if (!strcmp(name, "ALLOW")) {
return BattleRules::ToolMode::ALLOW;
} else if (!strcmp(name, "CLEAR_AND_ALLOW")) {
return BattleRules::ToolMode::CLEAR_AND_ALLOW;
} else if (!strcmp(name, "FORBID_ALL")) {
return BattleRules::ToolMode::FORBID_ALL;
} else {
throw invalid_argument("invalid BattleRules::ToolMode name");
}
}
template <>
const char* name_for_enum<BattleRules::TrapMode>(BattleRules::TrapMode v) {
switch (v) {
case BattleRules::TrapMode::DEFAULT:
return "DEFAULT";
case BattleRules::TrapMode::ALL_PLAYERS:
return "ALL_PLAYERS";
default:
throw invalid_argument("invalid BattleRules::TrapMode value");
}
}
template <>
BattleRules::TrapMode enum_for_name<BattleRules::TrapMode>(const char* name) {
if (!strcmp(name, "DEFAULT")) {
return BattleRules::TrapMode::DEFAULT;
} else if (!strcmp(name, "ALL_PLAYERS")) {
return BattleRules::TrapMode::ALL_PLAYERS;
} else {
throw invalid_argument("invalid BattleRules::TrapMode name");
}
}
template <>
const char* name_for_enum<BattleRules::MesetaMode>(BattleRules::MesetaMode v) {
switch (v) {
case BattleRules::MesetaMode::ALLOW:
return "ALLOW";
case BattleRules::MesetaMode::FORBID_ALL:
return "FORBID_ALL";
case BattleRules::MesetaMode::CLEAR_AND_ALLOW:
return "CLEAR_AND_ALLOW";
default:
throw invalid_argument("invalid BattleRules::MesetaDropMode value");
}
}
template <>
BattleRules::MesetaMode enum_for_name<BattleRules::MesetaMode>(const char* name) {
if (!strcmp(name, "ALLOW")) {
return BattleRules::MesetaMode::ALLOW;
} else if (!strcmp(name, "FORBID_ALL")) {
return BattleRules::MesetaMode::FORBID_ALL;
} else if (!strcmp(name, "CLEAR_AND_ALLOW")) {
return BattleRules::MesetaMode::CLEAR_AND_ALLOW;
} else {
throw invalid_argument("invalid BattleRules::MesetaDropMode name");
}
}
const ChallengeTemplateDefinition& get_challenge_template_definition(uint32_t class_flags, size_t index) {
static auto make_template_item = +[](bool equipped, uint64_t first_data, uint64_t second_data = 0) -> PlayerInventoryItem {
PlayerInventoryItem ret = {
.present = 1,
.extension_data1 = 0,
.extension_data2 = 0,
.flags = (equipped ? 8 : 0),
.data = ItemData()};
ret.data.data1[0] = first_data >> 56;
ret.data.data1[1] = first_data >> 48;
ret.data.data1[2] = first_data >> 40;
ret.data.data1[3] = first_data >> 32;
ret.data.data1[4] = first_data >> 24;
ret.data.data1[5] = first_data >> 16;
ret.data.data1[6] = first_data >> 8;
ret.data.data1[7] = first_data >> 0;
ret.data.data1[8] = second_data >> 56;
ret.data.data1[9] = second_data >> 48;
ret.data.data1[10] = second_data >> 40;
ret.data.data1[11] = second_data >> 32;
ret.data.data2[0] = second_data >> 24;
ret.data.data2[1] = second_data >> 16;
ret.data.data2[2] = second_data >> 8;
ret.data.data2[3] = second_data >> 0;
return ret;
};
// clang-format off
static const vector<ChallengeTemplateDefinition> hunter_templates({
{0, {make_template_item(true, 0x0001000000000000, 0x0000000000000000), make_template_item(true, 0x0101000000000000, 0x0000000000000000), make_template_item(true, 0x02000500F4010000, 0x0000000028000012), make_template_item(false, 0x0300000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{4, {make_template_item(true, 0x0001000500000000, 0x0000000000000000), make_template_item(true, 0x0101010000000000, 0x0000000000000000), make_template_item(true, 0x0102000000000000, 0x0000000000000000), make_template_item(true, 0x02010D002003F401, 0x0000000028000012), make_template_item(false, 0x0300000000060000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{6, {make_template_item(true, 0x0002000000000000, 0x0000000000000000), make_template_item(true, 0x0101020000000000, 0x0000000000000000), make_template_item(true, 0x0102010000000000, 0x0000000000000000), make_template_item(true, 0x0201100020032003, 0x0000000028000012), make_template_item(false, 0x0300000000060000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{9, {make_template_item(true, 0x0002000500000000, 0x0000000000000000), make_template_item(true, 0x0101030000000000, 0x0000000000000000), make_template_item(true, 0x0102020000000000, 0x0000000000000000), make_template_item(true, 0x02011300E8032003, 0x0000640028000012), make_template_item(false, 0x0300000000080000, 0x0000000000000000), make_template_item(false, 0x0301000000020000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{12, {make_template_item(true, 0x0001010000000000, 0x0000000000000000), make_template_item(true, 0x0101030000000000, 0x0000000000000000), make_template_item(true, 0x0102030000000000, 0x0000000000000000), make_template_item(true, 0x020116004C04E803, 0x0000640028000012), make_template_item(false, 0x0300000000080000, 0x0000000000000000), make_template_item(false, 0x0300010000030000, 0x0000000000000000), make_template_item(false, 0x0301000000020000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{14, {make_template_item(true, 0x0001010500000000, 0x0000000000000000), make_template_item(true, 0x0101040000000000, 0x0000000000000000), make_template_item(true, 0x0102030000000000, 0x0000000000000000), make_template_item(true, 0x020118004C04E803, 0x6400C80028000012), make_template_item(false, 0x0300000000080000, 0x0000000000000000), make_template_item(false, 0x0300010000030000, 0x0000000000000000), make_template_item(false, 0x0301000000020000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{17, {make_template_item(true, 0x0002010000000000, 0x0000000000000000), make_template_item(true, 0x0101040000000000, 0x0000000000000000), make_template_item(true, 0x0102040000000000, 0x0000000000000000), make_template_item(true, 0x02012700DC056C07, 0xC8002C0128000012), make_template_item(false, 0x0300000000080000, 0x0000000000000000), make_template_item(false, 0x0300010000050000, 0x0000000000000000), make_template_item(false, 0x0301000000030000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{19, {make_template_item(true, 0x0002010500000000, 0x0000000000000000), make_template_item(true, 0x0101050000000000, 0x0000000000000000), make_template_item(true, 0x0102040000000000, 0x0000000000000000), make_template_item(true, 0x02012200DC057805, 0xC8002C0128000012), make_template_item(false, 0x0300000000080000, 0x0000000000000000), make_template_item(false, 0x0300010000050000, 0x0000000000000000), make_template_item(false, 0x0301000000030000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{22, {make_template_item(true, 0x0001020000000000, 0x0000000000000000), make_template_item(true, 0x0101050000000000, 0x0000000000000000), make_template_item(true, 0x0102050000000000, 0x0000000000000000), make_template_item(true, 0x020E260008071405, 0x2C01900128000012), make_template_item(false, 0x03000000000A0000, 0x0000000000000000), make_template_item(false, 0x0300010000050000, 0x0000000000000000), make_template_item(false, 0x0301000000030000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{24, {make_template_item(true, 0x0001030000000000, 0x0000000000000000), make_template_item(true, 0x0101070000000000, 0x0000000000000000), make_template_item(true, 0x0102070000000000, 0x0000000000000000), make_template_item(true, 0x02054600D007B80B, 0xE803E80328000012), make_template_item(false, 0x03000100000A0000, 0x0000000000000000), make_template_item(false, 0x0301010000050000, 0x0000000000000000), make_template_item(false, 0x0306010000050000, 0x0000000000000000), make_template_item(false, 0x0306000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{50, {make_template_item(true, 0x0001040000000000, 0x0000000000000000), make_template_item(true, 0x01010E0000000000, 0x0000000000000000), make_template_item(true, 0x01020E0000000000, 0x0000000000000000), make_template_item(true, 0x02058C00A00F7017, 0xD007D00728000012), make_template_item(false, 0x03000200000A0000, 0x0000000000000000), make_template_item(false, 0x0301020000050000, 0x0000000000000000), make_template_item(false, 0x0306010000050000, 0x0000000000000000), make_template_item(false, 0x0306000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{99, {make_template_item(true, 0x0001050000000000, 0x0000000000000000), make_template_item(true, 0x0101160000000000, 0x0000000000000000), make_template_item(true, 0x0102120000000000, 0x0000000000000000), make_template_item(true, 0x0205B40070177017, 0xB80BB80B28000012), make_template_item(false, 0x03000200000A0000, 0x0000000000000000), make_template_item(false, 0x0301020000050000, 0x0000000000000000), make_template_item(false, 0x0306010000050000, 0x0000000000000000), make_template_item(false, 0x0306000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{0, {make_template_item(true, 0x02000500F4010000, 0x0000000028000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{24, {make_template_item(true, 0x02054600D007B80B, 0xE803E80328000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{50, {make_template_item(true, 0x02058200A00F8813, 0xD007D00728000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{99, {make_template_item(true, 0x0205BE007017581B, 0xB80BB80B28000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
});
static const vector<ChallengeTemplateDefinition> ranger_templates({
{0, {make_template_item(true, 0x0006000000000000, 0x0000000000000000), make_template_item(true, 0x0101000000000000, 0x0000000000000000), make_template_item(true, 0x02000500F4010000, 0x0000000028000012), make_template_item(false, 0x0300000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{4, {make_template_item(true, 0x0006000500000000, 0x0000000000000000), make_template_item(true, 0x0101010000000000, 0x0000000000000000), make_template_item(true, 0x0102000000000000, 0x0000000000000000), make_template_item(true, 0x020D0C00F401C800, 0xF401000028000012), make_template_item(false, 0x0300000000050000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{5, {make_template_item(true, 0x0006000500000000, 0x0000000000000000), make_template_item(true, 0x0101010000000000, 0x0000000000000000), make_template_item(true, 0x0102010000000000, 0x0000000000000000), make_template_item(true, 0x020D0E00F401C800, 0xBC02000028000012), make_template_item(false, 0x0300000000050000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{8, {make_template_item(true, 0x0006000500000000, 0x0000000000000000), make_template_item(true, 0x0101020000000000, 0x0000000000000000), make_template_item(true, 0x0102020000000000, 0x0000000000000000), make_template_item(true, 0x020D1000F4012C01, 0x2003000028000012), make_template_item(false, 0x0300000000050000, 0x0000000000000000), make_template_item(false, 0x0301000000010000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{10, {make_template_item(true, 0x0006000500000000, 0x0000000000000000), make_template_item(true, 0x0101020000000000, 0x0000000000000000), make_template_item(true, 0x0102030000000000, 0x0000000000000000), make_template_item(true, 0x020D120058029001, 0x2003000028000012), make_template_item(false, 0x0300000000060000, 0x0000000000000000), make_template_item(false, 0x0300010000020000, 0x0000000000000000), make_template_item(false, 0x0301000000010000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{12, {make_template_item(true, 0x0006010000000000, 0x0000000000000000), make_template_item(true, 0x0101030000000000, 0x0000000000000000), make_template_item(true, 0x0102030000000000, 0x0000000000000000), make_template_item(true, 0x020D140058029001, 0x2003C80028000012), make_template_item(false, 0x0300000000060000, 0x0000000000000000), make_template_item(false, 0x0300010000020000, 0x0000000000000000), make_template_item(false, 0x0301000000010000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{14, {make_template_item(true, 0x0006010500000000, 0x0000000000000000), make_template_item(true, 0x0101040000000000, 0x0000000000000000), make_template_item(true, 0x0102040000000000, 0x0000000000000000), make_template_item(true, 0x020D1700BC02F401, 0x8403C80028000012), make_template_item(false, 0x0300000000070000, 0x0000000000000000), make_template_item(false, 0x0300010000030000, 0x0000000000000000), make_template_item(false, 0x0301000000020000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{15, {make_template_item(true, 0x0006010500000000, 0x0000000000000000), make_template_item(true, 0x0101040000000000, 0x0000000000000000), make_template_item(true, 0x0102040000000000, 0x0000000000000000), make_template_item(true, 0x020D190020035802, 0x8403C80028000012), make_template_item(false, 0x0300000000070000, 0x0000000000000000), make_template_item(false, 0x0300010000030000, 0x0000000000000000), make_template_item(false, 0x0301000000020000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{18, {make_template_item(true, 0x0006020000000000, 0x0000000000000000), make_template_item(true, 0x0101050000000000, 0x0000000000000000), make_template_item(true, 0x0102050000000000, 0x0000000000000000), make_template_item(true, 0x020D1E002003BC02, 0xB0042C0128000012), make_template_item(false, 0x0300000000070000, 0x0000000000000000), make_template_item(false, 0x0300010000050000, 0x0000000000000000), make_template_item(false, 0x0301000000030000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{24, {make_template_item(true, 0x0006030000000000, 0x0000000000000000), make_template_item(true, 0x0101070000000000, 0x0000000000000000), make_template_item(true, 0x0102070000000000, 0x0000000000000000), make_template_item(true, 0x020C4600D007E803, 0xB80BE80328000012), make_template_item(false, 0x0300010000050000, 0x0000000000000000), make_template_item(false, 0x0301010000030000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{50, {make_template_item(true, 0x0006040000000000, 0x0000000000000000), make_template_item(true, 0x01010E0000000000, 0x0000000000000000), make_template_item(true, 0x01020E0000000000, 0x0000000000000000), make_template_item(true, 0x020C8C00B80BC409, 0x7017C40928000012), make_template_item(false, 0x0300020000050000, 0x0000000000000000), make_template_item(false, 0x0301020000030000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{99, {make_template_item(true, 0x0006050000000000, 0x0000000000000000), make_template_item(true, 0x0101160000000000, 0x0000000000000000), make_template_item(true, 0x0102120000000000, 0x0000000000000000), make_template_item(true, 0x0206B400B80BB80B, 0x2823B80B28000012), make_template_item(false, 0x0300020000080000, 0x0000000000000000), make_template_item(false, 0x0301020000050000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0308000000050000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{0, {make_template_item(true, 0x02000500F4010000, 0x0000000028000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{24, {make_template_item(true, 0x020C4600D007E803, 0xB80BE80328000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{50, {make_template_item(true, 0x020C8C00B80BC409, 0x7017C40928000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{99, {make_template_item(true, 0x0206B400B80BB80B, 0x2823B80B28000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
});
static const vector<ChallengeTemplateDefinition> force_templates({
{0, {make_template_item(true, 0x000A000000000000, 0x0000000000000000), make_template_item(true, 0x0101000000000000, 0x0000000000000000), make_template_item(true, 0x02000500F4010000, 0x0000000028000012), make_template_item(false, 0x0300000000040000, 0x0000000000000000), make_template_item(false, 0x0301000000040000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{4, {make_template_item(true, 0x000A000500000000, 0x0000000000000000), make_template_item(true, 0x0101000000000000, 0x0000000000000000), make_template_item(true, 0x0102000000000000, 0x0000000000000000), make_template_item(true, 0x02190D0020036400, 0x0000900128000012), make_template_item(false, 0x0300000000060000, 0x0000000000000000), make_template_item(false, 0x0301000000060000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 2}, {0x03, 2}, {0x0D, 2}, {0x0A, 2}}},
{6, {make_template_item(true, 0x000B000000000000, 0x0000000000000000), make_template_item(true, 0x0101000000000000, 0x0000000000000000), make_template_item(true, 0x0102000000000000, 0x0000000000000000), make_template_item(true, 0x02190F002003C800, 0x0000F40128000012), make_template_item(false, 0x0300000000060000, 0x0000000000000000), make_template_item(false, 0x0301000000060000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 2}, {0x03, 2}, {0x0D, 2}, {0x0A, 2}}},
{9, {make_template_item(true, 0x000B000500000000, 0x0000000000000000), make_template_item(true, 0x0101000000000000, 0x0000000000000000), make_template_item(true, 0x0102000000000000, 0x0000000000000000), make_template_item(true, 0x0219120084032C01, 0x0000580228000012), make_template_item(false, 0x0300000000060000, 0x0000000000000000), make_template_item(false, 0x0301000000060000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 2}, {0x03, 2}, {0x0D, 2}, {0x0A, 2}}},
{11, {make_template_item(true, 0x000B000500000000, 0x0000000000000000), make_template_item(true, 0x0101000000000000, 0x0000000000000000), make_template_item(true, 0x0102000000000000, 0x0000000000000000), make_template_item(true, 0x02191400E8032C01, 0x0000BC0228000012), make_template_item(false, 0x0300000000060000, 0x0000000000000000), make_template_item(false, 0x0300010000020000, 0x0000000000000000), make_template_item(false, 0x0301000000080000, 0x0000000000000000), make_template_item(false, 0x0301010000030000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 4}, {0x06, 4}, {0x03, 4}, {0x0D, 2}, {0x0A, 2}, {0x0B, 2}, {0x0C, 2}}},
{12, {make_template_item(true, 0x000B000500000000, 0x0000000000000000), make_template_item(true, 0x0101030000000000, 0x0000000000000000), make_template_item(true, 0x0102000000000000, 0x0000000000000000), make_template_item(true, 0x02191600E8039001, 0x6400BC0228000012), make_template_item(false, 0x0300000000070000, 0x0000000000000000), make_template_item(false, 0x0300010000020000, 0x0000000000000000), make_template_item(false, 0x0301000000070000, 0x0000000000000000), make_template_item(false, 0x0301010000030000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 4}, {0x06, 4}, {0x03, 4}, {0x0D, 2}, {0x0A, 2}, {0x0B, 2}, {0x0C, 2}}},
{15, {make_template_item(true, 0x000B000A00000000, 0x0000000000000000), make_template_item(true, 0x0101040000000000, 0x0000000000000000), make_template_item(true, 0x0102040000000000, 0x0000000000000000), make_template_item(true, 0x02191B00B004F401, 0xC800200328000012), make_template_item(false, 0x0300000000070000, 0x0000000000000000), make_template_item(false, 0x0300010000030000, 0x0000000000000000), make_template_item(false, 0x0301000000080000, 0x0000000000000000), make_template_item(false, 0x0301010000040000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 6}, {0x06, 6}, {0x03, 6}, {0x01, 0}, {0x04, 0}, {0x0D, 3}, {0x0A, 3}, {0x0B, 3}, {0x0C, 3}, {0x0F, 2}}},
{16, {make_template_item(true, 0x000B000A00000000, 0x0000000000000000), make_template_item(true, 0x0101040000000000, 0x0000000000000000), make_template_item(true, 0x0102040000000000, 0x0000000000000000), make_template_item(true, 0x02191D00B0045802, 0xC800840328000012), make_template_item(false, 0x0300000000080000, 0x0000000000000000), make_template_item(false, 0x0300010000030000, 0x0000000000000000), make_template_item(false, 0x03010000000A0000, 0x0000000000000000), make_template_item(false, 0x0301010000040000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 6}, {0x06, 6}, {0x03, 6}, {0x01, 0}, {0x04, 0}, {0x0D, 3}, {0x0A, 3}, {0x0B, 3}, {0x0C, 3}, {0x0F, 2}}},
{19, {make_template_item(true, 0x000A010000000000, 0x0000000000000000), make_template_item(true, 0x0101040000000000, 0x0000000000000000), make_template_item(true, 0x0102040000000000, 0x0000000000000000), make_template_item(true, 0x02192200DC05BC02, 0xC800E80328000012), make_template_item(false, 0x0300000000080000, 0x0000000000000000), make_template_item(false, 0x0300010000050000, 0x0000000000000000), make_template_item(false, 0x0301010000050000, 0x0000000000000000), make_template_item(false, 0x03010000000A0000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 6}, {0x06, 6}, {0x03, 6}, {0x01, 0}, {0x04, 0}, {0x0D, 3}, {0x0A, 3}, {0x0B, 3}, {0x0C, 3}, {0x0F, 2}}},
{24, {make_template_item(true, 0x000A010A00000000, 0x0000000000000000), make_template_item(true, 0x0101060000000000, 0x0000000000000000), make_template_item(true, 0x0102060000000000, 0x0000000000000000), make_template_item(true, 0x021C4600D007E803, 0xE803B80B28000012), make_template_item(false, 0x0300010000050000, 0x0000000000000000), make_template_item(false, 0x0301010000080000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 7}, {0x06, 7}, {0x03, 7}, {0x01, 4}, {0x04, 4}, {0x0D, 7}, {0x0A, 7}, {0x0B, 7}, {0x0C, 7}, {0x0F, 6}}},
{50, {make_template_item(true, 0x000A020000000000, 0x0000000000000000), make_template_item(true, 0x01010E0000000000, 0x0000000000000000), make_template_item(true, 0x01020D0000000000, 0x0000000000000000), make_template_item(true, 0x021C8C00B80BD007, 0xD007581B28000012), make_template_item(false, 0x0300020000050000, 0x0000000000000000), make_template_item(false, 0x0301020000080000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 9}, {0x06, 9}, {0x03, 9}, {0x01, 9}, {0x04, 9}, {0x0D, 9}, {0x0A, 9}, {0x0B, 9}, {0x0C, 9}, {0x0F, 9}}},
{99, {make_template_item(true, 0x000A040000000000, 0x0000000000000000), make_template_item(true, 0x0101160000000000, 0x0000000000000000), make_template_item(true, 0x0102110000000000, 0x0000000000000000), make_template_item(true, 0x021CB400AC0DD007, 0xC409102728000012), make_template_item(false, 0x0300020000050000, 0x0000000000000000), make_template_item(false, 0x03010200000A0000, 0x0000000000000000), make_template_item(false, 0x0306010000030000, 0x0000000000000000), make_template_item(false, 0x0306000000030000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {{0x00, 14}, {0x06, 14}, {0x03, 14}, {0x01, 14}, {0x04, 14}, {0x0D, 14}, {0x0A, 14}, {0x0B, 14}, {0x0C, 14}, {0x0F, 14}}},
{0, {make_template_item(true, 0x02000500F4010000, 0x0000000028000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{24, {make_template_item(true, 0x021C4600D007E803, 0xE803B80B28000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{50, {make_template_item(true, 0x021C8C00B80BD007, 0xD007581B28000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
{99, {make_template_item(true, 0x021CB400AC0DD007, 0xC409102728000012), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000), make_template_item(false, 0x0309000000000000, 0x0000000000000000)}, {}},
});
// clang-format on
if ((class_flags & 0xE0) == 0x20) {
return hunter_templates.at(index);
} else if ((class_flags & 0xE0) == 0x40) {
return ranger_templates.at(index);
} else if ((class_flags & 0xE0) == 0x80) {
return force_templates.at(index);
} else {
throw runtime_error("invalid class flags on original player");
}
}
+578
View File
@@ -0,0 +1,578 @@
#pragma once
#include <inttypes.h>
#include <stddef.h>
#include <array>
#include <phosg/Encoding.hh>
#include <phosg/JSON.hh>
#include <string>
#include <utility>
#include <vector>
#include "FileContentsCache.hh"
#include "ItemData.hh"
#include "LevelTable.hh"
#include "Text.hh"
#include "Version.hh"
extern FileContentsCache player_files_cache;
// PSO V2 stored some extra data in the character structs in a format that I'm
// sure Sega thought was very clever for backward compatibility, but for us is
// just plain annoying. Specifically, they used the third and fourth bytes of
// the InventoryItem struct to store some things not present in V1. The game
// stores arrays of bytes striped across these structures. In newserv, we call
// those fields extension_data. They contain:
// items[0].extension_data1 through items[19].extension_data1:
// Extended technique levels. The values in the technique_levels_v1 array
// only go up to 14 (tech level 15); if the player has a technique above
// level 15, the corresponding extension_data1 field holds the remaining
// levels (so a level 20 tech would have 14 in technique_levels_v1 and 5
// in the corresponding item's extension_data1 field).
// items[0].extension_data2 through items[3].extension_data2:
// The flags field from the PSOGCCharacterFile::Character struct; see
// SaveFileFormats.hh for details.
// items[4].extension_data2 through items[7].extension_data2:
// The timestamp when the character was last saved, in seconds since
// January 1, 2000. Stored little-endian, so items[4] contains the LSB.
// items[8].extension_data2 through items[12].extension_data2:
// Number of power materials, mind materials, evade materials, def
// materials, and luck materials (respectively) used by the player.
// items[13].extension_data2 through items[15].extension_data2:
// Unknown. These are not an array, but do appear to be related.
struct PlayerInventoryItem {
/* 00 */ le_uint16_t present = 0;
// See note above about these fields
/* 02 */ uint8_t extension_data1 = 0;
/* 03 */ uint8_t extension_data2 = 0;
/* 04 */ le_uint32_t flags = 0; // 8 = equipped
/* 08 */ ItemData data;
/* 1C */
} __attribute__((packed));
struct PlayerBankItem {
/* 00 */ ItemData data;
/* 14 */ le_uint16_t amount = 0;
/* 16 */ le_uint16_t present = 0;
/* 18 */
} __attribute__((packed));
struct PlayerInventory {
/* 0000 */ uint8_t num_items = 0;
/* 0001 */ uint8_t hp_materials_used = 0;
/* 0002 */ uint8_t tp_materials_used = 0;
/* 0003 */ uint8_t language = 1; // English
/* 0004 */ parray<PlayerInventoryItem, 30> items;
/* 034C */
PlayerInventory();
size_t find_item(uint32_t item_id) const;
size_t find_item_by_primary_identifier(uint32_t primary_identifier) const;
size_t find_equipped_weapon() const;
size_t find_equipped_armor() const;
size_t find_equipped_mag() const;
size_t remove_all_items_of_type(uint8_t data0, int16_t data1 = -1);
void decode_mags(GameVersion version);
void encode_mags(GameVersion version);
} __attribute__((packed));
struct PlayerBank {
/* 0000 */ le_uint32_t num_items = 0;
/* 0004 */ le_uint32_t meseta = 0;
/* 0008 */ parray<PlayerBankItem, 200> items;
/* 12C8 */
void load(const std::string& filename);
void save(const std::string& filename, bool save_to_filesystem) const;
bool switch_with_file(const std::string& save_filename,
const std::string& load_filename);
void add_item(const ItemData& item);
ItemData remove_item(uint32_t item_id, uint32_t amount);
size_t find_item(uint32_t item_id);
} __attribute__((packed));
struct PlayerDispDataBB;
struct PlayerVisualConfig {
/* 00 */ ptext<char, 0x10> name;
/* 10 */ parray<uint8_t, 8> unknown_a2;
/* 18 */ le_uint32_t name_color = 0x00000000; // RGBA
/* 1C */ uint8_t extra_model = 0;
/* 1D */ parray<uint8_t, 0x0F> unused;
/* 2C */ le_uint32_t unknown_a3 = 0;
/* 30 */ uint8_t section_id = 0;
/* 31 */ uint8_t char_class = 0;
/* 32 */ uint8_t v2_flags = 0;
/* 33 */ uint8_t version = 0;
// class_flags specifies features of the character's class. The bits are:
// -------- -------- -------- FRHANMfm
// F = force, R = ranger, H = hunter
// A = android, N = newman, M = human
// f = female, m = male
/* 34 */ le_uint32_t class_flags = 0;
/* 38 */ le_uint16_t costume = 0;
/* 3A */ le_uint16_t skin = 0;
/* 3C */ le_uint16_t face = 0;
/* 3E */ le_uint16_t head = 0;
/* 40 */ le_uint16_t hair = 0;
/* 42 */ le_uint16_t hair_r = 0;
/* 44 */ le_uint16_t hair_g = 0;
/* 46 */ le_uint16_t hair_b = 0;
/* 48 */ le_float proportion_x = 0.0;
/* 4C */ le_float proportion_y = 0.0;
/* 50 */
} __attribute__((packed));
struct PlayerDispDataDCPCV3 {
/* 00 */ PlayerStats stats;
/* 24 */ PlayerVisualConfig visual;
/* 74 */ parray<uint8_t, 0x48> config;
/* BC */ parray<uint8_t, 0x14> technique_levels_v1;
/* D0 */
void enforce_lobby_join_limits(GameVersion target_version);
PlayerDispDataBB to_bb() const;
} __attribute__((packed));
struct PlayerDispDataBBPreview {
/* 00 */ le_uint32_t experience = 0;
/* 04 */ le_uint32_t level = 0;
// The name field in this structure is used for the player's Guild Card
// number, apparently (possibly because it's a char array and this is BB)
/* 08 */ PlayerVisualConfig visual;
/* 58 */ ptext<char16_t, 0x10> name;
/* 78 */ uint32_t play_time = 0;
/* 7C */
} __attribute__((packed));
// BB player appearance and stats data
struct PlayerDispDataBB {
/* 0000 */ PlayerStats stats;
/* 0024 */ PlayerVisualConfig visual;
/* 0074 */ ptext<char16_t, 0x0C> name;
/* 008C */ le_uint32_t play_time = 0;
/* 0090 */ uint32_t unknown_a3 = 0;
/* 0094 */ parray<uint8_t, 0xE8> config;
/* 017C */ parray<uint8_t, 0x14> technique_levels_v1;
/* 0190 */
void enforce_lobby_join_limits(GameVersion target_version);
PlayerDispDataDCPCV3 to_dcpcv3() const;
PlayerDispDataBBPreview to_preview() const;
void apply_preview(const PlayerDispDataBBPreview&);
void apply_dressing_room(const PlayerDispDataBBPreview&);
} __attribute__((packed));
struct GuildCardPC {
/* 00 */ le_uint32_t player_tag = 0;
/* 04 */ le_uint32_t guild_card_number = 0;
// TODO: Is the length of the name field correct here?
/* 08 */ ptext<char16_t, 0x18> name;
/* 38 */ ptext<char16_t, 0x5A> description;
/* EC */ uint8_t present = 0;
/* ED */ uint8_t language = 0;
/* EE */ uint8_t section_id = 0;
/* EF */ uint8_t char_class = 0;
/* F0 */
} __attribute__((packed));
// TODO: Is this the same for XB as it is for GC? (This struct is based on the
// GC format)
struct GuildCardV3 {
/* 00 */ le_uint32_t player_tag = 0;
/* 04 */ le_uint32_t guild_card_number = 0;
/* 08 */ ptext<char, 0x18> name;
/* 20 */ ptext<char, 0x6C> description;
/* 8C */ uint8_t present = 0;
/* 8D */ uint8_t language = 0;
/* 8E */ uint8_t section_id = 0;
/* 8F */ uint8_t char_class = 0;
/* 90 */
} __attribute__((packed));
// BB guild card format
struct GuildCardBB {
/* 0000 */ le_uint32_t guild_card_number = 0;
/* 0004 */ ptext<char16_t, 0x18> name;
/* 0034 */ ptext<char16_t, 0x10> team_name;
/* 0054 */ ptext<char16_t, 0x58> description;
/* 0104 */ uint8_t present = 0;
/* 0105 */ uint8_t language = 0;
/* 0106 */ uint8_t section_id = 0;
/* 0107 */ uint8_t char_class = 0;
/* 0108 */
void clear();
} __attribute__((packed));
// an entry in the BB guild card file
struct GuildCardEntryBB {
GuildCardBB data;
ptext<char16_t, 0x58> comment;
parray<uint8_t, 0x4> unknown_a1;
void clear();
} __attribute__((packed));
// the format of the BB guild card file
struct GuildCardFileBB {
parray<uint8_t, 0x114> unknown_a1;
GuildCardBB blocked[0x1C];
parray<uint8_t, 0x180> unknown_a2;
GuildCardEntryBB entries[0x69];
uint32_t checksum() const;
} __attribute__((packed));
struct KeyAndTeamConfigBB {
parray<uint8_t, 0x0114> unknown_a1; // 0000
parray<uint8_t, 0x016C> key_config; // 0114
parray<uint8_t, 0x0038> joystick_config; // 0280
le_uint32_t guild_card_number = 0; // 02B8
le_uint32_t team_id = 0; // 02BC
le_uint64_t team_info = 0; // 02C0
le_uint16_t team_privilege_level = 0; // 02C8
le_uint16_t reserved = 0; // 02CA
ptext<char16_t, 0x0010> team_name; // 02CC
parray<uint8_t, 0x0800> team_flag; // 02EC
le_uint32_t team_rewards = 0; // 0AEC
} __attribute__((packed));
struct PlayerLobbyDataPC {
le_uint32_t player_tag = 0;
le_uint32_t guild_card = 0;
// There's a strange behavior (bug? "feature"?) in Episode 3 where the start
// button does nothing in the lobby (hence you can't "quit game") if the
// client's IP address is zero. So, we fill it in with a fake nonzero value to
// avoid this behavior, and to be consistent, we make IP addresses fake and
// nonzero on all other versions too.
be_uint32_t ip_address = 0x7F000001;
le_uint32_t client_id = 0;
ptext<char16_t, 0x10> name;
void clear();
} __attribute__((packed));
struct PlayerLobbyDataDCGC {
le_uint32_t player_tag = 0;
le_uint32_t guild_card = 0;
be_uint32_t ip_address = 0x7F000001;
le_uint32_t client_id = 0;
ptext<char, 0x10> name;
void clear();
} __attribute__((packed));
struct XBNetworkLocation {
le_uint32_t internal_ipv4_address = 0x0A0A0A0A;
le_uint32_t external_ipv4_address = 0x23232323;
le_uint16_t port = 9100;
parray<uint8_t, 6> mac_address = 0x77;
parray<le_uint32_t, 2> unknown_a1;
le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF;
parray<le_uint32_t, 4> unknown_a2;
void clear();
} __attribute__((packed));
struct PlayerLobbyDataXB {
le_uint32_t player_tag = 0;
le_uint32_t guild_card = 0;
XBNetworkLocation netloc;
le_uint32_t client_id = 0;
ptext<char, 0x10> name;
void clear();
} __attribute__((packed));
struct PlayerLobbyDataBB {
le_uint32_t player_tag = 0;
le_uint32_t guild_card = 0;
// This field is a guess; the official builds didn't use this, but all other
// versions have it
be_uint32_t ip_address = 0x7F000001;
parray<uint8_t, 0x10> unknown_a1;
le_uint32_t client_id = 0;
ptext<char16_t, 0x10> name;
le_uint32_t unknown_a2 = 0;
void clear();
} __attribute__((packed));
template <bool IsWideChar>
struct PlayerRecordsDCPC_Challenge {
using CharT = typename std::conditional<IsWideChar, char16_t, char>::type;
/* 00 */ le_uint16_t title_color = 0x7FFF;
/* 02 */ parray<uint8_t, 2> unknown_u0;
/* 04 */ ptext<CharT, 0x0C> rank_title; // Encrypted; see decrypt_challenge_rank_text
/* 10 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time. TODO: This might be offline times
/* 34 */ le_uint16_t unknown_g3 = 0;
/* 36 */ le_uint16_t grave_deaths = 0;
/* 38 */ parray<le_uint32_t, 5> grave_coords_time;
/* 4C */ ptext<CharT, 0x14> grave_team;
/* 60 */ ptext<CharT, 0x18> grave_message;
/* 78 */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time. TODO: This might be online times
/* 9C */ parray<uint8_t, 4> unknown_l4;
/* A0 */
} __attribute__((packed));
struct PlayerRecordsDC_Challenge : PlayerRecordsDCPC_Challenge<false> {
} __attribute__((packed));
struct PlayerRecordsPC_Challenge : PlayerRecordsDCPC_Challenge<true> {
} __attribute__((packed));
template <bool IsBigEndian>
struct PlayerRecordsV3_Challenge {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::type;
// Offsets are (1) relative to start of C5 entry, and (2) relative to start
// of save file structure
struct Stats {
/* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555
/* 02:1E */ parray<uint8_t, 2> unknown_u0;
/* 04:20 */ parray<U32T, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time
/* 28:44 */ parray<U32T, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time
/* 3C:58 */ parray<U32T, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time
/* 60:7C */ parray<uint8_t, 4> unknown_g3;
/* 64:80 */ U16T grave_deaths = 0;
/* 66:82 */ parray<uint8_t, 2> unknown_u4;
/* 68:84 */ parray<U32T, 5> grave_coords_time;
/* 7C:98 */ ptext<char, 0x14> grave_team;
/* 90:AC */ ptext<char, 0x20> grave_message;
/* B0:CC */ parray<uint8_t, 4> unknown_m5;
/* B4:D0 */ parray<U32T, 9> unknown_t6;
/* D8:F4 */
} __attribute__((packed));
/* 0000:001C */ Stats stats;
// On Episode 3, there are special cases that apply to this field - if the
// text ends with certain strings (after decrypt_challenge_rank_text), the
// player will have particle effects emanate from their character in the
// lobby every 2 seconds. These effects are:
// Ends with ":GOD" => blue circle
// Ends with ":KING" => white particles
// Ends with ":LORD" => rising yellow sparkles
// Ends with ":CHAMP" => green circle
/* 00D8:00F4 */ ptext<char, 0x0C> rank_title;
/* 00E4:0100 */ parray<uint8_t, 0x1C> unknown_l7;
/* 0100:011C */
} __attribute__((packed));
struct PlayerRecordsBB_Challenge {
/* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555
/* 0002 */ parray<uint8_t, 2> unknown_u0;
/* 0004 */ parray<le_uint32_t, 9> times_ep1_online; // Encrypted; see decrypt_challenge_time
/* 0028 */ parray<le_uint32_t, 5> times_ep2_online; // Encrypted; see decrypt_challenge_time
/* 003C */ parray<le_uint32_t, 9> times_ep1_offline; // Encrypted; see decrypt_challenge_time
/* 0060 */ parray<uint8_t, 4> unknown_g3;
/* 0064 */ le_uint16_t grave_deaths = 0;
/* 0066 */ parray<uint8_t, 2> unknown_u4;
/* 0068 */ parray<le_uint32_t, 5> grave_coords_time;
/* 007C */ ptext<char16_t, 0x14> grave_team;
/* 00A4 */ ptext<char16_t, 0x20> grave_message;
/* 00E4 */ parray<uint8_t, 4> unknown_m5;
/* 00E8 */ parray<le_uint32_t, 9> unknown_t6;
/* 010C */ ptext<char16_t, 0x0C> rank_title; // Encrypted; see decrypt_challenge_rank_text
/* 0124 */ parray<uint8_t, 0x1C> unknown_l7;
/* 0140 */
PlayerRecordsBB_Challenge() = default;
PlayerRecordsBB_Challenge(const PlayerRecordsBB_Challenge& other) = default;
PlayerRecordsBB_Challenge& operator=(const PlayerRecordsBB_Challenge& other) = default;
PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec);
PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec);
PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge<false>& rec);
operator PlayerRecordsDC_Challenge() const;
operator PlayerRecordsPC_Challenge() const;
operator PlayerRecordsV3_Challenge<false>() const;
} __attribute__((packed));
template <bool IsBigEndian>
struct PlayerRecords_Battle {
using U16T = typename std::conditional<IsBigEndian, be_uint16_t, le_uint16_t>::type;
// On Episode 3, place_counts[0] is win count and [1] is loss count
/* 00 */ parray<U16T, 4> place_counts;
/* 08 */ U16T disconnect_count = 0;
/* 0A */ parray<uint16_t, 3> unknown_a1;
/* 10 */ parray<uint32_t, 2> unknown_a2;
/* 18 */
} __attribute__((packed));
template <typename ItemIDT>
struct ChoiceSearchConfig {
// 0 = enabled, 1 = disabled. Unused for command C3
le_uint32_t choice_search_disabled = 0;
struct Entry {
ItemIDT parent_category_id = 0;
ItemIDT category_id = 0;
} __attribute__((packed));
parray<Entry, 5> entries;
} __attribute__((packed));
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 PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3>(
const PlayerDispDataDCPCV3& src) {
return src;
}
template <>
inline PlayerDispDataDCPCV3 convert_player_disp_data<PlayerDispDataDCPCV3, PlayerDispDataBB>(
const PlayerDispDataBB& src) {
return src.to_dcpcv3();
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB, PlayerDispDataDCPCV3>(
const PlayerDispDataDCPCV3& src) {
return src.to_bb();
}
template <>
inline PlayerDispDataBB convert_player_disp_data<PlayerDispDataBB>(
const PlayerDispDataBB& src) {
return src;
}
struct BattleRules {
enum class TechDiskMode : uint8_t {
ALLOW = 0,
FORBID_ALL = 1,
LIMIT_LEVEL = 2,
};
enum class WeaponAndArmorMode : uint8_t {
ALLOW = 0,
CLEAR_AND_ALLOW = 1,
FORBID_ALL = 2,
FORBID_RARES = 3,
};
enum class MagMode : uint8_t {
ALLOW = 0,
FORBID_ALL = 1,
};
enum class ToolMode : uint8_t {
ALLOW = 0,
CLEAR_AND_ALLOW = 1,
FORBID_ALL = 2,
};
enum class TrapMode : uint8_t {
DEFAULT = 0,
ALL_PLAYERS = 1,
};
enum class MesetaMode : uint8_t {
ALLOW = 0,
FORBID_ALL = 1,
CLEAR_AND_ALLOW = 2,
};
// Set by quest opcode F812, but values are remapped.
// F812 00 => FORBID_ALL
// F812 01 => ALLOW
// F812 02 => LIMIT_LEVEL
/* 00 */ TechDiskMode tech_disk_mode = TechDiskMode::ALLOW;
// Set by quest opcode F813, but values are remapped.
// F813 00 => FORBID_ALL
// F813 01 => ALLOW
// F813 02 => CLEAR_AND_ALLOW
// F813 03 => FORBID_RARES
/* 01 */ WeaponAndArmorMode weapon_and_armor_mode = WeaponAndArmorMode::ALLOW;
// Set by quest opcode F814, but values are remapped.
// F814 00 => FORBID_ALL
// F814 01 => ALLOW
/* 02 */ MagMode mag_mode = MagMode::ALLOW;
// Set by quest opcode F815, but values are remapped.
// F815 00 => FORBID_ALL
// F815 01 => ALLOW
// F815 02 => CLEAR_AND_ALLOW
/* 03 */ ToolMode tool_mode = ToolMode::ALLOW;
// Set by quest opcode F816. Values are not remapped.
// F816 00 => DEFAULT
// F816 01 => ALL_PLAYERS
/* 04 */ TrapMode trap_mode = TrapMode::DEFAULT;
// Set by quest opcode F817. Value appears to be unused in all PSO versions.
/* 05 */ uint8_t unused_F817 = 0;
// Set by quest opcode F818, but values are remapped.
// F818 00 => 01
// F818 01 => 00
// F818 02 => 02
// TODO: Define an enum class for this field.
/* 06 */ uint8_t respawn_mode = 0;
// Set by quest opcode F819.
/* 07 */ uint8_t replace_char = 0;
// Set by quest opcode F81A, but value is inverted.
/* 08 */ uint8_t drop_weapon = 0;
// Set by quest opcode F81B.
/* 09 */ uint8_t is_teams = 0;
// Set by quest opcode F852.
/* 0A */ uint8_t hide_target_reticle = 0;
// Set by quest opcode F81E. Values are not remapped.
// F81E 00 => ALLOW
// F81E 01 => FORBID_ALL
// F81E 02 => CLEAR_AND_ALLOW
/* 0B */ MesetaMode meseta_mode = MesetaMode::ALLOW;
// Set by quest opcode F81D.
/* 0C */ uint8_t death_level_up = 0;
// Set by quest opcode F851. The trap type is remapped:
// F851 00 XX => set count to XX for trap type 00
// F851 01 XX => set count to XX for trap type 02
// F851 02 XX => set count to XX for trap type 03
// F851 03 XX => set count to XX for trap type 01
/* 0D */ parray<uint8_t, 4> trap_counts;
// Set by quest opcode F85E.
/* 11 */ uint8_t enable_sonar = 0;
// Set by quest opcode F85F.
/* 12 */ uint8_t sonar_count = 0;
// Set by quest opcode F89E.
/* 13 */ uint8_t forbid_scape_dolls = 0;
// This value does not appear to be set by any quest opcode.
/* 14 */ le_uint32_t unknown_a1 = 0;
// Set by quest opcode F86F.
/* 18 */ le_uint32_t lives = 0;
// Set by quest opcode F870.
/* 1C */ le_uint32_t max_tech_level = 0;
// Set by quest opcode F871.
/* 20 */ le_uint32_t char_level = 0;
// Set by quest opcode F872.
/* 24 */ le_uint32_t time_limit = 0;
// Set by quest opcode F8A8.
/* 28 */ le_uint16_t death_tech_level_up = 0;
/* 2A */ parray<uint8_t, 2> unused;
// Set by quest opcode F86B.
/* 2C */ le_uint32_t box_drop_area = 0;
/* 30 */
BattleRules() = default;
explicit BattleRules(const JSON& json);
JSON json() const;
bool operator==(const BattleRules& other) const = default;
bool operator!=(const BattleRules& other) const = default;
} __attribute__((packed));
struct ChallengeTemplateDefinition {
uint32_t level;
std::vector<PlayerInventoryItem> items;
struct TechLevel {
uint8_t tech_num;
uint8_t level;
};
std::vector<TechLevel> tech_levels;
};
const ChallengeTemplateDefinition& get_challenge_template_definition(uint32_t class_flags, size_t index);
-22
View File
@@ -1,22 +0,0 @@
#pragma once
#include <stdint.h>
#include <string>
#include <unordered_map>
// product_is_valid_slow is Sega's implementation; product_is_valid_fast
// produces identical results but is about 7000 times faster.
bool product_is_valid_slow(
const std::string& s, uint8_t domain, uint8_t subdomain = 0xFF);
bool product_is_valid_fast(
const std::string& s, uint8_t domain, uint8_t subdomain = 0xFF);
bool product_is_valid_fast(
uint32_t product, uint8_t domain, uint8_t subdomain = 0xFF);
bool decoded_product_is_valid_fast(
uint32_t product, uint8_t domain, uint8_t subdomain = 0xFF);
std::string generate_product(uint8_t domain, uint8_t subdomain = 0xFF);
std::unordered_map<uint32_t, std::string> generate_all_products(uint8_t domain = 0xFF, uint8_t subdomain = 0xFF);
void product_speed_test(uint64_t seed = 0xFFFFFFFFFFFFFFFF);
+503 -534
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -8,8 +8,7 @@
#include "ServerState.hh"
void on_proxy_command(
std::shared_ptr<ServerState> s,
ProxyServer::LinkedSession& session,
std::shared_ptr<ProxyServer::LinkedSession> ses,
bool from_server,
uint16_t command,
uint32_t flag,
+129 -109
View File
@@ -145,31 +145,31 @@ void ProxyServer::on_client_connect(
this->next_unlicensed_session_id = 0xFF00000000000001;
}
auto emplace_ret = this->id_to_session.emplace(session_id, new LinkedSession(this, session_id, listen_port, version, *default_destination));
auto emplace_ret = this->id_to_session.emplace(session_id, new LinkedSession(this->shared_from_this(), session_id, listen_port, version, *default_destination));
if (!emplace_ret.second) {
throw logic_error("linked session already exists for unlicensed client");
}
auto session = emplace_ret.first->second;
session->log.info("Opened linked session");
auto ses = emplace_ret.first->second;
ses->log.info("Opened linked session");
Channel ch(bev, version, nullptr, nullptr, session.get(), "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN);
session->resume(std::move(ch));
Channel ch(bev, version, nullptr, nullptr, ses.get(), "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN);
ses->resume(std::move(ch));
// If no default destination exists, or the client is not a patch client,
// create an unlinked session - we'll have to get the destination from the
// client's config, which we'll get via a 9E command soon.
} else {
auto emplace_ret = this->bev_to_unlinked_session.emplace(bev, new UnlinkedSession(this, bev, listen_port, version));
auto emplace_ret = this->bev_to_unlinked_session.emplace(bev, new UnlinkedSession(this->shared_from_this(), bev, listen_port, version));
if (!emplace_ret.second) {
throw logic_error("stale unlinked session exists");
}
auto session = emplace_ret.first->second;
auto ses = emplace_ret.first->second;
proxy_server_log.info("Opened unlinked session");
// Note that this should only be set when the linked session is created, not
// when it is resumed!
if (default_destination) {
session->next_destination = *default_destination;
ses->next_destination = *default_destination;
}
switch (version) {
@@ -183,13 +183,13 @@ void ProxyServer::on_client_connect(
uint32_t client_key = random_object<uint32_t>();
auto cmd = prepare_server_init_contents_console(
server_key, client_key, 0);
session->channel.send(0x02, 0x00, &cmd, sizeof(cmd));
ses->channel.send(0x02, 0x00, &cmd, sizeof(cmd));
if ((version == GameVersion::DC) || (version == GameVersion::PC)) {
session->channel.crypt_out.reset(new PSOV2Encryption(server_key));
session->channel.crypt_in.reset(new PSOV2Encryption(client_key));
ses->channel.crypt_out.reset(new PSOV2Encryption(server_key));
ses->channel.crypt_in.reset(new PSOV2Encryption(client_key));
} else {
session->channel.crypt_out.reset(new PSOV3Encryption(server_key));
session->channel.crypt_in.reset(new PSOV3Encryption(client_key));
ses->channel.crypt_out.reset(new PSOV3Encryption(server_key));
ses->channel.crypt_in.reset(new PSOV3Encryption(client_key));
}
break;
}
@@ -199,15 +199,15 @@ void ProxyServer::on_client_connect(
random_data(server_key.data(), server_key.bytes());
random_data(client_key.data(), client_key.bytes());
auto cmd = prepare_server_init_contents_bb(server_key, client_key, 0);
session->channel.send(0x03, 0x00, &cmd, sizeof(cmd));
session->detector_crypt.reset(new PSOBBMultiKeyDetectorEncryption(
ses->channel.send(0x03, 0x00, &cmd, sizeof(cmd));
ses->detector_crypt.reset(new PSOBBMultiKeyDetectorEncryption(
this->state->bb_private_keys,
bb_crypt_initial_client_commands,
cmd.basic_cmd.client_key.data(),
sizeof(cmd.basic_cmd.client_key)));
session->channel.crypt_in = session->detector_crypt;
session->channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
session->detector_crypt,
ses->channel.crypt_in = ses->detector_crypt;
ses->channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
ses->detector_crypt,
cmd.basic_cmd.server_key.data(),
sizeof(cmd.basic_cmd.server_key),
true));
@@ -220,7 +220,7 @@ void ProxyServer::on_client_connect(
}
ProxyServer::UnlinkedSession::UnlinkedSession(
ProxyServer* server,
shared_ptr<ProxyServer> server,
struct bufferevent* bev,
uint16_t local_port,
GameVersion version)
@@ -240,11 +240,25 @@ ProxyServer::UnlinkedSession::UnlinkedSession(
memset(&this->next_destination, 0, sizeof(this->next_destination));
}
std::shared_ptr<ProxyServer> ProxyServer::UnlinkedSession::require_server() const {
auto server = this->server.lock();
if (!server) {
throw logic_error("server is deleted");
}
return server;
}
std::shared_ptr<ServerState> ProxyServer::UnlinkedSession::require_server_state() const {
return this->require_server()->state;
}
void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint32_t, std::string& data) {
auto* session = reinterpret_cast<UnlinkedSession*>(ch.context_obj);
auto* ses = reinterpret_cast<UnlinkedSession*>(ch.context_obj);
auto server = ses->require_server();
auto s = server->state;
bool should_close_unlinked_session = false;
shared_ptr<const License> license;
shared_ptr<License> license;
uint32_t sub_version = 0;
uint8_t language = 1; // Default = English
string character_name;
@@ -253,12 +267,12 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
string hardware_id;
try {
if (session->version == GameVersion::DC) {
if (ses->version == GameVersion::DC) {
// We should only get a 93 or 9D while the session is unlinked; if we get
// anything else, disconnect
if (command == 0x93) {
const auto& cmd = check_size_t<C_LoginV1_DC_93>(data);
license = session->server->state->license_manager->verify_pc(
license = s->license_index->verify_v1_v2(
stoul(cmd.serial_number, nullptr, 16), cmd.access_key);
sub_version = cmd.sub_version;
language = cmd.language;
@@ -267,7 +281,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
client_config.cfg.flags |= Client::Flag::IS_DC_V1;
} else if (command == 0x9D) {
const auto& cmd = check_size_t<C_Login_DC_PC_GC_9D>(data, sizeof(C_LoginExtended_DC_GC_9D));
license = session->server->state->license_manager->verify_pc(
license = s->license_index->verify_v1_v2(
stoul(cmd.serial_number, nullptr, 16), cmd.access_key);
sub_version = cmd.sub_version;
language = cmd.language;
@@ -276,20 +290,20 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
throw runtime_error("command is not 93 or 9D");
}
} else if (session->version == GameVersion::PC) {
} else if (ses->version == GameVersion::PC) {
// We should only get a 9D while the session is unlinked; if we get
// anything else, disconnect
if (command != 0x9D) {
throw runtime_error("command is not 9D");
}
const auto& cmd = check_size_t<C_Login_DC_PC_GC_9D>(data, sizeof(C_LoginExtended_PC_9D));
license = session->server->state->license_manager->verify_pc(
license = s->license_index->verify_v1_v2(
stoul(cmd.serial_number, nullptr, 16), cmd.access_key);
sub_version = cmd.sub_version;
language = cmd.language;
character_name = cmd.name;
} else if (session->version == GameVersion::GC) {
} else if (ses->version == GameVersion::GC) {
// We should only get a 9E while the session is unlinked; if we get
// anything else, disconnect
// TODO: GCTE will send 9D; we should presumably handle that too, sigh
@@ -297,17 +311,17 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
throw runtime_error("command is not 9E");
}
const auto& cmd = check_size_t<C_Login_GC_9E>(data, sizeof(C_LoginExtended_GC_9E));
license = session->server->state->license_manager->verify_gc(
license = s->license_index->verify_gc(
stoul(cmd.serial_number, nullptr, 16), cmd.access_key);
sub_version = cmd.sub_version;
language = cmd.language;
character_name = cmd.name;
client_config.cfg = cmd.client_config.cfg;
} else if (session->version == GameVersion::XB) {
} else if (ses->version == GameVersion::XB) {
throw runtime_error("xbox licenses are not implemented");
} else if (session->version == GameVersion::BB) {
} else if (ses->version == GameVersion::BB) {
// We should only get a 93 while the session is unlinked; if we get
// anything else, disconnect
if (command != 0x93) {
@@ -315,16 +329,20 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
}
const auto& cmd = check_size_t<C_Login_BB_93>(data);
try {
license = session->server->state->license_manager->verify_bb(
license = s->license_index->verify_bb(
cmd.username, cmd.password);
} catch (const missing_license&) {
if (!session->server->state->allow_unregistered_users) {
} catch (const LicenseIndex::missing_license&) {
if (!s->allow_unregistered_users) {
throw;
}
shared_ptr<License> l = LicenseManager::create_license_bb(
fnv1a32(cmd.username) & 0x7FFFFFFF, cmd.username, cmd.password, true);
session->server->state->license_manager->add(l);
shared_ptr<License> l(new License());
l->serial_number = fnv1a32(cmd.username) & 0x7FFFFFFF;
l->bb_username = cmd.username;
l->bb_password = cmd.password;
s->license_index->add(l);
license = l;
string l_str = l->str();
ses->log.info("Created license %s", l_str.c_str());
}
login_command_bb = std::move(data);
@@ -333,7 +351,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
}
} catch (const exception& e) {
session->log.error("Failed to process command from unlinked client: %s", e.what());
ses->log.error("Failed to process command from unlinked client: %s", e.what());
should_close_unlinked_session = true;
}
@@ -350,94 +368,84 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
should_close_unlinked_session = true;
// Look up the linked session for this license (if any)
shared_ptr<LinkedSession> linked_session;
shared_ptr<LinkedSession> linked_ses;
try {
linked_session = session->server->id_to_session.at(license->serial_number);
linked_session->log.info("Resuming linked session from unlinked session");
linked_ses = server->id_to_session.at(license->serial_number);
linked_ses->log.info("Resuming linked session from unlinked session");
} catch (const out_of_range&) {
// If there's no open session for this license, then there must be a valid
// destination somewhere - either in the client config or in the unlinked
// session
if (client_config.cfg.magic == CLIENT_CONFIG_MAGIC) {
linked_session.reset(new LinkedSession(
session->server,
session->local_port,
session->version,
license,
client_config));
linked_session->log.info("Opened licensed session for unlinked session based on client config");
} else if (session->next_destination.ss_family == AF_INET) {
linked_session.reset(new LinkedSession(
session->server,
session->local_port,
session->version,
license,
session->next_destination));
linked_session->log.info("Opened licensed session for unlinked session based on unlinked default destination");
linked_ses.reset(new LinkedSession(
server, ses->local_port, ses->version, license, client_config));
linked_ses->log.info("Opened licensed session for unlinked session based on client config");
} else if (ses->next_destination.ss_family == AF_INET) {
linked_ses.reset(new LinkedSession(
server, ses->local_port, ses->version, license, ses->next_destination));
linked_ses->log.info("Opened licensed session for unlinked session based on unlinked default destination");
} else {
session->log.error("Cannot open linked session: no valid destination in client config or unlinked session");
ses->log.error("Cannot open linked session: no valid destination in client config or unlinked session");
}
}
if (linked_session.get()) {
session->server->id_to_session.emplace(license->serial_number, linked_session);
if (linked_session->version != session->version) {
linked_session->log.error("Linked session has different game version");
if (linked_ses.get()) {
server->id_to_session.emplace(license->serial_number, linked_ses);
if (linked_ses->version != ses->version) {
linked_ses->log.error("Linked session has different game version");
} else {
// Resume the linked session using the unlinked session
try {
if (session->version == GameVersion::BB) {
linked_session->resume(
std::move(session->channel),
session->detector_crypt,
if (ses->version == GameVersion::BB) {
linked_ses->resume(
std::move(ses->channel),
ses->detector_crypt,
std::move(login_command_bb));
} else {
linked_session->resume(
std::move(session->channel),
session->detector_crypt,
linked_ses->resume(
std::move(ses->channel),
ses->detector_crypt,
sub_version,
language,
character_name,
hardware_id);
}
} catch (const exception& e) {
linked_session->log.error("Failed to resume linked session: %s", e.what());
linked_ses->log.error("Failed to resume linked session: %s", e.what());
}
}
}
}
if (should_close_unlinked_session) {
session->server->delete_session(session_key);
server->delete_session(session_key);
}
}
void ProxyServer::UnlinkedSession::on_error(Channel& ch, short events) {
auto* session = reinterpret_cast<UnlinkedSession*>(ch.context_obj);
auto* ses = reinterpret_cast<UnlinkedSession*>(ch.context_obj);
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
session->log.warning("Error %d (%s) in unlinked client stream", err,
ses->log.warning("Error %d (%s) in unlinked client stream", err,
evutil_socket_error_to_string(err));
}
if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) {
session->log.info("Client has disconnected");
session->server->delete_session(session->channel.bev.get());
ses->log.info("Client has disconnected");
ses->require_server()->delete_session(ses->channel.bev.get());
}
}
ProxyServer::LinkedSession::LinkedSession(
ProxyServer* server,
shared_ptr<ProxyServer> server,
uint64_t id,
uint16_t local_port,
GameVersion version)
: server(server),
id(id),
log(string_printf("[ProxyServer:LinkedSession:%08" PRIX64 "] ", this->id), proxy_server_log.min_level),
timeout_event(event_new(this->server->base.get(), -1, EV_TIMEOUT,
&LinkedSession::dispatch_on_timeout, this),
event_free),
timeout_event(event_new(server->base.get(), -1, EV_TIMEOUT, &LinkedSession::dispatch_on_timeout, this), event_free),
license(nullptr),
client_channel(
version,
@@ -477,10 +485,10 @@ ProxyServer::LinkedSession::LinkedSession(
}
ProxyServer::LinkedSession::LinkedSession(
ProxyServer* server,
shared_ptr<ProxyServer> server,
uint16_t local_port,
GameVersion version,
shared_ptr<const License> license,
shared_ptr<License> license,
const ClientConfigBB& newserv_client_config)
: LinkedSession(server, license->serial_number, local_port, version) {
this->license = license;
@@ -493,10 +501,10 @@ ProxyServer::LinkedSession::LinkedSession(
}
ProxyServer::LinkedSession::LinkedSession(
ProxyServer* server,
shared_ptr<ProxyServer> server,
uint16_t local_port,
GameVersion version,
std::shared_ptr<const License> license,
std::shared_ptr<License> license,
const struct sockaddr_storage& next_destination)
: LinkedSession(server, license->serial_number, local_port, version) {
this->license = license;
@@ -504,7 +512,7 @@ ProxyServer::LinkedSession::LinkedSession(
}
ProxyServer::LinkedSession::LinkedSession(
ProxyServer* server,
shared_ptr<ProxyServer> server,
uint64_t id,
uint16_t local_port,
GameVersion version,
@@ -513,6 +521,18 @@ ProxyServer::LinkedSession::LinkedSession(
this->next_destination = destination;
}
shared_ptr<ProxyServer> ProxyServer::LinkedSession::require_server() const {
auto server = this->server.lock();
if (!server) {
throw logic_error("server is deleted");
}
return server;
}
std::shared_ptr<ServerState> ProxyServer::LinkedSession::require_server_state() const {
return this->require_server()->state;
}
void ProxyServer::LinkedSession::resume(
Channel&& client_channel,
shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
@@ -579,7 +599,7 @@ void ProxyServer::LinkedSession::connect() {
this->log.info("Connecting to %s", netloc_str.c_str());
this->server_channel.set_bufferevent(bufferevent_socket_new(
this->server->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS));
this->require_server()->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS));
if (bufferevent_socket_connect(this->server_channel.bev.get(),
reinterpret_cast<const sockaddr*>(dest_sin), sizeof(*dest_sin)) != 0) {
throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR()));
@@ -606,7 +626,7 @@ ProxyServer::LinkedSession::SavingFile::SavingFile(
void ProxyServer::LinkedSession::SavingFile::write() const {
string data = join(this->blocks);
if (is_download && (ends_with(this->basename, ".bin") || ends_with(this->basename, ".dat"))) {
data = Quest::decode_dlq_data(data);
data = decode_dlq_data(data);
}
save_file(this->output_filename, data);
}
@@ -617,38 +637,38 @@ void ProxyServer::LinkedSession::dispatch_on_timeout(
}
void ProxyServer::LinkedSession::on_timeout() {
this->server->delete_session(this->id);
this->require_server()->delete_session(this->id);
}
void ProxyServer::LinkedSession::on_error(Channel& ch, short events) {
auto* session = reinterpret_cast<LinkedSession*>(ch.context_obj);
bool is_server_stream = (&ch == &session->server_channel);
auto* ses = reinterpret_cast<LinkedSession*>(ch.context_obj);
bool is_server_stream = (&ch == &ses->server_channel);
if (events & BEV_EVENT_CONNECTED) {
session->log.info("%s channel connected", is_server_stream ? "Server" : "Client");
ses->log.info("%s channel connected", is_server_stream ? "Server" : "Client");
if (is_server_stream && (session->options.override_lobby_event >= 0) &&
(((session->version == GameVersion::GC) && !(session->newserv_client_config.cfg.flags & Client::Flag::IS_GC_TRIAL_EDITION)) ||
(session->version == GameVersion::XB) ||
(session->version == GameVersion::BB))) {
session->client_channel.send(0xDA, session->options.override_lobby_event);
if (is_server_stream && (ses->options.override_lobby_event >= 0) &&
(((ses->version == GameVersion::GC) && !(ses->newserv_client_config.cfg.flags & Client::Flag::IS_GC_TRIAL_EDITION)) ||
(ses->version == GameVersion::XB) ||
(ses->version == GameVersion::BB))) {
ses->client_channel.send(0xDA, ses->options.override_lobby_event);
}
}
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
session->log.warning("Error %d (%s) in %s stream",
ses->log.warning("Error %d (%s) in %s stream",
err, evutil_socket_error_to_string(err),
is_server_stream ? "server" : "client");
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
session->log.info("%s has disconnected",
ses->log.info("%s has disconnected",
is_server_stream ? "Server" : "Client");
// If the server disconnected, send the client back to the game server so
// they're not disconnected completely.
if (is_server_stream) {
session->send_to_game_server("The server has\ndisconnected.");
ses->send_to_game_server("The server has\ndisconnected.");
}
session->disconnect();
ses->disconnect();
}
}
@@ -683,7 +703,8 @@ void ProxyServer::LinkedSession::send_to_game_server(const char* error_message)
this->client_channel.send(this->is_in_game ? 0x66 : 0x69, leaving_id, &cmd, sizeof(cmd));
}
string encoded_name = encode_sjis(this->server->state->name);
auto s = this->require_server_state();
string encoded_name = encode_sjis(s->name);
if (this->is_in_game) {
send_ship_info(this->client_channel, decode_sjis(string_printf("You cannot return\nto $C6%s$C7\nwhile in a game.\n\n%s", encoded_name.c_str(), error_message ? error_message : "")));
this->disconnect();
@@ -701,7 +722,7 @@ void ProxyServer::LinkedSession::send_to_game_server(const char* error_message)
const auto& port_name = version_to_login_port_name.at(static_cast<size_t>(
this->version));
S_Reconnect_19 reconnect_cmd = {{0, this->server->state->name_to_port_config.at(port_name)->port, 0}};
S_Reconnect_19 reconnect_cmd = {{0, s->name_to_port_config.at(port_name)->port, 0}};
// If the client is on a virtual connection, we can use any address
// here and they should be able to connect back to the game server. If
@@ -760,25 +781,24 @@ bool ProxyServer::LinkedSession::is_connected() const {
}
void ProxyServer::LinkedSession::on_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data) {
auto* session = reinterpret_cast<LinkedSession*>(ch.context_obj);
bool is_server_stream = (&ch == &session->server_channel);
auto* ses = reinterpret_cast<LinkedSession*>(ch.context_obj);
bool is_server_stream = (&ch == &ses->server_channel);
try {
if (is_server_stream) {
size_t bytes_to_save = min<size_t>(data.size(), sizeof(session->prev_server_command_bytes));
memcpy(session->prev_server_command_bytes, data.data(), bytes_to_save);
size_t bytes_to_save = min<size_t>(data.size(), sizeof(ses->prev_server_command_bytes));
memcpy(ses->prev_server_command_bytes, data.data(), bytes_to_save);
}
on_proxy_command(
session->server->state,
*session,
ses->shared_from_this(),
is_server_stream,
command,
flag,
data);
} catch (const exception& e) {
session->log.error("Failed to process command from %s: %s",
ses->log.error("Failed to process command from %s: %s",
is_server_stream ? "server" : "client", e.what());
session->disconnect();
ses->disconnect();
}
}
@@ -805,10 +825,10 @@ shared_ptr<ProxyServer::LinkedSession> ProxyServer::get_session_by_name(
}
shared_ptr<ProxyServer::LinkedSession> ProxyServer::create_licensed_session(
shared_ptr<const License> l, uint16_t local_port, GameVersion version,
shared_ptr<License> l, uint16_t local_port, GameVersion version,
const ClientConfigBB& newserv_client_config) {
shared_ptr<LinkedSession> session(new LinkedSession(
this, local_port, version, l, newserv_client_config));
this->shared_from_this(), local_port, version, l, newserv_client_config));
auto emplace_ret = this->id_to_session.emplace(session->id, session);
if (!emplace_ret.second) {
throw runtime_error("session already exists for this license");
+23 -20
View File
@@ -16,7 +16,7 @@
#include "PSOProtocol.hh"
#include "ServerState.hh"
class ProxyServer {
class ProxyServer : public std::enable_shared_from_this<ProxyServer> {
public:
ProxyServer() = delete;
ProxyServer(const ProxyServer&) = delete;
@@ -31,14 +31,14 @@ public:
void connect_client(struct bufferevent* bev, uint16_t server_port);
struct LinkedSession {
ProxyServer* server;
struct LinkedSession : std::enable_shared_from_this<LinkedSession> {
std::weak_ptr<ProxyServer> server;
uint64_t id;
PrefixedLogger log;
std::unique_ptr<struct event, void (*)(struct event*)> timeout_event;
std::shared_ptr<const License> license;
std::shared_ptr<License> license;
Channel client_channel;
Channel server_channel;
@@ -73,17 +73,14 @@ public:
// A null handler in here means to forward the response to the remote server
std::deque<std::function<void(uint32_t return_value, uint32_t checksum)>> function_call_return_handler_queue;
G_SwitchStateChanged_6x05 last_switch_enabled_command;
PlayerInventoryItem next_drop_item;
ItemData next_drop_item;
uint32_t next_item_id;
struct LobbyPlayer {
uint32_t guild_card_number;
uint32_t guild_card_number = 0;
std::string name;
uint8_t section_id;
uint8_t char_class;
LobbyPlayer() : guild_card_number(0),
section_id(0),
char_class(0) {}
uint8_t section_id = 0;
uint8_t char_class = 0;
};
std::vector<LobbyPlayer> lobby_players;
size_t lobby_client_id;
@@ -115,29 +112,32 @@ public:
// TODO: This first constructor should be private
LinkedSession(
ProxyServer* server,
std::shared_ptr<ProxyServer> server,
uint64_t id,
uint16_t local_port,
GameVersion version);
LinkedSession(
ProxyServer* server,
std::shared_ptr<ProxyServer> server,
uint16_t local_port,
GameVersion version,
std::shared_ptr<const License> license,
std::shared_ptr<License> license,
const ClientConfigBB& newserv_client_config);
LinkedSession(
ProxyServer* server,
std::shared_ptr<ProxyServer> server,
uint16_t local_port,
GameVersion version,
std::shared_ptr<const License> license,
std::shared_ptr<License> license,
const struct sockaddr_storage& next_destination);
LinkedSession(
ProxyServer* server,
std::shared_ptr<ProxyServer> server,
uint64_t id,
uint16_t local_port,
GameVersion version,
const struct sockaddr_storage& next_destination);
std::shared_ptr<ProxyServer> require_server() const;
std::shared_ptr<ServerState> require_server_state() const;
void resume(
Channel&& client_channel,
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
@@ -171,7 +171,7 @@ public:
std::shared_ptr<LinkedSession> get_session();
std::shared_ptr<LinkedSession> get_session_by_name(const std::string& name);
std::shared_ptr<LinkedSession> create_licensed_session(
std::shared_ptr<const License> l,
std::shared_ptr<License> l,
uint16_t local_port,
GameVersion version,
const ClientConfigBB& newserv_client_config);
@@ -205,7 +205,7 @@ private:
};
struct UnlinkedSession {
ProxyServer* server;
std::weak_ptr<ProxyServer> server;
PrefixedLogger log;
Channel channel;
@@ -215,7 +215,10 @@ private:
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt;
UnlinkedSession(ProxyServer* server, struct bufferevent* bev, uint16_t port, GameVersion version);
UnlinkedSession(std::shared_ptr<ProxyServer> server, struct bufferevent* bev, uint16_t port, GameVersion version);
std::shared_ptr<ProxyServer> require_server() const;
std::shared_ptr<ServerState> require_server_state() const;
void receive_and_process_commands();
+690 -529
View File
File diff suppressed because it is too large Load Diff
+81 -64
View File
@@ -7,9 +7,19 @@
#include <string>
#include <vector>
#include "PlayerSubordinates.hh"
#include "QuestScript.hh"
#include "StaticGameData.hh"
enum class QuestFileFormat {
BIN_DAT = 0,
BIN_DAT_UNCOMPRESSED,
BIN_DAT_GCI,
BIN_DAT_VMS,
BIN_DAT_DLQ,
QST,
};
struct QuestCategoryIndex {
struct Category {
enum Flag {
@@ -43,94 +53,101 @@ struct QuestCategoryIndex {
const Category& at(uint32_t category_id) const;
};
class Quest {
public:
struct DATSectionHeader {
le_uint32_t type; // 1 = objects, 2 = enemies. There are other types too
le_uint32_t section_size; // Includes this header
le_uint32_t area;
le_uint32_t data_size;
} __attribute__((packed));
enum class FileFormat {
BIN_DAT = 0,
BIN_DAT_UNCOMPRESSED,
BIN_DAT_GCI,
BIN_DAT_VMS,
BIN_DAT_DLQ,
QST,
};
int64_t internal_id;
uint32_t menu_item_id;
struct VersionedQuest {
uint32_t quest_number;
uint32_t category_id;
Episode episode;
bool joinable;
QuestScriptVersion version;
std::string file_basename; // we append -<version>.<bin/dat> when reading
FileFormat file_format;
bool has_mnm_extension;
bool is_dlq_encoded;
std::u16string name;
QuestScriptVersion version;
uint8_t language;
bool is_dlq_encoded;
std::u16string short_description;
std::u16string long_description;
std::shared_ptr<const std::string> bin_contents;
std::shared_ptr<const std::string> dat_contents;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
Quest(const std::string& file_basename, QuestScriptVersion version, std::shared_ptr<const QuestCategoryIndex> category_index);
VersionedQuest(
uint32_t quest_number,
uint32_t category_id,
QuestScriptVersion version,
uint8_t language,
std::shared_ptr<const std::string> bin_contents,
std::shared_ptr<const std::string> dat_contents,
std::shared_ptr<const BattleRules> battle_rules = nullptr,
ssize_t challenge_template_index = -1);
std::string bin_filename() const;
std::string dat_filename() const;
std::shared_ptr<VersionedQuest> create_download_quest() const;
std::string encode_qst() const;
};
class Quest {
public:
Quest() = delete;
explicit Quest(std::shared_ptr<const VersionedQuest> initial_version);
Quest(const Quest&) = default;
Quest(Quest&&) = default;
Quest& operator=(const Quest&) = default;
Quest& operator=(Quest&&) = default;
std::string bin_filename() const;
std::string dat_filename() const;
void add_version(std::shared_ptr<const VersionedQuest> vq);
bool has_version(QuestScriptVersion v, uint8_t language) const;
std::shared_ptr<const VersionedQuest> version(QuestScriptVersion v, uint8_t language) const;
std::shared_ptr<const std::string> bin_contents() const;
std::shared_ptr<const std::string> dat_contents() const;
static uint16_t versions_key(QuestScriptVersion v, uint8_t language);
static std::string encode_download_quest_file(
const std::string& compressed_data, size_t decompressed_size = 0, uint32_t encryption_seed = 0);
std::shared_ptr<Quest> create_download_quest() const;
static std::string decode_gci_file(
const std::string& filename,
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1);
static std::string decode_vms_file(
const std::string& filename,
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1);
static std::string decode_dlq_file(const std::string& filename);
static std::string decode_dlq_data(const std::string& filename);
static std::pair<std::string, std::string> decode_qst_file(const std::string& filename);
static std::string encode_qst(
const std::string& bin_data,
const std::string& dat_data,
const std::u16string& name,
const std::string& file_basename,
QuestScriptVersion version,
bool is_dlq_encoded);
std::string encode_qst() const;
private:
// these are populated when requested
mutable std::shared_ptr<std::string> bin_contents_ptr;
mutable std::shared_ptr<std::string> dat_contents_ptr;
uint32_t quest_number;
uint32_t category_id;
Episode episode;
bool joinable;
std::u16string name;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
std::map<uint16_t, std::shared_ptr<const VersionedQuest>> versions;
};
struct QuestIndex {
std::string directory;
std::shared_ptr<const QuestCategoryIndex> category_index;
std::map<std::pair<QuestScriptVersion, 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<uint32_t, std::shared_ptr<Quest>> quests_by_number;
std::map<std::string, std::shared_ptr<std::string>> gba_file_contents;
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index);
std::shared_ptr<const Quest> get(QuestScriptVersion version, uint32_t id) const;
std::shared_ptr<const Quest> get(uint32_t quest_number) const;
std::shared_ptr<const std::string> get_gba(const std::string& name) const;
std::vector<std::shared_ptr<const Quest>> filter(
QuestScriptVersion version, uint32_t category_id) const;
std::vector<std::shared_ptr<const Quest>> filter(uint32_t category_id, QuestScriptVersion version, uint8_t language) const;
};
std::string encode_download_quest_data(
const std::string& compressed_data,
size_t decompressed_size = 0,
uint32_t encryption_seed = 0);
std::string decode_gci_data(
const std::string& data,
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1,
bool skip_checksum = false);
std::string decode_vms_data(
const std::string& data,
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1,
bool skip_checksum = false);
std::string decode_dlq_data(const std::string& data);
std::pair<std::string, std::string> decode_qst_data(const std::string& data);
std::string encode_qst_file(
const std::string& bin_data,
const std::string& dat_data,
const std::u16string& name,
uint32_t quest_number,
QuestScriptVersion version,
bool is_dlq_encoded);
+622 -706
View File
File diff suppressed because it is too large Load Diff
+48 -44
View File
@@ -25,61 +25,65 @@ template <>
const char* name_for_enum<QuestScriptVersion>(QuestScriptVersion v);
struct PSOQuestHeaderDC { // Same format for DC v1 and v2
le_uint32_t code_offset;
le_uint32_t function_table_offset;
le_uint32_t size;
le_uint32_t unused;
uint8_t is_download;
uint8_t unknown1;
le_uint16_t quest_number; // 0xFFFF for challenge quests
ptext<char, 0x20> name;
ptext<char, 0x80> short_description;
ptext<char, 0x120> long_description;
/* 0000 */ le_uint32_t code_offset;
/* 0004 */ le_uint32_t function_table_offset;
/* 0008 */ le_uint32_t size;
/* 000C */ le_uint32_t unused;
/* 0010 */ uint8_t is_download;
/* 0011 */ uint8_t unknown1;
/* 0012 */ le_uint16_t quest_number; // 0xFFFF for challenge quests
/* 0014 */ ptext<char, 0x20> name;
/* 0034 */ ptext<char, 0x80> short_description;
/* 00B4 */ ptext<char, 0x120> long_description;
/* 01D4 */
} __attribute__((packed));
struct PSOQuestHeaderPC {
le_uint32_t code_offset;
le_uint32_t function_table_offset;
le_uint32_t size;
le_uint32_t unused;
uint8_t is_download;
uint8_t unknown1;
le_uint16_t quest_number; // 0xFFFF for challenge quests
ptext<char16_t, 0x20> name;
ptext<char16_t, 0x80> short_description;
ptext<char16_t, 0x120> long_description;
/* 0000 */ le_uint32_t code_offset;
/* 0004 */ le_uint32_t function_table_offset;
/* 0008 */ le_uint32_t size;
/* 000C */ le_uint32_t unused;
/* 0010 */ uint8_t is_download;
/* 0011 */ uint8_t unknown1;
/* 0012 */ le_uint16_t quest_number; // 0xFFFF for challenge quests
/* 0014 */ ptext<char16_t, 0x20> name;
/* 0054 */ ptext<char16_t, 0x80> short_description;
/* 0154 */ ptext<char16_t, 0x120> long_description;
/* 0394 */
} __attribute__((packed));
// TODO: Is the XB quest header format the same as on GC? If not, make a
// separate struct; if so, rename this struct to V3.
struct PSOQuestHeaderGC {
le_uint32_t code_offset;
le_uint32_t function_table_offset;
le_uint32_t size;
le_uint32_t unused;
uint8_t is_download;
uint8_t unknown1;
uint8_t quest_number;
uint8_t episode; // 1 = Ep2. Apparently some quests have 0xFF here, which means ep1 (?)
ptext<char, 0x20> name;
ptext<char, 0x80> short_description;
ptext<char, 0x120> long_description;
/* 0000 */ le_uint32_t code_offset;
/* 0004 */ le_uint32_t function_table_offset;
/* 0008 */ le_uint32_t size;
/* 000C */ le_uint32_t unused;
/* 0010 */ uint8_t is_download;
/* 0011 */ uint8_t unknown1;
/* 0012 */ uint8_t quest_number;
/* 0013 */ uint8_t episode; // 1 = Ep2. Apparently some quests have 0xFF here, which means ep1 (?)
/* 0014 */ ptext<char, 0x20> name;
/* 0034 */ ptext<char, 0x80> short_description;
/* 00B4 */ ptext<char, 0x120> long_description;
/* 01D4 */
} __attribute__((packed));
struct PSOQuestHeaderBB {
le_uint32_t code_offset;
le_uint32_t function_table_offset;
le_uint32_t size;
le_uint32_t unused;
le_uint16_t quest_number; // 0xFFFF for challenge quests
le_uint16_t unused2;
uint8_t episode; // 0 = Ep1, 1 = Ep2, 2 = Ep4
uint8_t max_players;
uint8_t joinable_in_progress;
uint8_t unknown;
ptext<char16_t, 0x20> name;
ptext<char16_t, 0x80> short_description;
ptext<char16_t, 0x120> long_description;
/* 0000 */ le_uint32_t code_offset;
/* 0004 */ le_uint32_t function_table_offset;
/* 0008 */ le_uint32_t size;
/* 000C */ le_uint32_t unused;
/* 0010 */ le_uint16_t quest_number; // 0xFFFF for challenge quests
/* 0012 */ le_uint16_t unused2;
/* 0014 */ uint8_t episode; // 0 = Ep1, 1 = Ep2, 2 = Ep4
/* 0015 */ uint8_t max_players;
/* 0016 */ uint8_t joinable_in_progress;
/* 0017 */ uint8_t unknown;
/* 0018 */ ptext<char16_t, 0x20> name;
/* 0058 */ ptext<char16_t, 0x80> short_description;
/* 0158 */ ptext<char16_t, 0x120> long_description;
/* 0398 */
} __attribute__((packed));
std::string disassemble_quest_script(const void* data, size_t size, QuestScriptVersion version);
+61 -9
View File
@@ -4,6 +4,7 @@
#include <phosg/Random.hh>
#include "BattleParamsIndex.hh"
#include "ItemData.hh"
#include "StaticGameData.hh"
using namespace std;
@@ -88,26 +89,69 @@ uint16_t RareItemSet::key_for_params(GameMode mode, Episode episode, uint8_t dif
return key;
}
AFSRareItemSet::AFSRareItemSet(shared_ptr<const string> data)
: afs(data) {
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (GameMode mode : modes) {
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
for (size_t section_id = 0; section_id < 10; section_id++) {
try {
size_t index = difficulty * 10 + section_id;
auto entry = this->afs.get(index);
if (entry.second < sizeof(Table)) {
throw runtime_error(string_printf("table %zu is too small", index));
}
this->tables.emplace(
this->key_for_params(mode, Episode::EP1, difficulty, section_id),
reinterpret_cast<const Table*>(entry.first));
} catch (const out_of_range&) {
}
}
}
}
}
std::vector<RareItemSet::ExpandedDrop> AFSRareItemSet::get_enemy_specs(
GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t rt_index) const {
try {
return this->tables.at(this->key_for_params(mode, episode, difficulty, secid))->get_enemy_specs(rt_index);
} catch (const out_of_range&) {
return {};
}
}
std::vector<RareItemSet::ExpandedDrop> AFSRareItemSet::get_box_specs(
GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t area) const {
try {
return this->tables.at(this->key_for_params(mode, episode, difficulty, secid))->get_box_specs(area);
} catch (const out_of_range&) {
return {};
}
}
GSLRareItemSet::GSLRareItemSet(shared_ptr<const string> data, bool is_big_endian)
: gsl(data, is_big_endian) {
const array<Episode, 2> episodes = {Episode::EP1, Episode::EP2};
const array<GameMode, 4> modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
for (GameMode mode : modes) {
for (Episode episode : episodes) {
for (size_t difficulty = 0; difficulty < 3; difficulty++) {
for (size_t difficulty = 0; difficulty < 4; difficulty++) {
for (size_t section_id = 0; section_id < 10; section_id++) {
string filename = string_printf("ItemRT%s%s%c%1zu.rel",
((mode == GameMode::CHALLENGE) ? "c" : ""),
((episode == Episode::EP2) ? "l" : ""),
tolower(abbreviation_for_difficulty(difficulty)), // One of "nhvu"
section_id);
auto entry = this->gsl.get(filename);
if (entry.second < sizeof(Table)) {
throw runtime_error(string_printf("table %s is too small", filename.c_str()));
try {
auto entry = this->gsl.get(filename);
if (entry.second < sizeof(Table)) {
throw runtime_error(string_printf("table %s is too small", filename.c_str()));
}
this->tables.emplace(
this->key_for_params(mode, episode, difficulty, section_id),
reinterpret_cast<const Table*>(entry.first));
} catch (const out_of_range&) {
}
this->tables.emplace(
this->key_for_params(mode, episode, difficulty, section_id),
reinterpret_cast<const Table*>(entry.first));
}
}
}
@@ -116,12 +160,20 @@ GSLRareItemSet::GSLRareItemSet(shared_ptr<const string> data, bool is_big_endian
std::vector<RareItemSet::ExpandedDrop> GSLRareItemSet::get_enemy_specs(
GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t rt_index) const {
return this->tables.at(this->key_for_params(mode, episode, difficulty, secid))->get_enemy_specs(rt_index);
try {
return this->tables.at(this->key_for_params(mode, episode, difficulty, secid))->get_enemy_specs(rt_index);
} catch (const out_of_range&) {
return {};
}
}
std::vector<RareItemSet::ExpandedDrop> GSLRareItemSet::get_box_specs(
GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t area) const {
return this->tables.at(this->key_for_params(mode, episode, difficulty, secid))->get_box_specs(area);
try {
return this->tables.at(this->key_for_params(mode, episode, difficulty, secid))->get_box_specs(area);
} catch (const out_of_range&) {
return {};
}
}
RELRareItemSet::RELRareItemSet(shared_ptr<const string> data) : data(data) {
+17
View File
@@ -2,12 +2,16 @@
#include <stdint.h>
#include <array>
#include <memory>
#include <phosg/JSON.hh>
#include <random>
#include <string>
#include "AFSArchive.hh"
#include "GSLArchive.hh"
#include "StaticGameData.hh"
#include "Text.hh"
class RareItemSet {
public:
@@ -64,6 +68,19 @@ protected:
static uint16_t key_for_params(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid);
};
class AFSRareItemSet : public RareItemSet {
public:
AFSRareItemSet(std::shared_ptr<const std::string> data);
virtual ~AFSRareItemSet() = default;
virtual std::vector<ExpandedDrop> get_enemy_specs(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t rt_index) const;
virtual std::vector<ExpandedDrop> get_box_specs(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t area) const;
private:
std::unordered_map<uint16_t, const Table*> tables;
AFSArchive afs;
};
class GSLRareItemSet : public RareItemSet {
public:
GSLRareItemSet(std::shared_ptr<const std::string> data, bool is_big_endian);
+919 -771
View File
File diff suppressed because it is too large Load Diff
+10 -12
View File
@@ -8,18 +8,16 @@ std::shared_ptr<Lobby> create_game_generic(
std::shared_ptr<ServerState> s,
std::shared_ptr<Client> c,
const std::u16string& name,
const std::u16string& password,
Episode episode,
GameMode mode,
uint8_t difficulty,
uint32_t flags,
const std::u16string& password = u"",
Episode episode = Episode::EP1,
GameMode mode = GameMode::NORMAL,
uint8_t difficulty = 0,
uint32_t flags = 0,
bool allow_v1 = false,
std::shared_ptr<Lobby> watched_lobby = nullptr,
std::shared_ptr<Episode3::BattleRecordPlayer> battle_player = nullptr);
void on_connect(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void on_disconnect(std::shared_ptr<ServerState> s,
std::shared_ptr<Client> c);
void on_command(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c,
uint16_t command, uint32_t flag, const std::string& data);
void on_command_with_header(std::shared_ptr<ServerState> s,
std::shared_ptr<Client> c, std::string& data);
void on_connect(std::shared_ptr<Client> c);
void on_disconnect(std::shared_ptr<Client> c);
void on_command(std::shared_ptr<Client> c, uint16_t command, uint32_t flag, const std::string& data);
void on_command_with_header(std::shared_ptr<Client> c, std::string& data);
+808 -557
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -6,8 +6,9 @@
#include "ServerState.hh"
void on_subcommand_multi(
std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c, uint8_t command, uint8_t flag,
std::shared_ptr<Client> c,
uint8_t command,
uint8_t flag,
const std::string& data);
bool subcommand_is_implemented(uint8_t which);
+30 -28
View File
@@ -70,26 +70,30 @@ shared_ptr<ReplaySession::Event> ReplaySession::create_event(
return event;
}
static bool string_is_basic(const string& data) {
if (data.empty()) {
return true;
}
char ch = data[0];
for (size_t z = 1; z < data.size(); z++) {
if ((data[z] != ch) && (data[z] != 0)) {
return false;
}
}
return true;
}
void ReplaySession::check_for_password(shared_ptr<const Event> ev) const {
auto version = this->clients.at(ev->client_id)->version;
auto check_pw = [&](const string& pw) {
if (!this->required_password.empty() && !pw.empty() && (pw != this->required_password)) {
if (this->require_basic_credentials && !string_is_basic(pw)) {
print_data(stderr, ev->data, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::OFFSET_16_BITS);
throw runtime_error(string_printf("(ev-line %zu) sent password is incorrect", ev->line_num));
}
};
auto check_ak = [&](const string& ak) {
if (this->required_access_key.empty() || ak.empty()) {
return;
}
string ref_access_key;
if (version == GameVersion::DC || version == GameVersion::PC || version == GameVersion::PATCH) {
ref_access_key = this->required_access_key.substr(0, 8);
} else {
ref_access_key = this->required_access_key;
}
if (ak != ref_access_key) {
if (this->require_basic_credentials && !ak.empty() && !string_is_basic(ak)) {
print_data(stderr, ev->data, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::OFFSET_16_BITS);
throw runtime_error(string_printf("(ev-line %zu) sent access key is incorrect", ev->line_num));
}
@@ -245,7 +249,7 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
mask.client_key = 0;
break;
}
case 0x19: {
case 0x19:
if (mask_size == sizeof(S_ReconnectSplit_19)) {
auto& mask = check_size_t<S_ReconnectSplit_19>(mask_data, mask_size);
mask.pc_address = 0;
@@ -255,8 +259,7 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
mask.address = 0;
}
break;
}
case 0x41: {
case 0x41:
if (version == GameVersion::PC) {
auto& mask = check_size_t<S_GuildCardSearchResult_PC_41>(mask_data, mask_size);
mask.reconnect_command.address = 0;
@@ -268,27 +271,30 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
mask.reconnect_command.address = 0;
}
break;
}
case 0x64: {
case 0x64:
if (version == GameVersion::PC) {
auto& mask = check_size_t<S_JoinGame_PC_64>(mask_data, mask_size);
mask.variations.clear(0);
mask.rare_seed = 0;
} else { // V3
auto& mask = check_size_t<S_JoinGame_DC_GC_64>(
auto& mask = check_size_t<S_JoinGame_DC_64>(
mask_data, mask_size, sizeof(S_JoinGame_GC_Ep3_64));
mask.variations.clear(0);
mask.rare_seed = 0;
}
break;
}
case 0xB1: {
case 0xE8:
if (version == GameVersion::GC) {
auto& mask = check_size_t<S_JoinSpectatorTeam_GC_Ep3_E8>(mask_data, mask_size);
mask.rare_seed = 0;
}
break;
case 0xB1:
for (size_t x = 4; x < ev->mask.size(); x++) {
ev->mask[x] = 0;
}
break;
}
case 0xC9: {
case 0xC9:
if (mask_size == 0xCC) {
auto& mask = check_size_t<G_ServerVersionStrings_GC_Ep3_6xB4x46>(
mask_data, mask_size);
@@ -297,8 +303,7 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
mask.date_str2.clear(0);
}
break;
}
case 0x6C: {
case 0x6C:
if (version == GameVersion::GC && mask_size >= 0x14) {
const auto& cmd = check_size_t<G_MapList_GC_Ep3_6xB6x40>(cmd_data, cmd_size, 0xFFFF);
if ((cmd.header.header.basic_header.subcommand == 0xB6) &&
@@ -314,7 +319,6 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
}
}
break;
}
}
break;
}
@@ -361,11 +365,9 @@ ReplaySession::ReplaySession(
shared_ptr<struct event_base> base,
FILE* input_log,
shared_ptr<ServerState> state,
const string& required_access_key,
const string& required_password)
bool require_basic_credentials)
: state(state),
required_access_key(required_access_key),
required_password(required_password),
require_basic_credentials(require_basic_credentials),
base(base),
commands_sent(0),
bytes_sent(0),
+2 -4
View File
@@ -18,8 +18,7 @@ public:
std::shared_ptr<struct event_base> base,
FILE* input_log,
std::shared_ptr<ServerState> state,
const std::string& required_access_key = "",
const std::string& required_password = "");
bool require_basic_credentials);
ReplaySession(const ReplaySession&) = delete;
ReplaySession(ReplaySession&&) = delete;
ReplaySession& operator=(const ReplaySession&) = delete;
@@ -65,8 +64,7 @@ private:
};
std::shared_ptr<ServerState> state;
std::string required_access_key;
std::string required_password;
bool require_basic_credentials;
std::unordered_map<uint64_t, std::shared_ptr<Client>> clients;
std::unordered_map<Channel*, std::shared_ptr<Client>> channel_to_client;
+32 -6
View File
@@ -46,6 +46,29 @@ void ShuffleTables::shuffle(void* vdest, const void* vsrc, size_t size, bool rev
memcpy(&dest[size & 0xFFFFFF00], &src[size & 0xFFFFFF00], size & 0xFF);
}
bool PSOVMSFileHeader::checksum_correct() const {
auto add_data = +[](const void* data, size_t size, uint16_t crc) -> uint16_t {
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(data);
for (size_t z = 0; z < size; z++) {
crc ^= (static_cast<uint16_t>(bytes[z]) << 8);
for (uint8_t bit = 0; bit < 8; bit++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc = (crc << 1);
}
}
}
return crc;
};
uint16_t crc = add_data(this, offsetof(PSOVMSFileHeader, crc), 0);
crc = add_data("\0\0", 2, crc);
crc = add_data(&this->data_size,
sizeof(PSOVMSFileHeader) - offsetof(PSOVMSFileHeader, data_size) + this->num_icons * 0x200 + this->data_size, crc);
return (crc == this->crc);
}
bool PSOGCIFileHeader::checksum_correct() const {
uint32_t cs = crc32(&this->game_name, this->game_name.bytes());
cs = crc32(&this->embedded_seed, sizeof(this->embedded_seed), cs);
@@ -64,7 +87,7 @@ void PSOGCIFileHeader::check() const {
if (this->developer_id[0] != '8' || this->developer_id[1] != 'P') {
throw runtime_error("GCI file is not for a Sega game");
}
if (this->game_id[0] != 'G') {
if ((this->game_id[0] != 'G') && (this->game_id[0] != 'D')) {
throw runtime_error("GCI file is not for a GameCube game");
}
if (this->game_id[1] != 'P') {
@@ -83,6 +106,10 @@ bool PSOGCIFileHeader::is_ep3() const {
return (this->game_id[2] == 'S');
}
bool PSOGCIFileHeader::is_trial() const {
return (this->game_id[0] == 'D');
}
uint32_t compute_psogc_timestamp(
uint16_t year,
uint8_t month,
@@ -101,14 +128,13 @@ uint32_t compute_psogc_timestamp(
return second + (minute + (hour + (res_day * 24)) * 60) * 60;
}
string decrypt_gci_fixed_size_file_data_section_for_salvage(
string decrypt_gci_fixed_size_data_section_for_salvage(
const void* data_section,
size_t size,
uint32_t round1_seed,
uint64_t round2_seed,
size_t max_decrypt_bytes) {
string decrypted = decrypt_gci_or_vms_v2_data_section<true>(
data_section, size, round1_seed, max_decrypt_bytes);
string decrypted = decrypt_data_section<true>(data_section, size, round1_seed, max_decrypt_bytes);
PSOV2Encryption round2_crypt(round2_seed);
round2_crypt.encrypt_big_endian(decrypted.data(), decrypted.size());
@@ -123,8 +149,8 @@ bool PSOGCSnapshotFile::checksum_correct() const {
}
static uint32_t decode_rgb565(uint16_t c) {
// Input: rrrrrggg gggbbbbb
// Output: rrrrrrrr gggggggg bbbbbbbb aaaaaaaa
// Input bits: rrrrrggg gggbbbbb
// Output bits: rrrrrrrr gggggggg bbbbbbbb aaaaaaaa
return ((c << 16) & 0xF8000000) | ((c << 11) & 0x07000000) | // R
((c << 13) & 0x00FC0000) | ((c << 7) & 0x00030000) | // G
((c << 11) & 0x0000F800) | ((c << 6) & 0x00000700) | // B
+252 -48
View File
@@ -25,6 +25,23 @@ struct ShuffleTables {
void shuffle(void* vdest, const void* vsrc, size_t size, bool reverse) const;
};
struct PSOVMSFileHeader {
/* 0000 */ ptext<char, 0x10> short_desc;
/* 0010 */ ptext<char, 0x20> long_desc;
/* 0030 */ ptext<char, 0x10> creator_id;
/* 0040 */ le_uint16_t num_icons;
/* 0042 */ le_uint16_t animation_speed;
/* 0044 */ le_uint16_t eyecatch_type;
/* 0046 */ le_uint16_t crc;
/* 0048 */ le_uint32_t data_size; // Not including header and icons
/* 004C */ parray<uint8_t, 0x14> unused;
/* 0060 */ parray<le_uint16_t, 0x10> icon_palette;
// Variable-length field:
/* 0080 */ // parray<uint8_t, num_icons> icon;
bool checksum_correct() const;
} __attribute__((packed));
struct PSOGCIFileHeader {
// Every PSOGC save file begins with a PSOGCIFileHeader. The first 0x40 bytes
// of this structure are the .gci file header; the remaining bytes after that
@@ -69,6 +86,7 @@ struct PSOGCIFileHeader {
bool is_ep12() const;
bool is_ep3() const;
bool is_trial() const;
} __attribute__((packed));
struct PSOGCSystemFile {
@@ -76,7 +94,10 @@ struct PSOGCSystemFile {
/* 0004 */ be_int16_t music_volume; // 0 = full volume; -250 = min volume
/* 0006 */ int8_t sound_volume; // 0 = full volume; -100 = min volume
/* 0007 */ uint8_t language;
/* 0008 */ be_uint32_t unknown_a3; // Default 1728000 (== 60 * 60 * 24 * 20)
// This field stores the effective time zone offset between the server and
// client, in frames. The default value is 1728000, which corresponds to 16
// hours. This is recomputed when the client receives a B1 command.
/* 0008 */ be_uint32_t server_time_delta_frames;
/* 000C */ be_uint16_t udp_behavior; // 0 = auto, 1 = on, 2 = off
/* 000E */ be_uint16_t surround_sound_enabled;
/* 0010 */ parray<uint8_t, 0x100> event_flags; // Can be set by quest opcode D8 or E8
@@ -117,6 +138,27 @@ struct PSOGCSaveFileSymbolChatEntry {
/* 58 */
} __attribute__((packed));
struct PSOPCSaveFileSymbolChatEntry {
/* 00 */ le_uint32_t present;
/* 04 */ ptext<char16_t, 0x18> name;
/* 34 */ uint8_t face_spec;
/* 35 */ uint8_t flags;
/* 36 */ be_uint16_t unused;
struct CornerObject {
uint8_t type;
uint8_t flags_color;
} __attribute__((packed));
/* 38 */ parray<CornerObject, 4> corner_objects;
struct FacePart {
uint8_t type;
uint8_t x;
uint8_t y;
uint8_t flags;
} __attribute__((packed));
/* 40 */ parray<FacePart, 12> face_parts;
/* 70 */
} __attribute__((packed));
struct PSOGCSaveFileChatShortcutEntry {
/* 00 */ be_uint32_t present_type;
/* 04 */ parray<uint8_t, 0x50> definition;
@@ -131,7 +173,11 @@ struct PSOGCCharacterFile {
// to the start of the second internal structure (second column).
/* 0000:---- */ PlayerInventory inventory;
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
/* 041C:0000 */ be_uint32_t unknown_a1;
// Known bits in the flags field:
// 00000001: Character was not saved after disconnecting (and the message
// about items being deleted is shown in the select menu)
// 00000002: Used for something, but it's not known what it does
/* 041C:0000 */ be_uint32_t flags;
/* 0420:0004 */ be_uint32_t creation_timestamp;
// The signature field holds the value 0xA205B064, which is 2718281828 in
// decimal - approximately e * 10^9. It's unknown why Sega chose this value.
@@ -157,7 +203,11 @@ struct PSOGCCharacterFile {
// R = Map direction (0 = non-fixed; 1 = fixed)
/* 042C:0010 */ be_uint32_t option_flags;
/* 0430:0014 */ be_uint32_t save_count;
/* 0434:0018 */ parray<uint8_t, 0x230> unknown_a4;
/* 0434:0018 */ parray<uint8_t, 0x1C> unknown_a4;
/* 0450:0034 */ parray<uint8_t, 0x10> unknown_a5;
// 1024 bits (flags) per difficulty
/* 0460:0044 */ parray<parray<uint8_t, 0x80>, 4> quest_flags;
/* 0660:0244 */ be_uint32_t death_count;
/* 0664:0248 */ PlayerBank bank;
/* 192C:1510 */ GuildCardV3 guild_card;
/* 19BC:15A0 */ parray<PSOGCSaveFileSymbolChatEntry, 12> symbol_chats;
@@ -168,7 +218,13 @@ struct PSOGCCharacterFile {
/* 25DC:21C0 */ parray<uint8_t, 4> unknown_a2;
/* 25E0:21C4 */ PlayerRecordsV3_Challenge<true> challenge_records;
/* 26E0:22C4 */ parray<be_uint16_t, 20> tech_menu_shortcut_entries;
/* 2708:22EC */ parray<uint8_t, 0x90> unknown_a6;
/* 2708:22EC */ parray<uint8_t, 0x28> unknown_a6;
/* 2730:2314 */ parray<be_uint32_t, 0x10> quest_global_flags;
/* 2770:2354 */ PlayerRecords_Battle<true> offline_battle_records;
/* 2788:236C */ parray<uint8_t, 4> unknown_f5;
/* 278C:2370 */ be_uint32_t unknown_f6;
/* 2790:2374 */ be_uint32_t unknown_f7;
/* 2794:2378 */ be_uint32_t unknown_f8;
/* 2798:237C */
} __attribute__((packed));
/* 00004 */ parray<Character, 7> characters;
@@ -189,7 +245,7 @@ struct PSOGCEp3CharacterFile {
// to the start of the second internal structure (second column).
/* 0000:---- */ PlayerInventory inventory;
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
/* 041C:0000 */ be_uint32_t unknown_a1;
/* 041C:0000 */ be_uint32_t flags;
/* 0420:0004 */ be_uint32_t creation_timestamp;
/* 0424:0008 */ be_uint32_t signature; // Same value as for Episodes 1&2 (above)
/* 0428:000C */ be_uint32_t play_time_seconds;
@@ -199,25 +255,26 @@ struct PSOGCEp3CharacterFile {
/* 0430:0014 */ be_uint32_t save_count;
/* 0434:0018 */ parray<uint8_t, 0x1C> unknown_a2;
/* 0450:0034 */ parray<uint8_t, 0x10> unknown_a3;
// seq_vars is an array of 1024 bits, which contain all the Episode 3 quest
// seq_vars is an array of 8192 bits, which contain all the Episode 3 quest
// progress flags. This includes things like which maps are unlocked, which
// NPC decks are unlocked, and whether the player has a VIP card or not.
/* 0460:0044 */ parray<uint8_t, 0x400> seq_vars;
/* 0860:0444 */ be_uint32_t unknown_a4;
/* 0864:0448 */ be_uint32_t unknown_a5;
/* 0868:044C */ be_uint32_t unknown_a6;
/* 086C:0450 */ parray<uint8_t, 0x60> unknown_a7;
/* 0860:0444 */ be_uint32_t death_count;
// The following three fields appear to actually be a PlayerBank structure
// with only 4 item slots instead of 200. They presumably didn't completely
// remove the bank in Ep3 because they would have to change too much code.
/* 0864:0448 */ be_uint32_t num_bank_items;
/* 0868:044C */ be_uint32_t bank_meseta;
/* 086C:0450 */ parray<PlayerBankItem, 4> bank_items;
/* 08CC:04B0 */ GuildCardV3 guild_card;
/* 095C:0540 */ parray<PSOGCSaveFileSymbolChatEntry, 12> symbol_chats;
/* 0D7C:0960 */ parray<PSOGCSaveFileChatShortcutEntry, 20> chat_shortcuts;
/* 140C:0FF0 */ ptext<char, 0xAC> auto_reply;
/* 14B8:109C */ ptext<char, 0xAC> info_board;
/* 1564:1148 */ be_uint16_t win_count;
/* 1566:114A */ be_uint16_t lose_count;
/* 1568:114C */ parray<be_uint16_t, 5> unknown_a8;
/* 1572:1156 */ parray<uint8_t, 2> unused;
/* 1574:1158 */ parray<be_uint32_t, 2> unknown_a9;
/* 157C:1160 */ parray<uint8_t, 0xDC> unknown_a10;
// In this struct, place_counts[0] is win_count and [1] is loss_count
/* 1564:1148 */ PlayerRecords_Battle<true> battle_records;
/* 157C:1160 */ parray<uint8_t, 4> unknown_a10;
/* 1580:1164 */ PlayerRecordsV3_Challenge<true>::Stats challenge_record_stats;
/* 1658:123C */ Episode3::PlayerConfig ep3_config;
/* 39A8:358C */ be_uint32_t unknown_a11;
/* 39AC:3590 */ be_uint32_t unknown_a12;
@@ -226,7 +283,7 @@ struct PSOGCEp3CharacterFile {
} __attribute__((packed));
/* 00004 */ parray<Character, 7> characters;
/* 193F0 */ ptext<char, 0x10> serial_number; // As %08X (not decimal)
/* 19400 */ ptext<char, 0x10> access_key;
/* 19400 */ ptext<char, 0x10> access_key; // As 12 ASCII characters (decimal)
/* 19410 */ ptext<char, 0x10> password;
// In Episode 3, this field still exists, but is unused since BGM test was
// removed from the options menu in favor of the jukebox. The jukebox is
@@ -234,16 +291,16 @@ struct PSOGCEp3CharacterFile {
// by the B7 command sent by the server instead.
/* 19420 */ be_uint64_t bgm_test_songs_unlocked;
/* 19428 */ be_uint32_t save_count;
// This is an array of 1000 bits, represented here as 128 bytes, the last few
// of which are unused. Each bit corresponds to a card ID with the bit's
// index; if the bit is set, then the card's rarity is replaced with D2 if its
// original rarity is S, SS, E, or D2, or with D1 if the original rarity is
// any other value. Upon receiving a B8 command (new card definitions), the
// game updates this array of bits based on which cards in the received update
// have D1 or D2 rarities. This could have been used by Sega to persist part
// of the online updates into offline play, but there's no indication that
// they ever used this functionality.
/* 1942C */ parray<uint8_t, 0x80> card_rarity_override_flags;
// This is an array of 999 bits, represented here as 128 bytes (the last bit
// is never used). Each bit corresponds to a card ID with the bit's index; if
// the bit is set, then during offline play, the card's rank is replaced with
// D2 if its original rank is S, SS, E, or D2, or with D1 if the original rank
// is any other value. Upon receiving a B8 command (server card definitions),
// the game clears this array, and sets all bits whose corresponding cards
// from the server have the D1 or D2 ranks. This could have been used by Sega
// to prevent broken cards from being used offline, but there's no indication
// that they ever used this functionality.
/* 1942C */ parray<uint8_t, 0x80> card_rank_override_flags;
/* 194AC */ be_uint32_t round2_seed;
/* 194B0 */
} __attribute__((packed));
@@ -284,6 +341,8 @@ struct PSOGCSnapshotFile {
/* 00000 */ be_uint32_t checksum;
/* 00004 */ be_uint16_t width;
/* 00006 */ be_uint16_t height;
// Pixels are stored as 4x4 blocks of RGB565 values. See the implementation
// of decode_image for details.
/* 00008 */ parray<be_uint16_t, 0xC000> pixels;
/* 18008 */ uint8_t unknown_a1; // Always 0x18?
/* 18009 */ uint8_t unknown_a2;
@@ -298,8 +357,7 @@ struct PSOGCSnapshotFile {
} __attribute__((packed));
template <bool IsBigEndian>
std::string decrypt_gci_or_vms_v2_data_section(
const void* data_section, size_t size, uint32_t round1_seed, size_t max_decrypt_bytes = 0) {
std::string decrypt_data_section(const void* data_section, size_t size, uint32_t round1_seed, size_t max_decrypt_bytes = 0) {
if (max_decrypt_bytes == 0) {
max_decrypt_bytes = size;
} else {
@@ -322,8 +380,7 @@ std::string decrypt_gci_or_vms_v2_data_section(
}
template <bool IsBigEndian>
std::string encrypt_gci_or_vms_v2_data_section(
const void* data_section, size_t size, uint32_t round1_seed) {
std::string encrypt_data_section(const void* data_section, size_t size, uint32_t round1_seed) {
std::string encrypted(reinterpret_cast<const char*>(data_section), size);
encrypted.resize((encrypted.size() + 3) & (~3));
@@ -338,23 +395,66 @@ std::string encrypt_gci_or_vms_v2_data_section(
return ret;
}
template <typename StructT>
StructT decrypt_gci_fixed_size_file_data_section(
template <bool IsBigEndian>
std::string decrypt_fixed_size_data_section_s(
const void* data_section,
size_t size,
uint32_t round1_seed,
bool skip_checksum = false,
uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF) {
std::string decrypted = decrypt_gci_or_vms_v2_data_section<true>(
data_section, size, round1_seed);
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
if (size < 2 * sizeof(U32T)) {
throw std::runtime_error("data size is too small");
}
std::string decrypted = decrypt_data_section<IsBigEndian>(data_section, size, round1_seed);
uint32_t round2_seed = override_round2_seed < 0x100000000
? static_cast<uint32_t>(override_round2_seed)
: reinterpret_cast<const U32T*>(decrypted.data() + decrypted.size() - sizeof(U32T))->load();
PSOV2Encryption round2_crypt(round2_seed);
if (IsBigEndian) {
round2_crypt.encrypt_big_endian(decrypted.data(), decrypted.size() - sizeof(U32T));
} else {
round2_crypt.encrypt(decrypted.data(), decrypted.size() - sizeof(U32T));
}
if (!skip_checksum) {
U32T& checksum = *reinterpret_cast<U32T*>(decrypted.data());
uint32_t expected_crc = checksum;
checksum = 0;
uint32_t actual_crc = crc32(decrypted.data(), decrypted.size());
checksum = expected_crc;
if (expected_crc != actual_crc) {
throw std::runtime_error(string_printf(
"incorrect decrypted data section checksum: expected %08" PRIX32 "; received %08" PRIX32,
expected_crc, actual_crc));
}
}
return decrypted;
}
template <typename StructT, bool IsBigEndian>
StructT decrypt_fixed_size_data_section_t(
const void* data_section,
size_t size,
uint32_t round1_seed,
bool skip_checksum = false,
uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF) {
std::string decrypted = decrypt_data_section<IsBigEndian>(data_section, size, round1_seed);
if (decrypted.size() < sizeof(StructT)) {
throw std::runtime_error("file too small for structure");
}
StructT ret = *reinterpret_cast<const StructT*>(decrypted.data());
PSOV2Encryption round2_crypt(override_round2_seed < 0x100000000 ? override_round2_seed : ret.round2_seed.load());
round2_crypt.encrypt_big_endian(&ret, offsetof(StructT, round2_seed));
if (IsBigEndian) {
round2_crypt.encrypt_big_endian(&ret, offsetof(StructT, round2_seed));
} else {
round2_crypt.encrypt(&ret, offsetof(StructT, round2_seed));
}
if (!skip_checksum) {
uint32_t expected_crc = ret.checksum;
@@ -371,28 +471,55 @@ StructT decrypt_gci_fixed_size_file_data_section(
return ret;
}
std::string decrypt_gci_fixed_size_file_data_section_for_salvage(
const void* data_section,
size_t size,
uint32_t round1_seed,
uint64_t round2_seed,
size_t max_decrypt_bytes);
template <bool IsBigEndian>
std::string encrypt_fixed_size_data_section_s(const void* data, size_t size, uint32_t round1_seed) {
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
template <typename StructT>
std::string encrypt_gci_fixed_size_file_data_section(
const StructT& s, uint32_t round1_seed) {
if (size < 2 * sizeof(U32T)) {
throw std::runtime_error("data size is too small");
}
uint32_t round2_seed = random_object<uint32_t>();
std::string encrypted(reinterpret_cast<const char*>(data), size);
*reinterpret_cast<U32T*>(encrypted.data()) = 0;
*reinterpret_cast<U32T*>(encrypted.data() + encrypted.size() - sizeof(U32T)) = round2_seed;
*reinterpret_cast<U32T*>(encrypted.data()) = crc32(encrypted.data(), encrypted.size());
PSOV2Encryption round2_crypt(round2_seed);
if (IsBigEndian) {
round2_crypt.encrypt_big_endian(encrypted.data(), encrypted.size());
} else {
round2_crypt.encrypt(encrypted.data(), encrypted.size());
}
return encrypt_data_section<IsBigEndian>(encrypted.data(), encrypted.size(), round1_seed);
}
template <typename StructT, bool IsBigEndian>
std::string encrypt_fixed_size_data_section_t(const StructT& s, uint32_t round1_seed) {
StructT encrypted = s;
encrypted.checksum = 0;
encrypted.round2_seed = random_object<uint32_t>();
encrypted.checksum = crc32(&encrypted, sizeof(encrypted));
PSOV2Encryption round2_crypt(encrypted.round2_seed);
round2_crypt.encrypt_big_endian(&encrypted, offsetof(StructT, round2_seed));
if (IsBigEndian) {
round2_crypt.encrypt_big_endian(&encrypted, offsetof(StructT, round2_seed));
} else {
round2_crypt.encrypt(&encrypted, offsetof(StructT, round2_seed));
}
return encrypt_gci_or_vms_v2_data_section<true>(
&encrypted, sizeof(StructT), round1_seed);
return encrypt_data_section<IsBigEndian>(&encrypted, sizeof(StructT), round1_seed);
}
std::string decrypt_gci_fixed_size_data_section_for_salvage(
const void* data_section,
size_t size,
uint32_t round1_seed,
uint64_t round2_seed,
size_t max_decrypt_bytes);
uint32_t compute_psogc_timestamp(
uint16_t year,
uint8_t month,
@@ -400,3 +527,80 @@ uint32_t compute_psogc_timestamp(
uint8_t hour,
uint8_t minute,
uint8_t second);
struct PSOPCCreationTimeFile { // PSO______FLS
// The game creates this file if necessary and fills it with random data.
// Most of the random data appears to be a decoy; only one field is used.
// As in other PSO versions, creation_timestamp is used as an encryption key
// for the other save files, but only if the serial number isn't set in the
// Windows registry.
/* 0000 */ parray<uint8_t, 0x624> unused1;
/* 0624 */ le_uint32_t creation_timestamp;
/* 0628 */ parray<uint8_t, 0xDD8> unused2;
/* 1400 */
} __attribute__((packed));
struct PSOPCSystemFile { // PSO______COM
/* 0000 */ le_uint32_t checksum;
// Most of these fields are guesses based on the format used in GC and the
// assumption that Sega didn't change much between versions.
/* 0004 */ le_int16_t music_volume;
/* 0006 */ int8_t sound_volume;
/* 0007 */ uint8_t language;
/* 0008 */ le_uint32_t server_time_delta_frames;
/* 000C */ parray<le_uint16_t, 0x10> unknown_a4; // Last one is always 0x1234?
/* 002C */ parray<uint8_t, 0x100> event_flags;
/* 012C */ le_uint32_t round1_seed;
/* 0130 */ parray<uint8_t, 0xD0> end_padding;
/* 0200 */
} __attribute__((packed));
struct PSOPCGuildCardFile { // PSO______GUD
/* 0000 */ le_uint32_t checksum;
// TODO: Figure out the PC guild card format.
/* 0004 */ parray<uint8_t, 0x7980> unknown_a1;
/* 7984 */ le_uint32_t creation_timestamp;
/* 7988 */ le_uint32_t round2_seed;
/* 798C */ parray<uint8_t, 0x74> end_padding;
/* 7A00 */
} __attribute__((packed));
struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD
/* 00000 */ le_uint32_t signature; // 'CAEN' (stored as 4E 45 41 43)
/* 00004 */ le_uint32_t extra_headers; // 1
/* 00008 */ le_uint32_t num_entries; // 0x80
/* 0000C */ le_uint32_t entry_size; // 0x1D54 (actual entry size is +0x40)
/* 00010 */ parray<uint8_t, 0x430> unknown_a1;
struct CharacterEntry {
/* 0000 */ le_uint32_t present; // 1 if character present, 0 if empty
struct Character {
/* 0000 */ le_uint32_t checksum;
/* 0004 */ PlayerInventory inventory;
/* 0350 */ PlayerDispDataDCPCV3 disp;
/* 0420 */ be_uint32_t flags;
/* 0424 */ be_uint32_t creation_timestamp;
/* 0428 */ be_uint32_t signature; // == 0x6C5D889E?
/* 042C */ be_uint32_t play_time_seconds;
/* 0430 */ be_uint32_t option_flags; // TODO: document bits in this field
/* 0434 */ be_uint32_t save_count;
// TODO: Figure out what this is. On GC, this is where the bank data goes.
/* 0438 */ parray<uint8_t, 0x7D4> unknown_a2;
/* 0C0C */ GuildCardPC guild_card;
/* 0CFC */ parray<PSOPCSaveFileSymbolChatEntry, 12> symbol_chats;
// TODO: Figure out what this is. On GC, this is where chat shortcuts and
// challenge/battle records go.
/* 123C */ parray<uint8_t, 0xAA0> unknown_a3;
/* 1CDC */ parray<le_uint16_t, 20> tech_menu_shortcut_entries;
/* 1D04 */ parray<uint8_t, 0x2C> unknown_a4;
/* 1D30 */ ptext<char, 0x10> serial_number; // As %08X (not decimal)
/* 1D40 */ ptext<char, 0x10> access_key; // As decimal
/* 1D50 */ le_uint32_t round2_seed;
/* 1D54 */
} __attribute__((packed));
/* 0004 */ Character character;
/* 1D58 */ parray<uint8_t, 0x3C> unused;
/* 1D94 */
} __attribute__((packed));
/* 00440 */ parray<CharacterEntry, 0x80> entries;
/* ECE40 */
} __attribute__((packed));
+456 -365
View File
File diff suppressed because it is too large Load Diff
+39 -64
View File
@@ -55,7 +55,7 @@ inline void send_command_excluding_client(std::shared_ptr<Lobby> l,
void send_command_if_not_loading(std::shared_ptr<Lobby> l,
uint16_t command, uint32_t flag, const void* data, size_t size);
inline void send_command_if_not_loading(std::shared_ptr<Lobby> l,
uint16_t command, uint32_t flag, const string& data) {
uint16_t command, uint32_t flag, const std::string& data) {
send_command_if_not_loading(l, command, flag, data.data(), data.size());
}
template <typename StructT>
@@ -127,18 +127,13 @@ prepare_server_init_contents_bb(
const parray<uint8_t, 0x30>& server_key,
const parray<uint8_t, 0x30>& client_key,
uint8_t flags);
void send_server_init(
std::shared_ptr<ServerState> s,
std::shared_ptr<Client> c,
uint8_t flags);
void send_server_init(std::shared_ptr<Client> c, uint8_t flags);
void send_update_client_config(std::shared_ptr<Client> c);
void empty_function_call_response_handler(uint32_t, uint32_t);
void send_quest_buffer_overflow(
std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void prepare_client_for_patches(
std::shared_ptr<ServerState> s, std::shared_ptr<Client> c, std::function<void()> on_complete);
void send_quest_buffer_overflow(std::shared_ptr<Client> c);
void prepare_client_for_patches(std::shared_ptr<Client> c, std::function<void()> on_complete);
void send_function_call(
Channel& ch,
uint64_t client_flags,
@@ -190,7 +185,7 @@ void send_ship_info(Channel& ch, 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_text_message(std::shared_ptr<ServerState> s, const std::u16string& text);
std::u16string prepare_chat_message(
GameVersion version,
@@ -213,7 +208,7 @@ void send_chat_message(
std::shared_ptr<Client> c,
uint32_t from_guild_card_number,
const std::u16string& from_name,
const u16string& text,
const std::u16string& text,
char private_flags);
void send_simple_mail(
std::shared_ptr<Client> c,
@@ -235,10 +230,9 @@ __attribute__((format(printf, 2, 3))) void send_text_message_printf(
__attribute__((format(printf, 2, 3))) void send_ep3_text_message_printf(
std::shared_ptr<ServerState> s, const char* format, ...);
void send_info_board(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
void send_info_board(std::shared_ptr<Client> c);
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);
@@ -246,41 +240,35 @@ void send_card_search_result(
void send_guild_card(
Channel& ch,
uint32_t guild_card_number,
const u16string& name,
const u16string& team_name,
const u16string& description,
const std::u16string& name,
const std::u16string& team_name,
const std::u16string& description,
uint8_t section_id,
uint8_t char_class);
void send_guild_card(std::shared_ptr<Client> c, std::shared_ptr<Client> source);
void send_menu(std::shared_ptr<Client> c, std::shared_ptr<const Menu> menu, bool is_info_menu = false);
void send_game_menu(
std::shared_ptr<Client> c,
std::shared_ptr<ServerState> s,
bool is_spectator_team_list,
bool is_tournament_game_list);
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);
void send_quest_menu(std::shared_ptr<Client> c, uint32_t menu_id,
std::shared_ptr<const QuestCategoryIndex> category_index, uint8_t flags);
void send_lobby_list(std::shared_ptr<Client> c, std::shared_ptr<ServerState> s);
void send_lobby_list(std::shared_ptr<Client> c);
void send_player_records(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l, std::shared_ptr<Client> joining_client = nullptr);
void send_join_lobby(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
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_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_execute_item_trade(std::shared_ptr<Client> c,
const std::vector<ItemData>& items);
void send_execute_card_trade(std::shared_ptr<Client> c,
const std::vector<std::pair<uint32_t, uint32_t>>& card_to_count);
void send_execute_item_trade(std::shared_ptr<Client> c, const std::vector<ItemData>& items);
void send_execute_card_trade(std::shared_ptr<Client> c, const std::vector<std::pair<uint32_t, uint32_t>>& card_to_count);
void send_arrow_update(std::shared_ptr<Lobby> l);
void send_resume_game(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> ready_client);
void send_resume_game(std::shared_ptr<Lobby> l, std::shared_ptr<Client> ready_client);
enum PlayerStatsChange {
SUBTRACT_HP = 0,
@@ -290,8 +278,7 @@ enum PlayerStatsChange {
ADD_TP = 4,
};
void send_player_stats_change(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
PlayerStatsChange stat, uint32_t amount);
void send_player_stats_change(std::shared_ptr<Client> c, PlayerStatsChange stat, uint32_t amount);
void send_player_stats_change(
Channel& ch, uint16_t client_id, PlayerStatsChange stat, uint32_t amount);
void send_warp(Channel& ch, uint8_t client_id, uint32_t area, bool is_private);
@@ -299,9 +286,8 @@ void send_warp(std::shared_ptr<Client> c, uint32_t area, bool is_private);
void send_warp(std::shared_ptr<Lobby> l, uint32_t area, bool is_private);
void send_ep3_change_music(Channel& ch, uint32_t song);
void send_set_player_visibility(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c, bool visible);
void send_revive_player(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c);
void send_set_player_visibility(std::shared_ptr<Client> c, bool visible);
void send_revive_player(std::shared_ptr<Client> c);
void send_drop_item(Channel& ch, const ItemData& item,
bool from_enemy, uint8_t area, float x, float z, uint16_t request_id);
@@ -311,37 +297,34 @@ void send_drop_stacked_item(Channel& ch, const ItemData& item,
uint8_t area, float x, float z);
void send_drop_stacked_item(std::shared_ptr<Lobby> l, const ItemData& item,
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,
const ItemData& item);
void send_destroy_item(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
uint32_t item_id, uint32_t amount);
void send_item_identify_result(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c);
void send_pick_up_item(std::shared_ptr<Client> c, uint32_t id, uint8_t area);
void send_create_inventory_item(std::shared_ptr<Client> c, const ItemData& item);
void send_destroy_item(std::shared_ptr<Client> c, uint32_t item_id, uint32_t amount);
void send_item_identify_result(std::shared_ptr<Client> c);
void send_bank(std::shared_ptr<Client> c);
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_level_up(std::shared_ptr<Client> c);
void send_give_experience(std::shared_ptr<Client> c, uint32_t amount);
void send_set_exp_multiplier(std::shared_ptr<Lobby> l);
void send_rare_enemy_index_list(std::shared_ptr<Client> c, const std::vector<size_t>& indexes);
void send_ep3_card_list_update(
std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void send_quest_function_call(Channel& ch, uint16_t function_id);
void send_quest_function_call(std::shared_ptr<Client> c, uint16_t function_id);
void send_ep3_card_list_update(std::shared_ptr<Client> c);
void send_ep3_media_update(
std::shared_ptr<Client> c,
uint32_t type,
uint32_t which,
const std::string& compressed_data);
void send_ep3_rank_update(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void send_ep3_rank_update(std::shared_ptr<Client> c);
void send_ep3_card_battle_table_state(std::shared_ptr<Lobby> l, uint16_t table_number);
void send_ep3_set_context_token(std::shared_ptr<Client> c, uint32_t context_token);
void send_ep3_confirm_tournament_entry(
std::shared_ptr<ServerState> s,
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament> t);
void send_ep3_tournament_list(
std::shared_ptr<ServerState> s,
std::shared_ptr<Client> c,
bool is_for_spectator_team_create);
void send_ep3_tournament_entry_list(
@@ -351,23 +334,17 @@ void send_ep3_tournament_entry_list(
void send_ep3_tournament_info(
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament> t);
void send_ep3_set_tournament_player_decks(
std::shared_ptr<ServerState> s,
std::shared_ptr<Lobby> l,
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament::Match> match);
void send_ep3_tournament_match_result(
std::shared_ptr<ServerState> s,
std::shared_ptr<Lobby> l,
std::shared_ptr<const Episode3::Tournament::Match> match);
void send_ep3_set_tournament_player_decks(std::shared_ptr<Client> c);
void send_ep3_tournament_match_result(std::shared_ptr<Lobby> l, uint32_t meseta_reward);
void send_ep3_tournament_details(
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament> t);
void send_ep3_game_details(
std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
void send_ep3_update_spectator_count(std::shared_ptr<Lobby> l);
void send_ep3_update_game_metadata(std::shared_ptr<Lobby> l);
void send_ep3_card_auction(std::shared_ptr<Lobby> l);
void send_ep3_disband_watcher_lobbies(std::shared_ptr<Lobby> primary_l);
// Pass mask_key = 0 to unmask the command
void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key);
@@ -386,16 +363,14 @@ void send_open_quest_file(
std::shared_ptr<const std::string> contents,
QuestFileType type);
void send_quest_file_chunk(
shared_ptr<Client> c,
const string& filename,
std::shared_ptr<Client> c,
const std::string& filename,
size_t chunk_index,
const void* data,
size_t size,
bool is_download_quest);
bool send_quest_barrier_if_all_clients_ready(std::shared_ptr<Lobby> l);
void send_card_auction_if_all_clients_ready(
std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l);
bool send_ep3_start_tournament_deck_select_if_all_clients_ready(std::shared_ptr<Lobby> l);
void send_server_time(std::shared_ptr<Client> c);
+31 -27
View File
@@ -38,11 +38,11 @@ void Server::disconnect_client(shared_ptr<Client> c) {
c->id, bufferevent_getfd(c->channel.bev.get()));
}
this->channel_to_client.erase(&c->channel);
this->state->channel_to_client.erase(&c->channel);
c->channel.disconnect();
try {
on_disconnect(this->state, c);
on_disconnect(c);
} catch (const exception& e) {
server_log.warning("Error during client disconnect cleanup: %s", e.what());
}
@@ -91,13 +91,13 @@ void Server::dispatch_on_listen_accept(
socklen);
}
void Server::dispatch_on_listen_error(struct evconnlistener* listener,
void* ctx) {
void Server::dispatch_on_listen_error(
struct evconnlistener* listener, void* ctx) {
reinterpret_cast<Server*>(ctx)->on_listen_error(listener);
}
void Server::on_listen_accept(struct evconnlistener* listener,
evutil_socket_t fd, struct sockaddr*, int) {
void Server::on_listen_accept(
struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr*, int) {
int listen_fd = evconnlistener_get_fd(listener);
ListeningSocket* listening_socket;
@@ -113,18 +113,18 @@ void Server::on_listen_accept(struct evconnlistener* listener,
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->shared_from_this(), bev, listening_socket->version, listening_socket->behavior));
c->game_data.should_save = this->state->allow_saving;
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);
this->state->channel_to_client.emplace(&c->channel, c);
server_log.info("Client connected: C-%" PRIX64 " on fd %d via %d (%s)",
c->id, fd, listen_fd, listening_socket->addr_str.c_str());
try {
on_connect(this->state, c);
on_connect(c);
} catch (const exception& e) {
server_log.warning("Error during client initialization: %s", e.what());
this->disconnect_client(c);
@@ -134,7 +134,7 @@ void Server::on_listen_accept(struct evconnlistener* listener,
void Server::connect_client(
struct bufferevent* bev, uint32_t address, uint16_t client_port,
uint16_t server_port, GameVersion version, ServerBehavior initial_state) {
shared_ptr<Client> c(new Client(bev, version, initial_state));
shared_ptr<Client> c(new Client(this->shared_from_this(), bev, version, initial_state));
c->game_data.should_save = this->state->allow_saving;
c->channel.on_command_received = Server::on_client_input;
c->channel.on_error = Server::on_client_error;
@@ -148,7 +148,7 @@ void Server::connect_client(
name_for_version(version),
name_for_server_behavior(initial_state));
this->channel_to_client.emplace(&c->channel, c);
this->state->channel_to_client.emplace(&c->channel, c);
// Manually set the remote address, since the bufferevent has no fd and the
// Channel constructor can't figure out the virtual remote address
@@ -158,7 +158,7 @@ void Server::connect_client(
remote_sin->sin_port = htons(client_port);
try {
on_connect(this->state, c);
on_connect(c);
} catch (const exception& e) {
server_log.error("Error during client initialization: %s", e.what());
this->disconnect_client(c);
@@ -174,20 +174,20 @@ void Server::on_listen_error(struct evconnlistener* listener) {
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);
shared_ptr<Client> c = server->state->channel_to_client.at(&ch);
if (c->should_disconnect) {
server->disconnect_client(c);
} else {
if (server->state->catch_handler_exceptions) {
try {
on_command(server->state, c, command, flag, data);
on_command(c, command, flag, data);
} catch (const exception& e) {
server_log.warning("Error processing client command: %s", e.what());
c->should_disconnect = true;
}
} else {
on_command(server->state, c, command, flag, data);
on_command(c, command, flag, data);
}
if (c->should_disconnect) {
server->disconnect_client(c);
@@ -197,7 +197,7 @@ void Server::on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::
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);
shared_ptr<Client> c = server->state->channel_to_client.at(&ch);
if (events & BEV_EVENT_ERROR) {
int err = EVUTIL_SOCKET_ERROR();
@@ -233,11 +233,15 @@ void Server::listen(
int port,
GameVersion version,
ServerBehavior behavior) {
int fd = ::listen(addr, port, SOMAXCONN);
string netloc_str = render_netloc(addr, port);
server_log.info("Listening on TCP interface %s on fd %d as %s",
netloc_str.c_str(), fd, addr_str.c_str());
this->add_socket(addr_str, fd, version, behavior);
if (port == 0) {
this->listen(addr_str, addr, version, behavior);
} else {
int fd = ::listen(addr, port, SOMAXCONN);
string netloc_str = render_netloc(addr, port);
server_log.info("Listening on TCP interface %s on fd %d as %s",
netloc_str.c_str(), fd, addr_str.c_str());
this->add_socket(addr_str, fd, version, behavior);
}
}
void Server::listen(const std::string& addr_str, int port, GameVersion version, ServerBehavior behavior) {
@@ -271,13 +275,13 @@ void Server::add_socket(
}
shared_ptr<Client> Server::get_client() const {
if (this->channel_to_client.empty()) {
if (this->state->channel_to_client.empty()) {
throw runtime_error("no clients on game server");
}
if (this->channel_to_client.size() > 1) {
if (this->state->channel_to_client.size() > 1) {
throw runtime_error("multiple clients on game server");
}
return this->channel_to_client.begin()->second;
return this->state->channel_to_client.begin()->second;
}
vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident) const {
@@ -296,7 +300,7 @@ vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident
// TODO: It's kind of not great that we do a linear search here, but this is
// only used in the shell, so it should be pretty rare.
vector<shared_ptr<Client>> results;
for (const auto& it : this->channel_to_client) {
for (const auto& it : this->state->channel_to_client) {
auto c = it.second;
if (c->license && c->license->serial_number == serial_number_dec) {
results.emplace_back(std::move(c));
@@ -306,12 +310,12 @@ vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident
results.emplace_back(std::move(c));
continue;
}
if (c->license && c->license->username == ident) {
if (c->license && c->license->bb_username == ident) {
results.emplace_back(std::move(c));
continue;
}
auto p = c->game_data.player(false);
auto p = c->game_data.player(false, false);
if (p && p->disp.name == u16name) {
results.emplace_back(std::move(c));
continue;
+6 -4
View File
@@ -10,7 +10,7 @@
#include "Client.hh"
#include "ServerState.hh"
class Server {
class Server : public std::enable_shared_from_this<Server> {
public:
Server() = delete;
Server(const Server&) = delete;
@@ -27,12 +27,17 @@ public:
void connect_client(struct bufferevent* bev, uint32_t address,
uint16_t client_port, uint16_t server_port,
GameVersion version, ServerBehavior initial_state);
void disconnect_client(std::shared_ptr<Client> c);
std::shared_ptr<Client> get_client() const;
std::vector<std::shared_ptr<Client>> get_clients_by_identifier(
const std::string& ident) const;
std::shared_ptr<struct event_base> get_base() const;
inline std::shared_ptr<ServerState> get_state() const {
return this->state;
}
private:
std::shared_ptr<struct event_base> base;
std::shared_ptr<struct event> destroy_clients_ev;
@@ -52,7 +57,6 @@ private:
ServerBehavior behavior);
};
std::unordered_map<int, ListeningSocket> listening_sockets;
std::unordered_map<Channel*, std::shared_ptr<Client>> channel_to_client;
std::unordered_set<std::shared_ptr<Client>> clients_to_destroy;
std::shared_ptr<ServerState> state;
@@ -65,8 +69,6 @@ private:
evutil_socket_t fd, struct sockaddr* address, int socklen, void* ctx);
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
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);
+139 -88
View File
@@ -116,11 +116,11 @@ Server commands:\n\
battle-params - reload the enemy stats files\n\
level-table - reload the level-up tables\n\
item-tables - reload the item generation tables\n\
ep3 - reload the Episode 3 card definitions and maps\n\
quests - reindex all quests\n\
ep3 - reload Episode 3 card definitions and maps (not download quests)\n\
quests - reindex all quests (including Episode 3 download quests)\n\
functions - recompile all client-side functions\n\
dol-files - reindex all DOL files\n\
config - reload some fields from config.json\n\
config - reload most fields from config.json\n\
Reloading will not affect items that are in use; for example, if an Episode\n\
3 battle is in progress, it will continue to use the previous map and card\n\
definitions. Similarly, BB clients are not forced to disconnect or reload\n\
@@ -134,7 +134,7 @@ Server commands:\n\
gc-password=<password> (GC password)\n\
access-key=<access-key> (DC/GC/PC access key)\n\
serial=<serial-number> (decimal serial number; required for all licenses)\n\
privileges=<privilege-mask> (can be normal, mod, admin, root, or numeric)\n\
flags=<privilege-mask> (can be normal, mod, admin, root, or numeric)\n\
update-license SERIAL-NUMBER PARAMETERS...\n\
Update an existing license. <serial-number> specifies which license to\n\
update. The options in <parameters> are the same as for the add-license\n\
@@ -161,10 +161,16 @@ Server commands:\n\
and map names, unless the names contain no spaces.\n\
OPTIONS may include:\n\
2v2: Set team size to 2 players (default is 1 without this option)\n\
no-coms: Don\'t add any COM teams to the tournament bracket\n\
shuffle: Shuffle entries when starting the tournament\n\
resize: If the tournament is less than half full when it starts, reduce\n\
the number of rounds to fit the existing entries\n\
dice=MIN-MAX: Set minimum and maximum dice rolls\n\
dice=MIN-MAX:MIN-MAX: Set minimum and maximum dice rolls for ATK and DEF\n\
dice separately\n\
overall-time-limit=N: Set battle time limit (in multiples of 5 minutes)\n\
phase-time-limit=N: Set phase time limit (in seconds)\n\
allowed-cards=ALL/N/NR/NRS: Set rarities of allowed cards\n\
allowed-cards=ALL/N/NR/NRS: Set ranks of allowed cards\n\
deck-shuffle=ON/OFF: Enable/disable deck shuffle\n\
deck-loop=ON/OFF: Enable/disable deck loop\n\
hp=N: Set Story Character initial HP\n\
@@ -181,7 +187,7 @@ Server commands:\n\
start-tournament TOURNAMENT-NAME\n\
End registration for a tournament and allow matches to begin. Quotes are\n\
required around the tournament name unless the name contains no spaces.\n\
tournament-state TOURNAMENT-NAME\n\
describe-tournament TOURNAMENT-NAME\n\
Show the current state of a tournament. Quotes are required around the\n\
tournament name unless the name contains no spaces.\n\
\n\
@@ -197,6 +203,8 @@ Proxy session commands:\n\
c TEXT\n\
chat TEXT\n\
Send a chat message to the server.\n\
wchat DATA\n\
Send a chat message with private_flags on Episode 3.\n\
dchat DATA\n\
Send a chat message to the server with arbitrary data in it.\n\
info-board TEXT\n\
@@ -275,6 +283,8 @@ Proxy session commands:\n\
this->state->load_level_table();
} else if (type == "item-tables") {
this->state->load_item_tables();
} else if (type == "word-select") {
this->state->load_word_select_table();
} else if (type == "ep3") {
this->state->load_ep3_data();
} else if (type == "quests") {
@@ -282,11 +292,13 @@ Proxy session commands:\n\
} else if (type == "functions") {
auto config_json = this->state->load_config();
this->state->compile_functions();
this->state->create_menus(config_json);
} else if (type == "dol-files") {
auto config_json = this->state->load_config();
this->state->load_dol_files();
this->state->create_menus(config_json);
} else if (type == "config") {
auto config_json = this->state->load_config();
this->state->parse_config(config_json, true);
this->state->resolve_ep3_card_names();
} else {
throw invalid_argument("incorrect data type");
}
@@ -300,7 +312,7 @@ Proxy session commands:\n\
if (token.size() >= 32) {
throw invalid_argument("username too long");
}
l->username = token.substr(12);
l->bb_username = token.substr(12);
} else if (starts_with(token, "bb-password=")) {
if (token.size() >= 32) {
@@ -323,18 +335,18 @@ Proxy session commands:\n\
} else if (starts_with(token, "serial=")) {
l->serial_number = stoul(token.substr(7));
} else if (starts_with(token, "privileges=")) {
} else if (starts_with(token, "flags=")) {
string mask = token.substr(11);
if (mask == "normal") {
l->privileges = 0;
l->flags = 0;
} else if (mask == "mod") {
l->privileges = Privilege::MODERATOR;
l->flags = License::Flag::MODERATOR;
} else if (mask == "admin") {
l->privileges = Privilege::ADMINISTRATOR;
l->flags = License::Flag::ADMINISTRATOR;
} else if (mask == "root") {
l->privileges = Privilege::ROOT;
l->flags = License::Flag::ROOT;
} else {
l->privileges = stoul(mask);
l->flags = stoul(mask);
}
} else {
@@ -346,7 +358,8 @@ Proxy session commands:\n\
throw invalid_argument("license does not contain serial number");
}
this->state->license_manager->add(l);
l->save();
this->state->license_index->add(l);
fprintf(stderr, "license added\n");
} else if (command_name == "update-license") {
@@ -356,71 +369,80 @@ Proxy session commands:\n\
}
uint32_t serial_number = stoul(tokens[0]);
tokens.erase(tokens.begin());
auto orig_l = this->state->license_manager->get(serial_number);
auto orig_l = this->state->license_index->get(serial_number);
shared_ptr<License> l(new License(*orig_l));
for (const string& token : tokens) {
if (starts_with(token, "bb-username=")) {
if (token.size() >= 32) {
throw invalid_argument("username too long");
}
l->username = token.substr(12);
this->state->license_index->remove(orig_l->serial_number);
try {
for (const string& token : tokens) {
if (starts_with(token, "bb-username=")) {
if (token.size() >= 32) {
throw invalid_argument("username too long");
}
l->bb_username = token.substr(12);
} else if (starts_with(token, "bb-password=")) {
if (token.size() >= 32) {
throw invalid_argument("bb-password too long");
}
l->bb_password = token.substr(12);
} else if (starts_with(token, "bb-password=")) {
if (token.size() >= 32) {
throw invalid_argument("bb-password too long");
}
l->bb_password = token.substr(12);
} else if (starts_with(token, "gc-password=")) {
if (token.size() > 20) {
throw invalid_argument("gc-password too long");
}
l->gc_password = token.substr(12);
} else if (starts_with(token, "gc-password=")) {
if (token.size() > 20) {
throw invalid_argument("gc-password too long");
}
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");
}
l->access_key = token.substr(11);
} else if (starts_with(token, "access-key=")) {
if (token.size() > 23) {
throw invalid_argument("access-key is too long");
}
l->access_key = token.substr(11);
} else if (starts_with(token, "serial=")) {
l->serial_number = stoul(token.substr(7));
} else if (starts_with(token, "serial=")) {
l->serial_number = stoul(token.substr(7));
} else if (starts_with(token, "flags=")) {
string mask = token.substr(11);
if (mask == "normal") {
l->flags = 0;
} else if (mask == "mod") {
l->flags = License::Flag::MODERATOR;
} else if (mask == "admin") {
l->flags = License::Flag::ADMINISTRATOR;
} else if (mask == "root") {
l->flags = License::Flag::ROOT;
} else {
l->flags = stoul(mask);
}
} else if (starts_with(token, "privileges=")) {
string mask = token.substr(11);
if (mask == "normal") {
l->privileges = 0;
} else if (mask == "mod") {
l->privileges = Privilege::MODERATOR;
} else if (mask == "admin") {
l->privileges = Privilege::ADMINISTRATOR;
} else if (mask == "root") {
l->privileges = Privilege::ROOT;
} else {
l->privileges = stoul(mask);
throw invalid_argument("incorrect field: " + token);
}
} else {
throw invalid_argument("incorrect field: " + token);
}
if (!l->serial_number) {
throw invalid_argument("license does not contain serial number");
}
} catch (const exception&) {
this->state->license_index->add(orig_l);
throw;
}
if (!l->serial_number) {
throw invalid_argument("license does not contain serial number");
}
this->state->license_manager->add(l);
l->save();
this->state->license_index->add(l);
fprintf(stderr, "license updated\n");
} else if (command_name == "delete-license") {
uint32_t serial_number = stoul(command_args);
this->state->license_manager->remove(serial_number);
auto l = this->state->license_index->get(serial_number);
l->delete_file();
this->state->license_index->remove(l->serial_number);
fprintf(stderr, "license deleted\n");
} else if (command_name == "list-licenses") {
for (const auto& l : this->state->license_manager->snapshot()) {
string s = l.str();
for (const auto& l : this->state->license_index->all()) {
string s = l->str();
fprintf(stderr, "%s\n", s.c_str());
}
@@ -451,24 +473,42 @@ Proxy session commands:\n\
} else if (command_name == "create-tournament") {
string name = get_quoted_string(command_args);
string map_name = get_quoted_string(command_args);
auto map = this->state->ep3_map_index->definition_for_name(map_name);
auto map = this->state->ep3_map_index->for_name(map_name);
uint32_t num_teams = stoul(get_quoted_string(command_args), nullptr, 0);
Episode3::Rules rules;
rules.set_defaults();
bool is_2v2 = false;
uint8_t flags = Episode3::Tournament::Flag::HAS_COM_TEAMS;
if (!command_args.empty()) {
auto tokens = split(command_args, ' ');
for (auto& token : tokens) {
token = tolower(token);
if (token == "2v2") {
is_2v2 = true;
flags |= Episode3::Tournament::Flag::IS_2V2;
} else if (token == "no-coms") {
flags &= (~Episode3::Tournament::Flag::HAS_COM_TEAMS);
} else if (token == "shuffle") {
flags |= Episode3::Tournament::Flag::SHUFFLE_ENTRIES;
} else if (token == "resize") {
flags |= Episode3::Tournament::Flag::RESIZE_ON_START;
} else if (starts_with(token, "dice=")) {
auto subtokens = split(token.substr(5), '-');
if (subtokens.size() != 2) {
throw runtime_error("dice option must be of the form dice=X-Y");
auto subtokens = split(token.substr(5), ':');
if (subtokens.size() == 1) {
rules.def_dice_range = 0x00;
} else if (subtokens.size() == 2) {
auto subsubtokens = split(subtokens[1], '-');
if (subsubtokens.size() != 2) {
throw runtime_error("dice option must be of the form dice=A-B or dice=A-B:C-D");
}
rules.def_dice_range = ((stoul(subsubtokens[0]) << 4) & 0xF0) | (stoul(subsubtokens[1]) & 0x0F);
} else {
throw runtime_error("dice option must be of the form dice=A-B or dice=A-B:C-D");
}
rules.min_dice = stoul(subtokens[0]);
rules.max_dice = stoul(subtokens[0]);
auto subsubtokens = split(subtokens[0], '-');
if (subsubtokens.size() != 2) {
throw runtime_error("dice option must be of the form dice=A-B or dice=A-B:C-D");
}
rules.min_dice = stoul(subsubtokens[0]);
rules.max_dice = stoul(subsubtokens[1]);
} else if (starts_with(token, "overall-time-limit=")) {
uint32_t limit = stoul(token.substr(19));
if (limit > 600) {
@@ -531,24 +571,20 @@ Proxy session commands:\n\
fprintf(stderr, "warning: some rules were invalid and reset to defaults\n");
}
auto tourn = this->state->ep3_tournament_index->create_tournament(
name, map, rules, num_teams, is_2v2);
this->state->ep3_tournament_index->save();
fprintf(stderr, "created tournament %02hhX\n", tourn->get_number());
name, map, rules, num_teams, flags);
fprintf(stderr, "created tournament \"%s\"\n", tourn->get_name().c_str());
} else if (command_name == "delete-tournament") {
string name = get_quoted_string(command_args);
auto tourn = this->state->ep3_tournament_index->get_tournament(name);
if (tourn) {
this->state->ep3_tournament_index->delete_tournament(tourn->get_number());
this->state->ep3_tournament_index->save();
if (this->state->ep3_tournament_index->delete_tournament(name)) {
fprintf(stderr, "tournament deleted\n");
} else {
fprintf(stderr, "no such tournament exists\n");
}
} else if (command_name == "list-tournaments") {
for (const auto& tourn : this->state->ep3_tournament_index->all_tournaments()) {
fprintf(stderr, " %s\n", tourn->get_name().c_str());
for (const auto& it : this->state->ep3_tournament_index->all_tournaments()) {
fprintf(stderr, " %s\n", it.second->get_name().c_str());
}
} else if (command_name == "start-tournament") {
@@ -557,13 +593,14 @@ Proxy session commands:\n\
if (tourn) {
tourn->start();
this->state->ep3_tournament_index->save();
tourn->send_all_state_updates();
send_ep3_text_message_printf(this->state, "$C7The tournament\n$C6%s$C7\nhas begun", tourn->get_name().c_str());
fprintf(stderr, "tournament started\n");
} else {
fprintf(stderr, "no such tournament exists\n");
}
} else if (command_name == "tournament-status") {
} else if (command_name == "describe-tournament") {
string name = get_quoted_string(command_args);
auto tourn = this->state->ep3_tournament_index->get_tournament(name);
if (tourn) {
@@ -612,7 +649,7 @@ Proxy session commands:\n\
if (c) {
if (command_name[1] == 's') {
on_command_with_header(this->state, c, data);
on_command_with_header(c, data);
} else {
send_command_with_header(c->channel, data.data(), data.size());
}
@@ -661,6 +698,21 @@ Proxy session commands:\n\
session->server_channel.send(0x06, 0x00, data);
}
} else if ((command_name == "wc") || (command_name == "wchat")) {
auto session = this->get_proxy_session(session_name);
if ((session->version != GameVersion::GC) ||
!(session->newserv_client_config.cfg.flags & Client::Flag::IS_EPISODE_3)) {
throw runtime_error("wchat can only be used on Episode 3");
}
string data(8, '\0');
data.push_back('\x40'); // private_flags: visible to all
data.push_back('\x09');
data.push_back('E');
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_name);
session->server_channel.send(0x89, stoul(command_args));
@@ -769,21 +821,20 @@ Proxy session commands:\n\
throw runtime_error("proxy session is not game leader");
}
PlayerInventoryItem item;
item.data = ItemData(command_args);
item.data.id = random_object<uint32_t>();
ItemData item(command_args);
item.id = random_object<uint32_t>();
if (command_name == "set-next-item") {
session->next_drop_item = item;
string name = session->next_drop_item.data.name(true);
string name = session->next_drop_item.name(true);
send_text_message(session->client_channel, u"$C7Next drop:\n" + decode_sjis(name));
} else {
send_drop_stacked_item(session->client_channel, item.data, session->area, session->x, session->z);
send_drop_stacked_item(session->server_channel, item.data, session->area, session->x, session->z);
send_drop_stacked_item(session->client_channel, item, session->area, session->x, session->z);
send_drop_stacked_item(session->server_channel, item, session->area, session->x, session->z);
string name = item.data.name(true);
string name = item.name(true);
send_text_message(session->client_channel, u"$C7Item created:\n" + decode_sjis(name));
}

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