Compare commits

...

191 Commits

Author SHA1 Message Date
Martin Michelsen c3aca29d9c fix meseta overdraft disconnect bug 2023-03-02 17:06:15 -08:00
Martin Michelsen 94bbd5685e support size disparities in ReplaySession 2023-03-02 16:58:30 -08:00
Martin Michelsen 4a4f06e9ac add format notes from DC NTE disassembly 2023-03-02 16:58:02 -08:00
Martin Michelsen 34afd42391 add .bind files to gitignore in ep3 maps directories 2023-03-02 16:57:37 -08:00
Martin Michelsen 6e80ccca54 document deck restrictions in Ep3 quest format 2023-03-01 00:27:53 -08:00
Martin Michelsen a485c25eb8 enforce ep3 map list size limit at startup time 2023-02-28 22:56:36 -08:00
Martin Michelsen fe5a15a1ab fix description of ep3 map tile 50 2023-02-28 22:56:36 -08:00
Martin Michelsen 203a2aaeb4 fix bug in ep3 map display 2023-02-28 22:56:08 -08:00
Martin Michelsen 78968f86dd document all Ep3 lobby banner positions 2023-02-27 22:38:00 -08:00
Martin Michelsen f1a64e6dbf add ALLOW_FILES flag to parse_data_string calls where needed 2023-02-27 22:37:46 -08:00
Martin Michelsen 215f5deff6 update some format notes 2023-02-27 21:31:46 -08:00
Martin Michelsen 2b959386d7 add show-slots shell command 2023-02-26 16:53:15 -08:00
Martin Michelsen b3ab759717 update chat commands in readme 2023-02-24 09:37:29 -08:00
Martin Michelsen 1595555b53 update ep3 meseta command notes 2023-02-24 09:37:17 -08:00
Martin Michelsen fdee74195b update proxy name option 2023-02-24 09:36:49 -08:00
Martin Michelsen bac429af94 shorten subcommand handler names 2023-02-23 15:56:50 -08:00
Martin Michelsen 8f0a33eb77 fix spectator count when multiple spectator teams exist 2023-02-22 18:03:19 -08:00
Martin Michelsen c7009569b7 fix name mask option 2023-02-22 18:03:19 -08:00
Discordian 0250e3c9e5 Shrink character support 2023-02-22 08:39:48 -08:00
Martin Michelsen fdf7af20bc add some subcommand notes 2023-02-21 20:24:56 -08:00
Martin Michelsen 4ed641e6f4 refine meet user extension structure 2023-02-21 20:24:42 -08:00
Martin Michelsen 9ff23b2aee more info on 0E command 2023-02-21 18:17:57 -08:00
Martin Michelsen 34812d5037 fix choice search config format 2023-02-21 18:17:57 -08:00
Martin Michelsen 79b0e82c50 send card defs timestamp in 6xB4x46 2023-02-21 18:17:57 -08:00
Martin Michelsen 43395492b2 add more subcommand documentation 2023-02-21 18:17:57 -08:00
Martin Michelsen 97172717da add $song on proxy server 2023-02-21 18:17:57 -08:00
Martin Michelsen 078fd4ac08 update comment in ep3 player config 2023-02-21 18:17:57 -08:00
Martin Michelsen fc6a26ee38 document player references in ep3 player data 2023-02-21 18:17:57 -08:00
Martin Michelsen 32c08032c5 use ptext in ep3 player data format 2023-02-21 18:17:57 -08:00
Discordian 138d2609a2 Add emote support in lobbies 2023-02-21 15:57:52 -08:00
Martin Michelsen 37438c94c7 document choice search in 61 command 2023-02-18 22:45:10 -08:00
Martin Michelsen ca551039ce rename v2/v3 crypt base class 2023-02-18 12:28:59 -08:00
Martin Michelsen bfdb6c0695 auto-decrypt episode 3 player config on proxy server 2023-02-17 23:53:35 -08:00
Martin Michelsen 1394dd681e hide patches from menu if they should only run in lobby/game 2023-02-17 22:07:09 -08:00
Martin Michelsen ba4a017ffb add patch to get all cards in Ep3 2023-02-17 21:50:13 -08:00
Martin Michelsen d5773b93da factor out Ep3 USA check in patches 2023-02-17 21:48:10 -08:00
Martin Michelsen bebb69649c update some command notes 2023-02-17 21:47:47 -08:00
Martin Michelsen 4946978ed7 add blank name option 2023-02-17 21:47:31 -08:00
Martin Michelsen 1eba82c739 make checksum style consistent 2023-02-11 20:05:47 -08:00
Martin Michelsen ef11592439 fix default value for new proxy option 2023-02-11 20:05:34 -08:00
Martin Michelsen 6955b7ea0c update quest format compatibility table 2023-02-10 12:53:56 -08:00
Martin Michelsen 95c1b4b6e8 add support for decoding download QST files 2023-02-10 11:03:03 -08:00
Martin Michelsen 3bb061951d add name color proxy option 2023-02-10 10:48:02 -08:00
Martin Michelsen 649246cda2 apply proxy rewrites to 98 as well as 61 2023-02-09 22:07:49 -08:00
Martin Michelsen ca439c7a0f fix incorrect version check 2023-02-06 22:58:16 -08:00
Martin Michelsen e9899a33a2 fix item usage tracking on PC 2023-02-06 22:26:25 -08:00
Martin Michelsen 6ced274108 update $exit note in readme 2023-02-05 14:48:54 -08:00
Martin Michelsen 6ffeda93a7 make $li output consistent on proxy server 2023-02-04 19:52:38 -08:00
Martin Michelsen c45246c1b5 implement spectator team tracking properly on proxy server 2023-02-04 19:51:15 -08:00
Martin Michelsen 8582e18861 add $exit on game server 2023-02-04 19:51:00 -08:00
Martin Michelsen ed770a8b74 fix chat shell command for pc and bb 2023-02-03 20:54:13 -08:00
Martin Michelsen 3cf309a008 create update-license shell command 2023-02-03 20:43:53 -08:00
Martin Michelsen d1a830040f fix interaction mode for join game errors 2023-02-03 20:16:11 -08:00
Martin Michelsen 64d7ec5cde fix item tracking in battle/challenge modes 2023-02-02 20:02:15 -08:00
Martin Michelsen 77f919980a don't disconnect players when creating a game of too low level 2023-02-02 19:49:18 -08:00
Martin Michelsen c5f4f2907e update some ep3 map format notes 2023-02-02 13:36:57 -08:00
Martin Michelsen 0ffa03d2b6 fix session hang on empty download quest menu 2023-02-01 10:22:06 -08:00
Martin Michelsen 1fdbcd6c4e add incomplete vms decoder 2023-02-01 10:22:06 -08:00
Martin Michelsen ec453d1fa8 block auction commands in non-Ep3 proxy sessions 2023-01-28 09:19:30 -08:00
Martin Michelsen a631fd50b4 update modification tile notes in ep3 map format 2023-01-28 09:19:06 -08:00
Martin Michelsen 8affe23c0d fix note in readme 2023-01-26 22:43:49 -08:00
Martin Michelsen 8cf11b3c48 fix send_function_call for JP Ep3 and v1.04 2023-01-26 19:29:09 -08:00
Martin Michelsen c39e60af8b document unused fields in 6x68 command 2023-01-26 19:29:09 -08:00
Martin Michelsen 194ed550e1 send tournament confirmation at login even if client is unregistered 2023-01-24 21:57:23 -08:00
Martin Michelsen ef0b72e95b document 6xBE field as unused 2023-01-24 21:54:12 -08:00
Martin Michelsen f3481fbd9f make chat filter also apply to info board on proxy server 2023-01-22 22:54:56 -08:00
Martin Michelsen 39d394cfae add $sc and $ss commands 2023-01-22 22:54:29 -08:00
Martin Michelsen 1b0f6cccf6 add option to disable chat commands on proxy server 2023-01-22 21:31:21 -08:00
Martin Michelsen 37c8491dc3 fix test-compression paths for github actions env 2023-01-22 15:32:46 -08:00
Martin Michelsen e364ce2d9c add bytes/sec in compression action log output 2023-01-22 15:23:56 -08:00
Martin Michelsen 15bbaa0837 update test for new $li format 2023-01-22 15:11:29 -08:00
Martin Michelsen edf234c0ff fix typo in error message 2023-01-22 12:14:15 -08:00
Martin Michelsen 4b63475662 clean up $li output 2023-01-21 21:36:39 -08:00
Martin Michelsen 4da71e127d restore deleted item functionality 2023-01-21 21:36:39 -08:00
Martin Michelsen 9d688c2092 fix compression test path 2023-01-21 09:25:25 -08:00
Martin Michelsen d669f7ce6c improve PRS efficiency further 2023-01-21 09:20:06 -08:00
Martin Michelsen b02c82bb0d make PRS and BC0 compression deterministic across environments 2023-01-19 19:21:52 -08:00
Martin Michelsen a4f52b9b22 support .bin/.bind files in ep3 maps directories 2023-01-19 19:12:37 -08:00
Martin Michelsen 9b136d9444 make $item more powerful 2023-01-19 19:12:12 -08:00
Martin Michelsen 7a5e759d9a optimize BC0 compressor 2023-01-17 22:58:38 -08:00
Martin Michelsen f923f51c22 fix ep3 game test for new PRS compression 2023-01-17 22:58:38 -08:00
Martin Michelsen 133ca0b3cc make PRS compression faster & more efficient 2023-01-17 22:02:24 -08:00
Martin Michelsen a937e50681 clean up some CLI option handling 2023-01-17 21:06:44 -08:00
Martin Michelsen b5b7345e5f list non-server behaviors in readme 2023-01-13 08:27:26 -08:00
Martin Michelsen 61751d681e explicitly initialize all TCPConnection fields in IPStackSimulator 2023-01-08 13:55:48 -08:00
Martin Michelsen 9ac01875fb fix potential uninitialized memory access 2023-01-08 13:47:56 -08:00
Martin Michelsen d076838747 fix implicit ptext length conversion 2023-01-08 09:01:14 -08:00
Martin Michelsen 8c5160e36f add some ep3 error debug messages 2023-01-08 09:00:56 -08:00
Martin Michelsen e77228fa97 clear client tournament state when starting proxy session 2023-01-07 09:07:04 -08:00
Martin Michelsen 517a735ab2 add more info to $li on proxy server 2023-01-07 09:06:45 -08:00
Martin Michelsen 353614e65c fix B1 command automask for running tests in 2023 2023-01-01 00:15:17 -08:00
Martin Michelsen d337517317 fix proxy option description formatting 2023-01-01 00:10:09 -08:00
Martin Michelsen 3c7b652f3a fix typo in static game data 2022-12-31 09:34:11 -08:00
Martin Michelsen cb11677214 fix proxy player data handling bug 2022-12-31 00:06:32 -08:00
Martin Michelsen 1dbdd3f191 add infinite ep3 meseta and ability to save media updates 2022-12-30 23:05:50 -08:00
Martin Michelsen 007e439281 remove TODO about battle tables 2022-12-30 23:05:20 -08:00
Martin Michelsen 350afbb436 update misc command notes 2022-12-30 14:29:13 -08:00
Martin Michelsen 08386c4019 more tournament command details 2022-12-30 14:11:04 -08:00
Martin Michelsen f57f903207 fix ep3 command debug log in terminal 2022-12-30 12:57:54 -08:00
Martin Michelsen 6727a25df0 skip implemented subcommand check for server-origin commands 2022-12-30 12:57:42 -08:00
Martin Michelsen a57b6ce57b ep3 debugging helpers 2022-12-30 00:33:20 -08:00
Martin Michelsen b52700c08e document ItemPT format 2022-12-29 19:54:29 -08:00
Martin Michelsen 68abac4fd4 support big-endian GSL archives 2022-12-29 15:02:29 -08:00
Martin Michelsen 52db9008a8 implement ss shell command on game server 2022-12-28 00:30:00 -08:00
Martin Michelsen eb2463a820 change tournament match title for final match 2022-12-26 23:58:04 -08:00
Martin Michelsen dfad80eb9a enable most shell commands to affect a specific session 2022-12-26 21:33:38 -08:00
Martin Michelsen de7239e3fb add color to info board text on proxy server 2022-12-26 18:57:28 -08:00
Martin Michelsen d6256183b5 describe text escapes in CommandFormats.hh 2022-12-26 16:45:24 -08:00
Martin Michelsen dbfb088630 sort proxy destinations by name 2022-12-26 10:22:16 -08:00
Martin Michelsen 3bb33a4de7 don't send spectator commands during loading 2022-12-25 21:21:39 -08:00
Martin Michelsen 5a25c3e865 describe tournament 2v2 option in shell help text 2022-12-25 19:50:58 -08:00
Martin Michelsen 007359e220 block time updates on proxy server 2022-12-25 15:46:10 -08:00
Martin Michelsen 5094db1306 show player & game count at main menu 2022-12-25 15:46:10 -08:00
Martin Michelsen be5d85fa04 add 6xB4x3B command when joining spectator team during battle 2022-12-25 15:42:57 -08:00
Martin Michelsen 2ff3f8b4fb show progress during slow prs and bc0 compression 2022-12-22 23:46:18 -08:00
Martin Michelsen 090379e520 make data output behavior more reasonable 2022-12-22 22:49:42 -08:00
Martin Michelsen f3dfa0989f don't bother with lobby id free list 2022-12-22 22:27:23 -08:00
Martin Michelsen c8b89a7cad make ep3 data index hot-reloadable 2022-12-22 22:23:22 -08:00
Martin Michelsen 1042b8df46 remove unneeded TODO 2022-12-22 22:20:42 -08:00
Martin Michelsen afacf72034 rename game server handlers for better searchability 2022-12-22 22:19:47 -08:00
Martin Michelsen 53938cf6a6 fix typo in command format notes 2022-12-22 21:27:31 -08:00
Martin Michelsen f2751a4e49 remove custom login options from proxy options menu 2022-12-22 21:27:31 -08:00
Martin Michelsen 7c98f42722 implement ep3 online quests 2022-12-19 23:56:43 -08:00
Martin Michelsen 5175c50945 document part of 6xB2 command 2022-12-19 23:56:43 -08:00
Martin Michelsen 13c438273b update some command format notes 2022-12-18 12:24:19 -08:00
Martin Michelsen 99c8d9957a fix card id in card text parser 2022-12-17 16:09:28 -08:00
Martin Michelsen a28ef86c60 fix some mistakes in readme 2022-12-17 16:08:55 -08:00
Martin Michelsen aa19fd347e add some TODOs 2022-12-17 10:31:55 -08:00
Martin Michelsen e5a9b1f330 fix negative remaining_turns in ep3 server 2022-12-17 01:16:48 -08:00
Martin Michelsen 2eb4770bdd fix handling of stray ep3 lobby counter state commands 2022-12-17 01:16:48 -08:00
Martin Michelsen a6ac56943c fix invalid command proxy message 2022-12-17 01:16:48 -08:00
Martin Michelsen d288fca087 fix proxy block events option 2022-12-17 01:15:35 -08:00
Martin Michelsen 889913400a fix some format notes 2022-12-16 21:13:42 -08:00
Martin Michelsen 5e2a42d852 rename scene_data2 to environment_number 2022-12-16 21:07:33 -08:00
Martin Michelsen abd2fb9e92 add tournament-state.json to gitignore 2022-12-16 21:07:32 -08:00
Martin Michelsen 5625999a90 add exit command in proxy sessions 2022-12-16 19:51:02 -08:00
Martin Michelsen 08dfbbcb5c factor out client and proxy options 2022-12-15 23:34:07 -08:00
Martin Michelsen 224e0df87e handle stray server data commands 2022-12-15 12:54:29 -08:00
Martin Michelsen 1bb0545b21 fix battle table edge case 2022-12-15 00:05:44 -08:00
Martin Michelsen 27cdf7e078 fix incorrect behavior when attempting to start non-pending tournament match 2022-12-14 23:35:08 -08:00
Martin Michelsen c01d1f623c use log levels for suppressing ip stack simulator output 2022-12-14 23:34:45 -08:00
Martin Michelsen 7612621fe9 unmask ep3 commands on proxy server 2022-12-14 20:37:48 -08:00
Martin Michelsen fa95a2f6d8 implement battle tables 2022-12-14 20:37:34 -08:00
Martin Michelsen 0b17b7174f skip wait phase if there's only one client in tournament match 2022-12-14 17:58:25 -08:00
Martin Michelsen cf2f1ef529 add option to disable save_files globally 2022-12-13 23:53:06 -08:00
Martin Michelsen ae49ca0189 add DISABLE_INTERFERENCE behavior flag 2022-12-13 23:39:32 -08:00
Martin Michelsen 79374d3dd1 make tournament entry details cleaner 2022-12-13 23:39:32 -08:00
Martin Michelsen 846401469e clean up some format notes 2022-12-13 23:38:36 -08:00
Martin Michelsen 6f11410107 fix tournament player positions on b team 2022-12-13 22:25:48 -08:00
Martin Michelsen 025556ecd3 restrict tournament trigger to a specific battle table 2022-12-13 22:10:08 -08:00
Martin Michelsen 5bcd16b6f2 make tournaments work with multiple human players 2022-12-13 21:40:09 -08:00
Martin Michelsen d52b882679 fix team count in tournament status command 2022-12-12 22:03:45 -08:00
Martin Michelsen 0d7f69eb66 implement spectator count view in primary game 2022-12-12 21:57:37 -08:00
Martin Michelsen 391a70f68d send tournament bracket updates when any match is complete 2022-12-12 21:54:53 -08:00
Martin Michelsen e858b2101d implement hack to make tournament specatators work 2022-12-12 00:42:39 -08:00
Martin Michelsen ed2568fc7a more ep3 comamnd details 2022-12-11 23:02:14 -08:00
Martin Michelsen 9a2ed4c5ec fix most assist cards 2022-12-11 22:46:13 -08:00
Martin Michelsen 398a93b56f implement spectator teams 2022-12-11 13:57:57 -08:00
Martin Michelsen cceaf5efde implement ep3 extended game/tournament info commands 2022-12-11 11:04:11 -08:00
Martin Michelsen 14639c63e3 name interference functions appropriately 2022-12-10 21:48:09 -08:00
Martin Michelsen 2ee7ca8600 fix quest barrier and implement v3/bb file chunk acknowledge commands 2022-12-10 10:02:19 -08:00
Martin Michelsen e800fd3fff fix prs_decompress_size 2022-12-10 09:19:43 -08:00
Martin Michelsen fb4aa0df22 persist tournament state across server restarts 2022-12-10 00:13:49 -08:00
Martin Michelsen b0a32600be add note about COM EX values 2022-12-09 18:17:08 -08:00
Martin Michelsen 12caf95f5d add offline decks to default tournament set 2022-12-09 00:33:22 -08:00
Martin Michelsen c3192bb398 fix tournament registration bug after disconnect 2022-12-09 00:33:10 -08:00
Martin Michelsen 8323c5e0af add ep3 ex value command for tournament matches 2022-12-08 21:44:12 -08:00
Martin Michelsen bdff48c343 fix some tournament state bugs 2022-12-08 18:38:46 -08:00
Martin Michelsen 5f04cbaecb fix results screen for final tournament match 2022-12-08 17:32:39 -08:00
Martin Michelsen 93f42a9398 automatically delete tournaments when complete 2022-12-08 17:32:39 -08:00
Martin Michelsen 2eacaa993e add note about tournament state 2022-12-08 10:13:49 -08:00
Martin Michelsen 9bb168b693 use bare array instead of parray in tournament index 2022-12-08 01:06:00 -08:00
Martin Michelsen 9a1ba56982 implement episode 3 tournaments 2022-12-08 01:01:58 -08:00
Martin Michelsen 8c2ea48b80 fix use-after-free when client disconnects intentionally 2022-12-05 21:04:43 -08:00
Martin Michelsen d4115450b2 make binary and disassembly filenames match 2022-12-03 21:33:08 -08:00
Martin Michelsen fd8f968994 document some ep3 tournament command fields 2022-12-03 18:38:17 -08:00
Martin Michelsen 7634e61400 temporarily disable BB test again 2022-12-03 12:32:08 -08:00
Martin Michelsen 1a7981dff5 remove context_token logic 2022-12-03 12:20:45 -08:00
Martin Michelsen c3c6f60664 document more ep3 commands 2022-12-03 12:14:58 -08:00
Martin Michelsen 421f27d63c document 6xB4x4B command 2022-12-03 11:06:31 -08:00
Martin Michelsen c314cb7cec fix E3 command format 2022-12-03 11:06:31 -08:00
Martin Michelsen 9f4b53178a add jsd0 2022-12-03 11:06:31 -08:00
Martin Michelsen 85fbd1b389 rename some unknown fields 2022-12-03 11:06:31 -08:00
Martin Michelsen 4f57ea30a1 enable BB test 2022-12-02 10:15:05 -08:00
Martin Michelsen 1ea44ac55c add heuristic-based trivial encryption basis finder 2022-12-01 21:41:15 -08:00
Martin Michelsen d44be66958 document some of ep3 extra player data format 2022-12-01 21:40:45 -08:00
Martin Michelsen 1a5d2537ad document --decrypt-trivial-data option 2022-12-01 19:21:32 -08:00
Martin Michelsen f68308a242 fix ep3 test 2022-11-30 23:53:25 -08:00
Martin Michelsen f622c9c91e update some documentation 2022-11-30 23:40:12 -08:00
Martin Michelsen 0828029051 optimize images 2022-11-30 23:25:45 -08:00
Martin Michelsen 2e3089cb10 make replay commands more usable 2022-11-30 23:23:17 -08:00
109 changed files with 8886 additions and 3120 deletions
+3
View File
@@ -15,6 +15,9 @@ Testing
# Files modified by the user and/or server that don't have defaults
system/config.json
system/ep3/tournament-state.json
system/ep3/maps-free/*.bind
system/ep3/maps-quest/*.bind
system/licenses.nsi
system/players/player_*
system/players/account_*
+7 -3
View File
@@ -20,8 +20,6 @@ list(APPEND CMAKE_PREFIX_PATH ${LOCAL_LIB_DIR})
include_directories(${LOCAL_INCLUDE_DIR})
link_directories(${LOCAL_LIB_DIR})
set(CMAKE_BUILD_TYPE Debug)
# Library search
@@ -59,6 +57,7 @@ add_executable(newserv
src/Episode3/PlayerStateSubordinates.cc
src/Episode3/RulerServer.cc
src/Episode3/Server.cc
src/Episode3/Tournament.cc
src/FileContentsCache.cc
src/FunctionCompiler.cc
src/GSLArchive.cc
@@ -117,9 +116,14 @@ foreach(TestCase IN ITEMS ${TestCases})
add_test(
NAME ${TestCase}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_BINARY_DIR}/newserv --replay-log ${TestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json --require-password=password --require-access-key=111111111111)
COMMAND ${CMAKE_BINARY_DIR}/newserv replay-log ${TestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json --require-password=password --require-access-key=111111111111)
endforeach()
add_test(
NAME compression
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_SOURCE_DIR}/test-compression.sh ${CMAKE_BINARY_DIR}/newserv)
# Installation configuration
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2018 Martin Michelsen
Copyright (c) 2023 Martin Michelsen
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
+92 -32
View File
@@ -29,8 +29,8 @@ 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:
- Support disconnect hooks to clean up state, like if a client disconnects during quest loading or during a trade window execution.
- Episode 3 battles are implemented but are not well-tested.
- Fix behavior when joining a spectator team after the beginning of a battle.
- PSOBB is not well-tested and likely will disconnect or misbehave when clients try to use unimplemented features.
- Enemy indexes also desync slightly in most games, often in later areas, leading to incorrect EXP values being given for killed enemies.
- 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 V2/V3).
@@ -40,8 +40,16 @@ Current known issues / missing features / things to do:
- 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.
- The internal menu abstraction is ugly and hard to work with. Rewrite it.
- Add default values for all commands (like we use for Episode 3 battle commands).
- VMS decoding doesn't work. Complete this reverse-engineering project.
- Code style
- The internal menu abstraction is ugly and hard to work with. Rewrite it.
- Add default values for all commands (like we use for Episode 3 battle commands).
- Clean up the way proxy session options are passed to the session from the client object (and add user-settable options for e.g. chat filter, which currently doesn't appear in the menu).
- Episode 3 bugs
- 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.
## Compatibility
@@ -63,16 +71,42 @@ newserv supports several versions of PSO. Specifically:
*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. Please submit a GitHub issue if you have a non-US DC version, and can provide a log from a connection attempt.*
2. *This version only supports the modem adapter, which Dolphin does not currently emulate, so it's difficult to test.*
3. *Episode 3 players can download quests, join lobbies, create and join games, trade cards, and participate in card auctions. CARD battles are also implemented but are not well-tested. Spectator teams and tournaments are not implemented.*
3. *See the following section about Episode 3 functionality.*
4. *newserv's implementation of PSOX is based on disassembly of the client executable; it has never been tested with a real client and most likely doesn't work.*
5. *Some basic features are not implemented in Blue Burst games, so the games are not very playable. A lot of work has to be done to get BB games to a playable state.*
6. *Support for PSO Dreamcast Trial Edition is very incomplete and probably never will be complete. This is really just exploring a curiosity that sheds some light on early network engineering done by Sega, not an actual attempt at supporting this version of the game.*
### Episode 3
The following Episode 3 features are well-tested and work normally:
* Downloading quests.
* Creating and joining games.
* Trading cards.
* Participating in card auctions. (The auction contents must be configured in config.json.)
* Tournaments. (See below)
The following Episode 3 features are implemented, but only partially tested:
* CARD battles. If you find a feature or card ability that doesn't work, please make a GitHub issue and describe the situation (including the attacking card(s), defending card(s), and ability that didn't work).
* Spectator teams are partially implemented, but are not well-tested. There is a known issue that prevents viewing battles unless you're in the spectator team when the battle begins.
* Battle replays sometimes cause the client to crash during the replay. Using the $playrec command is therefore not recommended.
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, 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. (This also means that, for example, not all matches in round 1 must be complete before round 2 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.
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.
* 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.
* 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).
## Usage
Currently newserv should build on macOS and Ubuntu. It will likely work on other Linux flavors too. It should work on Windows as well, but I haven't tested it - the build process could be very manual. Cygwin is likely the easiest Windows environment in which to build newserv.
Currently newserv should build on macOS and Ubuntu. It will likely work on other Linux flavors too. It should work on Windows as well, but I haven't tested it recently - the build process could be very manual. Cygwin is likely the easiest Windows environment in which to build newserv.
There is a probably-not-too-old macOS ARM64 release on the newserv GitHub repository. You may need to install libevent manually even if you use this release (run `brew install libevent`).
There is a fairly recent macOS ARM64 release on the newserv GitHub repository. You may need to install libevent manually even if you use this release (run `brew install libevent`).
If you're using an older AMD64 Mac, you're running Linux, or you just want to build newserv yourself, here's what you do:
1. Make sure you have CMake and libevent installed. (`brew install cmake libevent` on macOS, `sudo apt-get install cmake libevent-dev` on most Linuxes)
@@ -82,16 +116,18 @@ If you're using an older AMD64 Mac, you're running Linux, or you just want to bu
After building newserv or downloading a release, do this to set it up and use it:
1. In the system/ directory, make a copy of config.example.json named config.json, and edit it appropriately.
2. Run `./newserv` in the newserv directory. This will start the game server and run the interactive shell. You may need `sudo` if newserv's built-in DNS server is enabled.
3. Use the interactive shell to add a license. Run `help` in the shell to see how to do this.
4. If you plan to play PSO Blue Burst on newserv, set up the patch directory appropriately. See the "Client patch directories" section below.
2. If you plan to play PSO Blue Burst on newserv, set up the patch directory appropriately. See the "Client patch directories" section below.
3. Run `./newserv` in the newserv directory. This will start the game server and run the interactive shell. You may need `sudo` if newserv's built-in DNS server is enabled.
4. Use the interactive shell to add a license. Run `help` in the shell to see how to do this.
5. Set your client's network settings appropriately and start an online game. See the "Connecting local clients" or "Connecting remote clients" section to see how to get your game client to connect.
To use newserv in other ways (e.g. for translating data), see the end of this document.
### Installing quests
newserv automatically finds quests in the system/quests/ directory. To install your own quests, or to use quests you've saved using the proxy's set-save-files option, just put them in that directory and name them appropriately.
Standard quest 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`, and Episode 3 quests should be named like `e###-gc3.EXT`. The fields in each filename are:
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`, and Episode 3 download quests should be named like `e###-gc3.EXT`. The fields in each filename are:
- `###`: quest number (this doesn't really matter; it should just be unique for the PSO version)
- `CATEGORY`: ret = Retrieval, ext = Extermination, evt = Events, shp = Shops, vr = VR, twr = Tower, gov = Government (BB only), dl = Download (these don't appear during online play), 1p = Solo (BB only)
- `VERSION`: d1 = Dreamcast v1, dc = Dreamcast v2, pc = PC, gc = GameCube Episodes 1 & 2, gc3 = Episode 3, bb = Blue Burst
@@ -101,26 +137,28 @@ For example, the GameCube version of Lost HEAT SWORD is in two files named `q058
There are multiple PSO quest formats out there; newserv supports most of them. It can also decode any known format to standard .bin/.dat format. Specifically:
| Format | Extension | Supported online? | Offline decode option |
|---------------------------|-----------------------|-------------------|---------------------------|
| Compressed | .bin and .dat | Yes | None (1) |
| Compressed Ep3 | .bin or .mnm | Download only | None (1) |
| Uncompressed | .bind and .datd | Yes | --compress-data (2) |
| Uncompressed Ep3 | .bind or .mnm | Download only | --compress-data (2) |
| Unencrypted GCI | .bin.gci and .dat.gci | Yes | --decode-gci=FILENAME |
| Encrypted GCI with key | .bin.gci and .dat.gci | Yes | --decode-gci=FILENAME |
| Encrypted GCI without key | .bin.gci and .dat.gci | No | --decode-gci=FILENAME (3) |
| Ep3 GCI | .bin.gci or .mnm.gci | Download only | --decode-gci=FILENAME |
| Encrypted DLQ | .bin.dlq and .dat.dlq | Yes | --decode-dlq=FILENAME |
| Ep3 DLQ | .bin.dlq or .mnm.dlq | Download only | --decode-dlq=FILENAME |
| QST | .qst | Yes | --decode-qst=FILENAME |
| Format | Extension | Supported | Decode action |
|---------------------------|-----------------------|---------------|------------------|
| Compressed | .bin and .dat | Yes | None (1) |
| Compressed Ep3 | .bin or .mnm | Yes (4) | None (1) |
| Uncompressed | .bind and .datd | Yes | compress-prs (2) |
| Uncompressed Ep3 | .bind or .mnmd | Yes (4) | compress-prs (2) |
| Unencrypted GCI | .bin.gci and .dat.gci | Yes | decode-gci |
| Encrypted GCI with key | .bin.gci and .dat.gci | Yes | decode-gci |
| Encrypted GCI without key | .bin.gci and .dat.gci | No | decode-gci (3) |
| Ep3 GCI | .bin.gci or .mnm.gci | Download only | decode-gci |
| Encrypted DLQ | .bin.dlq and .dat.dlq | Yes | decode-dlq |
| Ep3 DLQ | .bin.dlq or .mnm.dlq | Download only | decode-dlq |
| Online QST | .qst | Yes | decode-qst |
| Download QST | .qst | Yes | decode-qst |
*Notes:*
1. *This is the default format. You can convert these to uncompressed format like this: `newserv --decompress-data < FILENAME.bin > FILENAME.bind`*
2. *Similar to (1), to compress an uncompressed quest file: `newserv --compress-data < FILENAME.bind > FILENAME.bin`*
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. *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 directory. 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.*
Episode 3 quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 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. There are no encrypted Episode 3 GCI formats because the game doesn't encrypt quests saved to the memory card, unlike Episodes 1&2.
When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats.
@@ -183,15 +221,19 @@ Some commands only work on the game server and not on the proxy server. The chat
* `$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.
* Debugging commands (game server only)
* `$dbgid`: Enable or disable high ID preference. When enabled, you'll be placed into the latest available slot in lobbies and games instead of the earliest. Can be useful for finding commands for which newserv doesn't handle client IDs properly.
* `$gc`: Send your own Guild Card to yourself.
* `$persist`: 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.
* Debugging commands
* `$dbgid` (game server only): Enable or disable high ID preference. When enabled, you'll be placed into the latest available slot in lobbies and games instead of the earliest. Can be useful for finding commands for which newserv doesn't handle client IDs properly.
* `$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.
* `$ss <data>` (proxy server only): Send a command to the remote server.
* Personal state commands
* `$arrow <color-id>`: Changes your lobby arrow color.
* `$secid <section-id>`: Sets your override section ID. After running this command, any games you create will use your override section ID for rare drops instead of your character's actual section ID. To revert to your actual section id, run `$secid` with no name after it.
* `$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.
* `$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 an Episode 3 game or spectator team, sends you to the lobby (but does not end your proxy session if you're in one).
* `$patch <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
* Blue Burst player commands (game server only)
* `$bbchar <username> <password> <1-4>`: Use this command when playing on a non-BB version of PSO. If the username and password are correct, this command converts your current character to BB format and saves it on the server in the given slot. Any character already in that slot is overwritten.
@@ -201,6 +243,9 @@ 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.
* `$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.
* Cheat mode commands
* `$cheat`: Enables or disables cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. This command does nothing on the proxy server - cheat commands are always available there.
@@ -208,12 +253,12 @@ Some commands only work on the game server and not on the proxy server. The chat
* `$warp <area-id>`: Warps yourself to the given area.
* `$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 <data>`: Sets the next item to be dropped from an enemy or box. 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 be the game leader and not using Blue Burst for this command to work. On the game server, this command works for all versions, and you do not have to be the game leader.
* `$item <data>` (or `$i <data>`): Create an item. 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.
* 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.
* `$allevent <event>` (game server only): Sets the current holiday event in all lobbies.
* `$song <song-id>` (game server only, Episode 3 only): Plays a specific song in the current lobby.
* `$song <song-id>` (Episode 3 only): Plays a specific song in the current lobby.
* Administration commands (game server only)
* `$ann <message>`: Sends an announcement message. The message text is sent to all players in all games and lobbies.
@@ -272,3 +317,18 @@ If you're using a version of Dolphin with tapserver support (currently only the
If you want to accept connections from outside your local network, you'll need to set ExternalAddress to your public IP address in the configuration file, and you'll likely need to open some ports in your router's NAT configuration - specifically, all the TCP ports listed in PortConfiguration in config.json.
For GC clients, you'll have to use newserv's built-in DNS server or set up your own DNS server as well. If you want external clients to be able to use your DNS server, you'll have to forward UDP port 53 to your newserv instance. Remote players can then connect to your server by entering your DNS server's IP address in their client's network configuration.
### Non-server usage
newserv has many CLI options, which can be used to access functionality other than the game/proxy server. Run `newserv help` to see these options and how to use them. The non-server things newserv can do are:
* Compress or decompress data in the PRS and BC0 formats
* Compute the decompressed size of compressed PRS data without decompressing it
* Encrypt or decrypt data using any PSO version's network encryption scheme
* Encrypt or decrypt data using Episode 3's trivial scheme
* Run a brute-force search for a decryption seed
* Decode Shift-JIS text to UTF-16
* Convert quests in .gci, .dlq, or .qst format to .bin/.dat format
* Extract the contents of a .gsl archive
* Connect to another PSO server and pretend to be a client
* Format Episode 3 game data in a human-readable manner
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 669 B

After

Width:  |  Height:  |  Size: 364 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

After

Width:  |  Height:  |  Size: 364 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 918 B

After

Width:  |  Height:  |  Size: 442 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 560 B

+3 -1
View File
@@ -105,6 +105,8 @@ void CatSession::on_channel_input(
}
}
// TODO: Use the iovec form of print_data here instead of
// prepend_command_header (which copies the string)
string full_cmd = prepend_command_header(
this->channel.version, this->channel.crypt_in.get(), command, flag, data);
print_data(stdout, full_cmd, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::OFFSET_16_BITS);
@@ -134,6 +136,6 @@ void CatSession::on_channel_error(short events) {
void CatSession::print_prompt() { }
void CatSession::execute_command(const std::string& command) {
string full_cmd = parse_data_string(command);
string full_cmd = parse_data_string(command, nullptr, ParseDataFlags::ALLOW_FILES);
send_command_with_header(this->channel, full_cmd.data(), full_cmd.size());
}
+246 -97
View File
@@ -5,17 +5,19 @@
#include <vector>
#include <string>
#include <unordered_map>
#include <phosg/Random.hh>
#include <phosg/Strings.hh>
#include <phosg/Time.hh>
#include "Loggers.hh"
#include "Server.hh"
#include "ProxyServer.hh"
#include "Lobby.hh"
#include "Client.hh"
#include "Lobby.hh"
#include "Loggers.hh"
#include "ProxyServer.hh"
#include "ReceiveCommands.hh"
#include "SendCommands.hh"
#include "Text.hh"
#include "Server.hh"
#include "StaticGameData.hh"
#include "Text.hh"
using namespace std;
@@ -95,52 +97,86 @@ static void check_is_leader(shared_ptr<Lobby> l, shared_ptr<Client> c) {
static void server_command_lobby_info(shared_ptr<ServerState>, shared_ptr<Lobby> l,
shared_ptr<Client> c, const std::u16string&) {
// no preconditions - everyone can use this command
vector<string> lines;
if (!l) {
send_text_message(c, u"$C6No lobby information");
} else if (l->is_game()) {
string level_string;
if (l->max_level == 0xFFFFFFFF) {
level_string = string_printf("Levels: %d+", l->min_level + 1);
} else {
level_string = string_printf("Levels: %d-%d", l->min_level + 1, l->max_level + 1);
}
send_text_message_printf(c,
"$C6Game ID: %08X\n%s\nSection ID: %s\nCheat mode: %s",
l->lobby_id, level_string.c_str(),
name_for_section_id(l->section_id).c_str(),
(l->flags & Lobby::Flag::CHEATS_ENABLED) ? "on" : "off");
lines.emplace_back("$C4No lobby info");
} else {
size_t num_clients = l->count_clients();
size_t max_clients = l->max_clients;
send_text_message_printf(c, "$C6Lobby ID: %08X\nPlayers: %zu/%zu",
l->lobby_id, num_clients, max_clients);
if (l->is_game()) {
lines.emplace_back(string_printf("Game ID: $C6%08X$C7", l->lobby_id));
if (!(l->flags & Lobby::Flag::EPISODE_3_ONLY)) {
if (l->max_level == 0xFFFFFFFF) {
lines.emplace_back(string_printf("Levels: $C6%d+$C7", l->min_level + 1));
} else {
lines.emplace_back(string_printf("Levels: $C6%d-%d$C7", l->min_level + 1, l->max_level + 1));
}
lines.emplace_back(string_printf("$C7Section ID: $C6%s$C7", name_for_section_id(l->section_id).c_str()));
lines.emplace_back(string_printf("$C7Cheat mode: $C6%s$C7", (l->flags & Lobby::Flag::CHEATS_ENABLED) ? "on" : "off"));
} else {
lines.emplace_back(string_printf("$C7State seed: $C6%08X$C7", l->random_seed));
}
} else {
lines.emplace_back(string_printf("$C7Lobby ID: $C6%08X$C7", l->lobby_id));
}
string slots_str = "Slots: ";
for (size_t z = 0; z < l->clients.size(); z++) {
if (!l->clients[z]) {
slots_str += string_printf("$C0%zX$C7", z);
} else {
bool is_self = l->clients[z] == c;
bool is_leader = z == l->leader_id;
if (is_self && is_leader) {
slots_str += string_printf("$C6%zX$C7", z);
} else if (is_self) {
slots_str += string_printf("$C2%zX$C7", z);
} else if (is_leader) {
slots_str += string_printf("$C4%zX$C7", z);
} else {
slots_str += string_printf("%zX", z);
}
}
}
lines.emplace_back(move(slots_str));
}
send_text_message(c, decode_sjis(join(lines, "\n")));
}
static void proxy_command_lobby_info(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string&) {
string msg;
if (session.license) {
msg = string_printf("$C7GC: $C6%" PRId64 "\n",
session.remote_guild_card_number);
string msg = string_printf("$C7GC: $C6%" PRId64 "$C7\nSlots: ",
session.remote_guild_card_number);
for (size_t z = 0; z < session.lobby_players.size(); z++) {
bool is_self = z == session.lobby_client_id;
bool is_leader = z == session.leader_client_id;
if (session.lobby_players[z].guild_card_number == 0) {
msg += string_printf("$C0%zX$C7", z);
} else if (is_self && is_leader) {
msg += string_printf("$C6%zX$C7", z);
} else if (is_self) {
msg += string_printf("$C2%zX$C7", z);
} else if (is_leader) {
msg += string_printf("$C4%zX$C7", z);
} else {
msg += string_printf("%zX", z);
}
}
msg += string_printf("$C7Client ID: $C6%zu%s",
session.lobby_client_id,
(session.leader_client_id == session.lobby_client_id) ? " (L)" : "");
vector<const char*> cheats_tokens;
if (session.switch_assist) {
if (session.options.switch_assist) {
cheats_tokens.emplace_back("SWA");
}
if (session.infinite_hp) {
if (session.options.infinite_hp) {
cheats_tokens.emplace_back("HP");
}
if (session.infinite_tp) {
if (session.options.infinite_tp) {
cheats_tokens.emplace_back("TP");
}
if (!cheats_tokens.empty()) {
@@ -149,13 +185,13 @@ static void proxy_command_lobby_info(shared_ptr<ServerState>,
}
vector<const char*> behaviors_tokens;
if (session.save_files) {
if (session.options.save_files) {
behaviors_tokens.emplace_back("SAVE");
}
if (session.suppress_remote_login) {
if (session.options.suppress_remote_login) {
behaviors_tokens.emplace_back("SL");
}
if (session.function_call_return_value >= 0) {
if (session.options.function_call_return_value >= 0) {
behaviors_tokens.emplace_back("BFC");
}
if (!behaviors_tokens.empty()) {
@@ -163,9 +199,9 @@ static void proxy_command_lobby_info(shared_ptr<ServerState>,
msg += join(behaviors_tokens, ",");
}
if (session.override_section_id >= 0) {
msg += "\n$C7SecID override: $C6";
msg += name_for_section_id(session.override_section_id);
if (session.options.override_section_id >= 0) {
msg += "\n$C7SecID*: $C6";
msg += name_for_section_id(session.options.override_section_id);
}
send_text_message(session.client_channel, decode_sjis(msg));
@@ -200,9 +236,9 @@ static void proxy_command_arrow(shared_ptr<ServerState>,
static void server_command_dbgid(shared_ptr<ServerState>, shared_ptr<Lobby>,
shared_ptr<Client> c, const std::u16string&) {
c->prefer_high_lobby_client_id = !c->prefer_high_lobby_client_id;
c->options.prefer_high_lobby_client_id = !c->options.prefer_high_lobby_client_id;
send_text_message_printf(c, "ID preference set\nto $C6%s",
c->prefer_high_lobby_client_id ? "high" : "low");
c->options.prefer_high_lobby_client_id ? "high" : "low");
}
static void server_command_auction(shared_ptr<ServerState>, shared_ptr<Lobby> l,
@@ -258,6 +294,41 @@ static void server_command_persist(shared_ptr<ServerState>, shared_ptr<Lobby> l,
}
}
static void server_command_exit(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const std::u16string&) {
if (l->is_game()) {
if (c->flags & Client::Flag::IS_EPISODE_3) {
c->channel.send(0xED, 0x00);
} else {
send_text_message(c, u"$C6You must return to\nthe lobby first");
}
} else {
send_self_leave_notification(c);
if (!(c->flags & Client::Flag::NO_D6)) {
send_message_box(c, u"");
}
const auto& port_name = version_to_login_port_name.at(
static_cast<size_t>(c->version()));
send_reconnect(c, s->connect_address_for_client(c),
s->name_to_port_config.at(port_name)->port);
}
}
static void proxy_command_exit(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string&) {
if (session.is_in_game) {
if (session.newserv_client_config.cfg.flags & Client::Flag::IS_EPISODE_3) {
session.client_channel.send(0xED, 0x00);
} else {
send_text_message(session.client_channel, u"$C6You must return to\nthe lobby first");
}
} else {
session.close_on_disconnect = true;
session.send_to_game_server();
}
}
static void server_command_get_self_card(shared_ptr<ServerState>, shared_ptr<Lobby>,
shared_ptr<Client> c, const std::u16string&) {
send_guild_card(c, c);
@@ -288,6 +359,24 @@ static void proxy_command_get_player_card(shared_ptr<ServerState>,
}
}
static void server_command_send_client(shared_ptr<ServerState>, shared_ptr<Lobby>,
shared_ptr<Client> c, const std::u16string& args) {
string data = parse_data_string(encode_sjis(args));
c->channel.send(data);
}
static void proxy_command_send_client(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string& args) {
string data = parse_data_string(encode_sjis(args));
session.client_channel.send(data);
}
static void proxy_command_send_server(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string& args) {
string data = parse_data_string(encode_sjis(args));
session.server_channel.send(data);
}
////////////////////////////////////////////////////////////////////////////////
// Lobby commands
@@ -307,11 +396,10 @@ static void server_command_cheat(shared_ptr<ServerState>, shared_ptr<Lobby> l,
if (!c) {
continue;
}
c->infinite_hp = false;
c->infinite_tp = false;
c->switch_assist = false;
c->options.infinite_hp = false;
c->options.infinite_tp = false;
c->options.switch_assist = false;
}
l->next_drop_item = PlayerInventoryItem();
}
}
@@ -333,17 +421,17 @@ static void server_command_lobby_event(shared_ptr<ServerState>, shared_ptr<Lobby
static void proxy_command_lobby_event(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string& args) {
if (args.empty()) {
session.override_lobby_event = -1;
session.options.override_lobby_event = -1;
} else {
uint8_t new_event = event_for_name(args);
if (new_event == 0xFF) {
send_text_message(session.client_channel, u"$C6No such lobby event.");
} else {
session.override_lobby_event = new_event;
session.options.override_lobby_event = new_event;
if ((session.version == GameVersion::GC && !(session.newserv_client_config.cfg.flags & Client::Flag::IS_TRIAL_EDITION)) ||
(session.version == GameVersion::XB) ||
(session.version == GameVersion::BB)) {
session.client_channel.send(0xDA, session.override_lobby_event);
session.client_channel.send(0xDA, session.options.override_lobby_event);
}
}
}
@@ -409,7 +497,7 @@ static void server_command_saverec(shared_ptr<ServerState>, shared_ptr<Lobby> l,
l->prev_battle_record.reset();
}
static void server_command_playrec(shared_ptr<ServerState>, shared_ptr<Lobby> l,
static void server_command_playrec(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
shared_ptr<Client> c, const std::u16string& args) {
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
send_text_message(c, u"$C4This command can\nonly be used on\nEpisode 3");
@@ -420,19 +508,20 @@ static void server_command_playrec(shared_ptr<ServerState>, shared_ptr<Lobby> l,
return;
}
if (l->is_game() && (l->flags & Lobby::Flag::EPISODE_3_ONLY) && (l->flags & Lobby::Flag::IS_SPECTATOR_TEAM) && l->battle_player) {
l->flags |= Lobby::Flag::BATTLE_IN_PROGRESS;
if (l->battle_player) {
l->battle_player->start();
} else if (args.empty()) {
c->next_game_battle_record.reset();
send_text_message(c, u"$C6Replay state\ncleared");
} else {
string filename = "system/ep3/battle-records/" + encode_sjis(args) + ".mzrd";
string data = load_file(filename);
c->next_game_battle_record.reset(new Episode3::BattleRecord(data));
send_text_message(c, u"$C6Replay state set");
uint32_t flags = Lobby::Flag::NON_V1_ONLY | Lobby::Flag::EPISODE_3_ONLY | Lobby::Flag::IS_SPECTATOR_TEAM;
string filename = encode_sjis(args);
if (filename[0] == '!') {
flags |= Lobby::Flag::START_BATTLE_PLAYER_IMMEDIATELY;
filename = filename.substr(1);
}
string data = load_file("system/ep3/battle-records/" + filename + ".mzrd");
shared_ptr<Episode3::BattleRecord> record(new Episode3::BattleRecord(data));
shared_ptr<Episode3::BattleRecordPlayer> battle_player(
new Episode3::BattleRecordPlayer(record, s->game_server->get_base()));
create_game_generic(s, c, args.c_str(), u"", 0xFF, 0, flags, nullptr, battle_player);
}
}
@@ -444,14 +533,14 @@ static void server_command_secid(shared_ptr<ServerState>, shared_ptr<Lobby> l,
check_is_game(l, false);
if (!args[0]) {
c->override_section_id = -1;
c->options.override_section_id = -1;
send_text_message(c, u"$C6Override section ID\nremoved");
} else {
uint8_t new_secid = section_id_for_name(args);
if (new_secid == 0xFF) {
send_text_message(c, u"$C6Invalid section ID");
} else {
c->override_section_id = new_secid;
c->options.override_section_id = new_secid;
send_text_message(c, u"$C6Override section ID\nset");
}
}
@@ -460,14 +549,14 @@ static void server_command_secid(shared_ptr<ServerState>, shared_ptr<Lobby> l,
static void proxy_command_secid(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string& args) {
if (!args[0]) {
session.override_section_id = -1;
session.options.override_section_id = -1;
send_text_message(session.client_channel, u"$C6Override section ID\nremoved");
} else {
uint8_t new_secid = section_id_for_name(args);
if (new_secid == 0xFF) {
send_text_message(session.client_channel, u"$C6Invalid section ID");
} else {
session.override_section_id = new_secid;
session.options.override_section_id = new_secid;
send_text_message(session.client_channel, u"$C6Override section ID\nset");
}
}
@@ -478,10 +567,10 @@ static void server_command_rand(shared_ptr<ServerState>, shared_ptr<Lobby> l,
check_is_game(l, false);
if (!args[0]) {
c->override_random_seed = -1;
c->options.override_random_seed = -1;
send_text_message(c, u"$C6Override seed\nremoved");
} else {
c->override_random_seed = stoul(encode_sjis(args), 0, 16);
c->options.override_random_seed = stoul(encode_sjis(args), 0, 16);
send_text_message(c, u"$C6Override seed\nset");
}
}
@@ -489,10 +578,10 @@ static void server_command_rand(shared_ptr<ServerState>, shared_ptr<Lobby> l,
static void proxy_command_rand(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string& args) {
if (!args[0]) {
session.override_random_seed = -1;
session.options.override_random_seed = -1;
send_text_message(session.client_channel, u"$C6Override seed\nremoved");
} else {
session.override_random_seed = stoul(encode_sjis(args), 0, 16);
session.options.override_random_seed = stoul(encode_sjis(args), 0, 16);
send_text_message(session.client_channel, u"$C6Override seed\nset");
}
}
@@ -514,6 +603,29 @@ static void server_command_password(shared_ptr<ServerState>, shared_ptr<Lobby> l
}
}
static void server_command_spec(shared_ptr<ServerState>, shared_ptr<Lobby> l,
shared_ptr<Client> c, const std::u16string&) {
check_is_game(l, true);
check_is_leader(l, c);
check_is_ep3(c, true);
if (!(l->flags & Lobby::Flag::EPISODE_3_ONLY)) {
throw logic_error("Episode 3 client in non-Episode 3 game");
}
if (l->flags & Lobby::Flag::SPECTATORS_FORBIDDEN) {
l->flags &= ~Lobby::Flag::SPECTATORS_FORBIDDEN;
send_text_message(l, u"$C6Spectators allowed");
} else {
l->flags |= Lobby::Flag::SPECTATORS_FORBIDDEN;
for (auto watcher_l : l->watcher_lobbies) {
send_command(watcher_l, 0xED, 0x00);
}
l->watcher_lobbies.clear();
send_text_message(l, u"$C6Spectators forbidden");
}
}
static void server_command_min_level(shared_ptr<ServerState>, shared_ptr<Lobby> l,
shared_ptr<Client> c, const std::u16string& args) {
check_is_game(l, true);
@@ -868,7 +980,7 @@ static void server_command_what(shared_ptr<ServerState>, shared_ptr<Lobby> l,
return;
}
if (!(l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) {
send_text_message(c, u"$C4Item tracking is off");
send_text_message(c, u"$C4Item tracking is\nnot available");
} else {
float min_dist2 = 0.0f;
uint32_t nearest_item_id = 0xFFFFFFFF;
@@ -900,7 +1012,17 @@ static void server_command_song(shared_ptr<ServerState>, shared_ptr<Lobby>,
check_is_ep3(c, true);
uint32_t song = stoul(encode_sjis(args), nullptr, 0);
send_ep3_change_music(c, song);
send_ep3_change_music(c->channel, song);
}
static void proxy_command_song(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string& args) {
int32_t song = stol(encode_sjis(args), nullptr, 0);
if (song < 0) {
song = -song;
send_ep3_change_music(session.server_channel, song);
}
send_ep3_change_music(session.client_channel, song);
}
static void server_command_infinite_hp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
@@ -908,15 +1030,16 @@ static void server_command_infinite_hp(shared_ptr<ServerState>, shared_ptr<Lobby
check_is_game(l, true);
check_cheats_enabled(l);
c->infinite_hp = !c->infinite_hp;
send_text_message_printf(c, "$C6Infinite HP %s", c->infinite_hp ? "enabled" : "disabled");
c->options.infinite_hp = !c->options.infinite_hp;
send_text_message_printf(c, "$C6Infinite HP %s",
c->options.infinite_hp ? "enabled" : "disabled");
}
static void proxy_command_infinite_hp(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string&) {
session.infinite_hp = !session.infinite_hp;
session.options.infinite_hp = !session.options.infinite_hp;
send_text_message_printf(session.client_channel, "$C6Infinite HP %s",
session.infinite_hp ? "enabled" : "disabled");
session.options.infinite_hp ? "enabled" : "disabled");
}
static void server_command_infinite_tp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
@@ -924,15 +1047,16 @@ static void server_command_infinite_tp(shared_ptr<ServerState>, shared_ptr<Lobby
check_is_game(l, true);
check_cheats_enabled(l);
c->infinite_tp = !c->infinite_tp;
send_text_message_printf(c, "$C6Infinite TP %s", c->infinite_tp ? "enabled" : "disabled");
c->options.infinite_tp = !c->options.infinite_tp;
send_text_message_printf(c, "$C6Infinite TP %s",
c->options.infinite_tp ? "enabled" : "disabled");
}
static void proxy_command_infinite_tp(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string&) {
session.infinite_tp = !session.infinite_tp;
session.options.infinite_tp = !session.options.infinite_tp;
send_text_message_printf(session.client_channel, "$C6Infinite TP %s",
session.infinite_tp ? "enabled" : "disabled");
session.options.infinite_tp ? "enabled" : "disabled");
}
static void server_command_switch_assist(shared_ptr<ServerState>, shared_ptr<Lobby> l,
@@ -940,14 +1064,16 @@ static void server_command_switch_assist(shared_ptr<ServerState>, shared_ptr<Lob
check_is_game(l, true);
check_cheats_enabled(l);
c->switch_assist = !c->switch_assist;
send_text_message_printf(c, "$C6Switch assist %s", c->switch_assist ? "enabled" : "disabled");
c->options.switch_assist = !c->options.switch_assist;
send_text_message_printf(c, "$C6Switch assist %s",
c->options.switch_assist ? "enabled" : "disabled");
}
static void proxy_command_switch_assist(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, const std::u16string&) {
session.switch_assist = !session.switch_assist;
send_text_message_printf(session.client_channel, "$C6Switch assist %s", session.switch_assist ? "enabled" : "disabled");
session.options.switch_assist = !session.options.switch_assist;
send_text_message_printf(session.client_channel, "$C6Switch assist %s",
session.options.switch_assist ? "enabled" : "disabled");
}
static void server_command_item(shared_ptr<ServerState>, shared_ptr<Lobby> l,
@@ -965,16 +1091,20 @@ static void server_command_item(shared_ptr<ServerState>, shared_ptr<Lobby> l,
return;
}
l->next_drop_item.clear();
PlayerInventoryItem item;
item.data.id = l->generate_item_id(c->lobby_client_id);
if (data.size() <= 12) {
memcpy(&l->next_drop_item.data.data1, data.data(), data.size());
memcpy(&item.data.data1, data.data(), data.size());
} else {
memcpy(&l->next_drop_item.data.data1, data.data(), 12);
memcpy(&l->next_drop_item.data.data2, data.data() + 12, data.size() - 12);
memcpy(&item.data.data1, data.data(), 12);
memcpy(&item.data.data2, data.data() + 12, data.size() - 12);
}
string name = name_for_item(l->next_drop_item.data, true);
send_text_message(c, u"$C7Next drop:\n" + decode_sjis(name));
l->add_item(item, c->area, c->x, c->z);
send_drop_stacked_item(l, item.data, c->area, c->x, c->z);
string name = name_for_item(item.data, true);
send_text_message(c, u"$C7Item created:\n" + decode_sjis(name));
}
static void proxy_command_item(shared_ptr<ServerState>,
@@ -995,6 +1125,8 @@ static void proxy_command_item(shared_ptr<ServerState>,
return;
}
bool set_drop = (!args.empty() && (args[0] == u'!'));
string data = parse_data_string(encode_sjis(args));
if (data.size() < 2) {
send_text_message(session.client_channel, u"$C6Item codes must be\n2 bytes or more");
@@ -1005,16 +1137,28 @@ static void proxy_command_item(shared_ptr<ServerState>,
return;
}
session.next_drop_item.clear();
PlayerInventoryItem item;
item.data.id = random_object<uint32_t>();
if (data.size() <= 12) {
memcpy(&session.next_drop_item.data.data1, data.data(), data.size());
memcpy(&item.data.data1, data.data(), data.size());
} else {
memcpy(&session.next_drop_item.data.data1, data.data(), 12);
memcpy(&session.next_drop_item.data.data2, data.data() + 12, data.size() - 12);
memcpy(&item.data.data1, data.data(), 12);
memcpy(&item.data.data2, data.data() + 12, data.size() - 12);
}
string name = name_for_item(session.next_drop_item.data, true);
send_text_message(session.client_channel, u"$C7Next drop:\n" + decode_sjis(name));
if (set_drop) {
session.next_drop_item = item;
string name = name_for_item(session.next_drop_item.data, 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);
string name = name_for_item(item.data, true);
send_text_message(session.client_channel, u"$C7Item created:\n" + decode_sjis(name));
}
}
@@ -1045,10 +1189,12 @@ static const unordered_map<u16string, ChatCommandDefinition> chat_commands({
{u"$dbgid", {server_command_dbgid, nullptr, u"Usage:\ndbgid"}},
{u"$edit", {server_command_edit, nullptr , u"Usage:\nedit <stat> <value>"}},
{u"$event", {server_command_lobby_event, proxy_command_lobby_event, u"Usage:\nevent <name>"}},
{u"$exit", {server_command_exit, proxy_command_exit, u"Usage:\nexit"}},
{u"$gc", {server_command_get_self_card, proxy_command_get_player_card, u"Usage:\ngc"}},
{u"$infhp", {server_command_infinite_hp, proxy_command_infinite_hp, u"Usage:\ninfhp"}},
{u"$inftp", {server_command_infinite_tp, proxy_command_infinite_tp, u"Usage:\ninftp"}},
{u"$item", {server_command_item, proxy_command_item, u"Usage:\nitem <item-code>"}},
{u"$i", {server_command_item, proxy_command_item, u"Usage:\ni <item-code>"}},
{u"$kick", {server_command_kick, nullptr, u"Usage:\nkick <name-or-number>"}},
{u"$li", {server_command_lobby_info, proxy_command_lobby_info, u"Usage:\nli"}},
{u"$maxlevel", {server_command_max_level, nullptr, u"Usage:\nmax_level <level>"}},
@@ -1060,10 +1206,13 @@ static const unordered_map<u16string, ChatCommandDefinition> chat_commands({
{u"$playrec", {server_command_playrec, nullptr, u"Usage:\nplayrec <filename>"}},
{u"$rand", {server_command_rand, proxy_command_rand, u"Usage:\nrand [hex seed]\nomit seed to revert\nto default"}},
{u"$saverec", {server_command_saverec, nullptr, u"Usage:\nsaverec <filename>"}},
{u"$sc", {server_command_send_client, proxy_command_send_client, u"Usage:\nsc <data>"}},
{u"$secid", {server_command_secid, proxy_command_secid, u"Usage:\nsecid [section ID]\nomit section ID to\nrevert to normal"}},
{u"$silence", {server_command_silence, nullptr, u"Usage:\nsilence <name-or-number>"}},
// TODO: implement this on proxy server
{u"$song", {server_command_song, nullptr, u"Usage:\nsong <song-number>"}},
{u"$song", {server_command_song, proxy_command_song, u"Usage:\nsong <song-number>"}},
{u"$spec", {server_command_spec, nullptr, u"Usage:\nspec"}},
{u"$ss", {nullptr, proxy_command_send_server, u"Usage:\nss <data>"}},
{u"$swa", {server_command_switch_assist, proxy_command_switch_assist, u"Usage:\nswa"}},
{u"$type", {server_command_lobby_type, nullptr, u"Usage:\ntype <name>"}},
{u"$warp", {server_command_warp, proxy_command_warp, u"Usage:\nwarp <area-number>"}},
+31 -11
View File
@@ -25,6 +25,27 @@ static atomic<uint64_t> next_id(1);
ClientOptions::ClientOptions()
: switch_assist(false),
infinite_hp(false),
infinite_tp(false),
prefer_high_lobby_client_id(false),
override_section_id(-1),
override_lobby_event(-1),
override_lobby_number(-1),
override_random_seed(-1),
save_files(false),
enable_chat_commands(true),
enable_chat_filter(true),
suppress_remote_login(false),
zero_remote_guild_card(false),
ep3_infinite_meseta(false),
red_name(false),
blank_name(false),
function_call_return_value(-1) { }
Client::Client(
struct bufferevent* bev,
GameVersion version,
@@ -46,7 +67,6 @@ Client::Client(
lobby_id(0),
lobby_client_id(0),
lobby_arrow_color(0),
prefer_high_lobby_client_id(false),
preferred_lobby_id(-1),
save_game_data_event(
event_new(
@@ -55,19 +75,10 @@ Client::Client(
event_free),
card_battle_table_number(-1),
card_battle_table_seat_number(0),
card_battle_table_seat_state(0),
next_exp_value(0),
override_section_id(-1),
override_random_seed(-1),
infinite_hp(false),
infinite_tp(false),
switch_assist(false),
can_chat(true),
pending_bb_save_player_index(0),
proxy_block_events(false),
proxy_block_function_calls(false),
proxy_save_files(false),
proxy_suppress_remote_login(false),
proxy_zero_remote_guild_card(false),
dol_base_addr(0) {
this->last_switch_enabled_command.header.subcommand = 0;
memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr));
@@ -78,6 +89,15 @@ Client::Client(
}
}
Client::~Client() {
if (!this->disconnect_hooks.empty()) {
this->log.warning("Disconnect hooks pending at client destruction time:");
for (const auto& it : this->disconnect_hooks) {
this->log.warning(" %s", it.first.c_str());
}
}
}
void Client::set_license(shared_ptr<const License> l) {
this->license = l;
this->game_data.guild_card_number = this->license->serial_number;
+34 -15
View File
@@ -15,6 +15,7 @@
#include "PSOProtocol.hh"
#include "Text.hh"
#include "Episode3/BattleRecord.hh"
#include "Episode3/Tournament.hh"
@@ -23,6 +24,31 @@ extern FileContentsCache client_options_cache;
struct ClientOptions {
// Options used on both game and proxy server
bool switch_assist;
bool infinite_hp;
bool infinite_tp;
bool prefer_high_lobby_client_id;
int16_t override_section_id;
int16_t override_lobby_event;
int16_t override_lobby_number;
int64_t override_random_seed;
// Options used only on proxy server
bool save_files;
bool enable_chat_commands;
bool enable_chat_filter;
bool suppress_remote_login;
bool zero_remote_guild_card;
bool ep3_infinite_meseta;
bool red_name;
bool blank_name;
int64_t function_call_return_value; // -1 = don't block function calls
ClientOptions();
};
struct Client {
enum Flag {
// This flag has two meanings. If set on a client with GameVersion::DC, then
@@ -96,48 +122,41 @@ struct Client {
bool should_send_to_proxy_server;
uint32_t proxy_destination_address;
uint16_t proxy_destination_port;
std::unordered_map<std::string, std::function<void()>> disconnect_hooks;
// Patch server
std::vector<PatchFileChecksumRequest> patch_file_checksum_requests;
// Lobby/positioning
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
bool prefer_high_lobby_client_id;
int64_t preferred_lobby_id; // <0 = no preference
ClientGameData game_data;
std::unique_ptr<struct event, void(*)(struct event*)> save_game_data_event;
int16_t card_battle_table_number;
uint8_t card_battle_table_seat_number;
uint16_t card_battle_table_seat_number;
uint16_t card_battle_table_seat_state;
std::weak_ptr<Episode3::Tournament::Team> ep3_tournament_team;
// Miscellaneous (used by chat commands)
uint32_t next_exp_value; // next EXP value to give
int16_t override_section_id; // valid if >= 0
int64_t override_random_seed; // valid if >= 0
bool infinite_hp; // cheats enabled
bool infinite_tp; // cheats enabled
bool switch_assist; // cheats enabled
G_SwitchStateChanged_6x05 last_switch_enabled_command;
bool can_chat;
std::string pending_bb_save_username;
uint8_t pending_bb_save_player_index;
std::shared_ptr<const Episode3::BattleRecord> next_game_battle_record;
bool proxy_block_events;
bool proxy_block_function_calls;
bool proxy_save_files;
bool proxy_suppress_remote_login;
bool proxy_zero_remote_guild_card;
// DOL file loading state
// File loading state
uint32_t dol_base_addr;
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();
inline GameVersion version() const {
return this->channel.version;
+821 -446
View File
File diff suppressed because it is too large Load Diff
+205
View File
@@ -0,0 +1,205 @@
#pragma once
#include <phosg/Encoding.hh>
#include "Text.hh"
// This file describes the structure of PSO GC ItemPT.gsl entries.
// TODO: This is not (yet) used anywhere in newserv, but we can use it as a base
// for PSOBB common item generation. Implement this.
// The ItemPT structure below describes the format of a single ItemPT entry
// (which corresponds to a single difficulty, episode, section ID, and challenge
// flag). ItemPT entries (within ItemPT.gsl) are named like this:
// string_printf("ItemPT_%s%s%c%1d.rel",
// (is_challenge ? "c" : ""),
// (is_ep2 ? "l" : ""),
// char_for_difficulty(difficulty), // One of "nhvu"
// section_id);
template <typename IntT>
struct Range {
IntT low;
IntT high;
} __attribute__((packed));
// For GC, use be_uint16_t/be_uint32_t; for other platforms use the le variants
template <typename U16T, U32T>
struct ItemPT {
// This data structure uses index probability tables in multiple places. An
// index probability table is a table where each entry holds the probability
// that that entry's index is used. For example, if the armor slot count
// probability table contains [77, 17, 5, 1, 0], this means there is a 77%
// chance of no slots, 17% chance of 1 slot, 5% chance of 2 slots, 1% chance
// of 3 slots, and no chance of 4 slots. The values in index probability
// tables do not have to add up to 100; the game sums all of them and chooses
// a random number less than that maximum.
// The area (floor) number is used in many places as well. Unlike the normal
// area numbers, which start with Pioneer 2, the area numbers in this
// structure start with Forest 1, and boss areas are treated as the first area
// of the next section (so De Rol Le has Mines 1 drops, for example). Final
// boss areas are treated as the last non-boss area (so Dark Falz boxes are
// like Ruins 3 boxes). We refer to these adjusted area numbers as (area - 1).
// This index probability table determines the types of non-rare weapons. The
// indexes in this table correspond to the non-rare weapon types 01 through 0C
// (Saber through Wand).
/* 0000 */ parray<uint8_t, 0x0C> base_weapon_type_prob_table;
// This table specifies the base subtype for each weapon type. Negative values
// here mean that the weapon cannot be found in the first N areas (so -2, for
// example, means that the weapon never appears in Forest 1 or 2 at all).
// Nonnegative values here mean the subtype can be found in all areas, and
// specify the base subtype (usually in the range [0, 4]). The subtype of
// weapon that actually appears depends on this value and a value from the
// following table.
/* 000C */ parray<int8_t, 0x0C> subtype_base_table;
// 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, 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.
/* 0018 */ parray<uint8_t, 0x0C> subtype_area_length_table;
// This index probability table specifies how likely each possible grind value
// is. The table is indexed as [grind][subtype_area_index], where the subtype
// area index is how many areas the player is beyond the first area in which
// the subtype can first be found (clamped to [0, 3]). To continue the example
// above, in Cave 3, subtype_area_index would be 2, since Swords can first be
// found two areas earlier in Cave 1.
// For example, this table could look like this:
// [64 1E 19 14] // Chance of getting a grind +0
// [00 1E 17 0F] // Chance of getting a grind +1
// [00 14 14 0E] // Chance of getting a grind +2
// ...
// C1 C2 C3 M1 // (Episode 1 area values from the example, for reference)
/* 0024 */ parray<parray<uint8_t, 4>, 9> grind_prob_tables;
// This array specifies the chance that a rare weapon will have each possible
// bonus value. The table is indexed as [(bonus_value - 10 / 5)][spec], so the
// first row refers the probability of getting a -10% bonus, the next row is
// the chance of getting -5%, etc., all the way up to +100%. For non-rare
// items, spec is determined randomly based on the following field; for rare
// items, spec is always 5.
/* 0048 */ parray<parray<U16T, 6>, 0x17> bonus_value_prob_tables;
// This array specifies the value of spec to be used in the above lookup for
// non-rare items. This is NOT an index probability table; this is a direct
// lookup with indexes [bonus_index][area - 1]. A value of 0xFF in any byte of
// this array prevents any weapon from having a bonus in that slot.
// For example, the array might look like this:
// [00 00 00 01 01 01 01 02 02 02]
// [FF FF FF 00 00 00 01 01 01 01]
// [FF FF FF FF FF FF FF FF FF 00]
// F1 F2 C1 C2 C3 M1 M2 R1 R2 R3 // (Episode 1 areas, for reference)
// In this example, spec is 0, 1, or 2 in all cases where a weapon can have a
// bonus. In Forest 1 and 2 and Cave 1, weapons may have at most one bonus; in
// all other areas except Ruins 3, they can have at most two bonuses, and in
// Ruins 3, they can have up to three bonuses.
/* 015C */ parray<parray<uint8_t, 10>, 3> nonrare_bonus_prob_spec;
// This array specifies the chance that a weapon will have each bonus type.
// The table is indexed as [bonus_type][area - 1] for non-rare items; for rare
// items, a random value in the range [0, 9] is used instead of (area - 1).
// For example, the table might look like this:
// [46 46 3F 3E 3E 3D 3C 3C 3A 3A] // Chance of getting no bonus
// [14 14 0A 0A 09 02 02 04 05 05] // Chance of getting Native bonus
// [0A 0A 12 11 11 09 09 08 08 08] // Chance of getting A.Beast bonus
// [00 00 09 0A 0B 13 12 08 09 09] // Chance of getting Machine bonus
// [00 00 00 01 01 08 0A 13 13 13] // Chance of getting Dark bonus
// [00 00 00 00 00 01 01 01 01 01] // Chance of getting Hit bonus
// F1 F2 C1 C2 C3 M1 M2 R1 R2 R3 // (Episode 1 areas, for reference)
/* 017A */ parray<parray<uint8_t, 10>, 6> bonus_type_prob_tables;
// This array (indexed by area - 1) specifies a multiplier of used in special
// ability determination. It seems this uses the star values from ItemPMT, but
// not yet clear exactly in what way.
// TODO: Figure out exactly what this does. Anchor: 80106FEC
/* 01B6 */ parray<uint8_t, 0x0A> special_mult;
// This array (indexed by area - 1) specifies the probability that any
// non-rare weapon will have a special ability.
/* 01C0 */ parray<uint8_t, 0x0A> special_percent;
// TODO: Figure out exactly how this table is used. Anchor: 80106D34
/* 01CA */ parray<uint8_t, 0x05> armor_shield_type_index_prob_table;
// This index probability table specifies how common each possible slot count
// is for armor drops.
/* 01CF */ parray<uint8_t, 0x05> armor_slot_count_prob_table;
// These values specify maximum indexes into another array which is generated
// at runtime. The values here are multiplied by a random float in the range
// [0, n] to look up the value in the secondary array, which is what ends up
// determining the unit type.
// TODO: Figure out and document the exact logic here. Anchor: 80106364
/* 01D4 */ parray<uint8_t, 0x0A> unit_maxes;
// This index probability table is indexed by [tool_class][area - 1]. The tool
// class refers to an entry in ItemPMT, which links it to the actual item
// code.
/* 01DE */ parray<parray<U16T, 0x0A>, 0x1C> tool_class_prob_table;
// This index probability table determines how likely each technique is to
// appear. The table is indexed as [technique_num][area - 1].
/* 040E */ parray<parray<uint8_t, 0x0A> 0x13> technique_index_prob_table;
// This table specifies the ranges for technique disk levels. The table is
// indexed as [technique_num][area - 1]. If either min or max in the range is
// 0xFF, or if max < min, technique disks are not dropped for that technique
// and area pair.
/* 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.)
/* 0648 */ parray<uint8_t, 0x64> enemy_type_drop_probs;
// This array (indexed by enemy_id) specifies the range of meseta values that
// each enemy can drop.
/* 06AC */ parray<Range<U16T>, 0x64> enemy_meseta_ranges;
// Each byte in this table (indexed by enemy_type) represents the class of
// item that the enemy can drop. The values are:
// 00 = weapon
// 01 = armor
// 02 = shield
// 03 = unit
// 04 = tool
// 05 = meseta
// Anything else = no item
/* 083C */ parray<uint8_t, 0x64> enemy_item_classes;
// This table (indexed by area - 1) specifies the ranges of meseta values that
// can drop from boxes.
/* 08A0 */ parray<Range<U16T>, 0x0A> box_meseta_ranges;
// This index probability table determines which type of items drop from
// boxes. The table is indexed as [item_class][area - 1], with item_class as
// the result value (that is, in the example below, the game looks at a single
// column and sums the values going down, then the chosen item class is one of
// the row indexes based on the weight values in the column.) The resulting
// item_class value has the same meaning as in enemy_item_classes above.
// For example, this array might look like the following:
// [07 07 08 08 06 07 08 09 09 0A] // Chances per area of a weapon drop
// [02 02 02 02 03 02 02 02 03 03] // Chances per area of an armor drop
// [02 02 02 02 03 02 02 02 03 03] // Chances per area of a shield drop
// [00 00 03 03 03 04 03 04 05 05] // Chances per area of a unit drop
// [11 11 12 12 12 12 12 12 12 12] // Chances per area of a tool drop
// [32 32 32 32 32 32 32 32 32 32] // Chances per area of a meseta drop
// [16 16 11 11 11 11 11 0F 0C 0B] // Chances per area of an empty box
// F1 F2 C1 C2 C3 M1 M2 R1 R2 R3 // (Episode 1 areas, for reference)
/* 08C8 */ parray<parray<uint8_t, 10>, 7> box_item_class_prob_tables;
/* 0910 */ U32T offset_table[0x1C];
/* 0980 */ U16T unknown_f1[0x20];
/* 09C0 */ U32T unknown_f1_offset;
/* 09C4 */ U32T unknown_f2[3];
/* 09D0 */ U32T offset_table_offset;
/* 09D4 */ U32T unknown_f3[3];
/* 09E0 (end of structure) */
} __attribute__((packed));
+226 -121
View File
@@ -14,12 +14,14 @@ using namespace std;
PRSCompressor::PRSCompressor()
: closed(false),
PRSCompressor::PRSCompressor(function<void(size_t, size_t)> progress_fn)
: progress_fn(progress_fn),
closed(false),
control_byte_offset(0),
pending_control_bits(0),
input_bytes(0),
compression_offset(0) {
compression_offset(0),
reverse_log_index(0x100) {
this->output.put_u8(0);
}
@@ -50,20 +52,28 @@ void PRSCompressor::advance() {
// Search for a match in the decompressed data history
size_t best_match_size = 0;
size_t best_match_offset = 0;
size_t start_offset = max<ssize_t>(
0, static_cast<ssize_t>(this->compression_offset) - 0x1FFF);
for (size_t match_offset = start_offset;
(match_offset < this->compression_offset - best_match_size) && (best_match_size < 0x100);
match_offset++) {
uint8_t first_v = this->forward_log[this->compression_offset & 0xFF];
const auto& start_offsets = this->reverse_log_index[first_v];
for (auto it = start_offsets.begin(); (it != start_offsets.end()) && (best_match_size < 0x100); it++) {
size_t match_offset = *it;
if (match_offset == this->compression_offset - 0x2000) {
continue;
}
size_t match_size = 0;
size_t match_loop_bytes = this->compression_offset - match_offset;
while ((match_size < 0x100) &&
(match_offset + match_size < this->compression_offset) &&
(this->compression_offset + match_size < this->input_bytes) &&
(this->reverse_log[(match_offset + match_size) & 0x1FFF] == this->forward_log[(this->compression_offset + match_size) & 0xFF])) {
(this->reverse_log[(match_offset + (match_size % match_loop_bytes)) & 0x1FFF] == this->forward_log[(this->compression_offset + match_size) & 0xFF])) {
match_size++;
}
if (match_size > best_match_size) {
// If there are multiple matches of the longest length, use the latest one,
// since it's more likely that it can be expressed as a short copy instead
// of a long copy.
if (match_size >= best_match_size) {
best_match_offset = match_offset;
best_match_size = match_size;
}
@@ -72,6 +82,7 @@ void PRSCompressor::advance() {
// If there is a suitable match, write a backreference
bool should_write_literal = false;
size_t advance_bytes = 0;
ssize_t backreference_offset = best_match_offset - this->compression_offset;
if (best_match_size < 2) {
should_write_literal = true;
@@ -79,13 +90,12 @@ void PRSCompressor::advance() {
// The backreference should be encoded:
// - As a short copy if offset in [-0x100, -1] and size in [2, 5]
// - As a long copy if offset in [-0x1FFF, -1] and size in [3, 9]
// - As an extended copy if offset in [-0x1FFF, -1] and size in [1, 0x100]
// Because an extended copy costs two control bits and three data bytes,
// it's not worth it to use an extended copy for sizes 1 and 2. In those
// cases, if a short copy can't reach back far enough, we just write a
// literal instead.
// - As an extended copy if offset in [-0x1FFF, -1] and size in [10, 0x100]
// Technically an extended copy can be used for sizes 1-9 as well, but if
// size is 1 or 2, writing literals is better (since it uses fewer data
// bytes and control bits), and a long copy can cover sizes 3-9 (and also
// uses fewer data bytes and control bits).
ssize_t backreference_offset = best_match_offset - this->compression_offset;
if ((backreference_offset >= -0x100) && (best_match_size <= 5)) {
// Write short copy
uint8_t size = best_match_size - 2;
@@ -132,7 +142,18 @@ void PRSCompressor::advance() {
}
for (size_t z = 0; z < advance_bytes; z++) {
this->reverse_log[this->compression_offset & 0x1FFF] = this->forward_log[this->compression_offset & 0xFF];
if ((this->compression_offset & 0x1000) && this->progress_fn) {
this->progress_fn(this->compression_offset, this->output.size());
}
size_t reverse_log_offset = this->compression_offset & 0x1FFF;
uint8_t existing_v = this->reverse_log[reverse_log_offset];
uint8_t new_v = this->forward_log[this->compression_offset & 0xFF];
if (this->compression_offset & (~0x1FFF)) {
this->reverse_log_index[existing_v].pop_front();
}
this->reverse_log[reverse_log_offset] = new_v;
this->reverse_log_index[new_v].emplace_back(this->compression_offset);
this->compression_offset++;
}
}
@@ -185,14 +206,16 @@ void PRSCompressor::flush_control() {
string prs_compress(const void* vdata, size_t size) {
PRSCompressor prs;
string prs_compress(
const void* vdata, size_t size, function<void(size_t, size_t)> progress_fn) {
PRSCompressor prs(progress_fn);
prs.add(vdata, size);
return move(prs.close());
}
string prs_compress(const string& data) {
return prs_compress(data.data(), data.size());
string prs_compress(
const string& data, function<void(size_t, size_t)> progress_fn) {
return prs_compress(data.data(), data.size(), progress_fn);
}
@@ -325,6 +348,7 @@ size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size
while (!r.eof()) {
if (cr.read()) {
ret++;
r.get_u8();
} else {
ssize_t offset;
@@ -366,17 +390,195 @@ size_t prs_decompress_size(const string& data, size_t max_output_size) {
void prs_disassemble(FILE* stream, const void* data, size_t size) {
size_t output_bytes = 0;
StringReader r(data, size);
ControlStreamReader cr(r);
while (!r.eof()) {
size_t r_offset = r.where();
if (cr.read()) {
fprintf(stream, "[%zX => %zX] literal %02hhX\n", r_offset, output_bytes, r.get_u8());
output_bytes++;
} else {
ssize_t offset;
size_t count;
bool is_long_copy = cr.read();
if (is_long_copy) {
uint16_t a = r.get_u8();
a |= (r.get_u8() << 8);
offset = (a >> 3) | (~0x1FFF);
if (offset == ~0x1FFF) {
fprintf(stream, "[%zX => %zX] end\n", r_offset, output_bytes);
break;
}
count = (a & 7) ? ((a & 7) + 2) : (r.get_u8() + 1);
} else {
count = cr.read() << 1;
count = (count | cr.read()) + 2;
offset = r.get_u8() | (~0xFF);
}
size_t read_offset = output_bytes + offset;
fprintf(stream, "[%zX => %zX] %s copy -%zX (from %zX) %zX\n",
r_offset, output_bytes, is_long_copy ? "long" : "short",
-offset, read_offset, count);
if (read_offset >= output_bytes) {
throw runtime_error("backreference offset beyond beginning of output");
}
output_bytes += count;
}
}
}
void prs_disassemble(FILE* stream, const std::string& data) {
return prs_disassemble(stream, data.data(), data.size());
}
// BC0 is a compression algorithm fairly similar to PRS, but with a simpler set
// of commands. Like PRS, there is a control stream, indicating when to copy a
// literal byte from the input and when to copy from a backreference; unlike
// PRS, there is only one type of backreference. Also, there is no stop opcode;
// the decompressor simply stops when there are no more input bytes to read.
string bc0_compress(
const string& data, function<void(size_t, size_t)> progress_fn) {
StringReader r(data);
StringWriter w;
parray<uint8_t, 0x1000> memo;
uint16_t memo_offset = 0x0FEE;
vector<deque<size_t>> memo_index(0x100);
auto write_memo = [&](uint8_t new_v) -> void {
uint8_t existing_v = memo[memo_offset];
if (existing_v != new_v) {
if (!memo_index[existing_v].empty()) {
memo_index[existing_v].pop_front();
}
memo[memo_offset] = new_v;
memo_index[new_v].emplace_back(memo_offset);
}
memo_offset = (memo_offset + 1) & 0xFFF;
};
size_t next_control_byte_offset = w.size();
w.put_u8(0);
uint16_t pending_control_bits = 0x0000;
parray<uint8_t, 18> match_buf;
while (!r.eof()) {
if ((r.where() & 0x1000) && progress_fn) {
progress_fn(r.where(), w.size());
}
// Search in the memo for the longest string matching the upcoming data, of
// size 3-18 bytes
size_t best_match_offset = 0;
size_t best_match_size = 0;
size_t max_match_size = min<size_t>(r.remaining(), 18);
const uint8_t* match_buf = &r.get<uint8_t>(false, max_match_size);
for (size_t match_size = 3; match_size <= max_match_size; match_size++) {
for (size_t offset : memo_index[match_buf[0]]) {
// Forbid matches that span the memo boundary - during decompression,
// the client will be overwriting its memo while reading from it and
// will likely generate incorrect data
// TODO: We can actually support this (and it will improve compression),
// but we have to set a loop boundary like we have in prs_compress and
// I'm lazy.
size_t start_memo_offset = offset;
size_t end_memo_offset = (offset + match_size) & 0xFFF;
if (end_memo_offset < start_memo_offset) {
if ((memo_offset < end_memo_offset) || (memo_offset > start_memo_offset)) {
continue;
}
} else {
if ((memo_offset > start_memo_offset) && (memo_offset < end_memo_offset)) {
continue;
}
}
// Note: We don't have to explicitly forbid matches that span the
// uninitialized part of the memo (during the first 0x12 bytes) because
// the preceding check will catch those too (and there can't be any
// start offsets in the memo index within that region anyway).
bool match_found = true;
for (size_t z = 0; z < match_size; z++) {
if (match_buf[z] != memo[(offset + z) & 0xFFF]) {
match_found = false;
break;
}
}
// If a match was found at this size, don't bother looking for other
// matches of the same size
if (match_found) {
best_match_size = match_size;
best_match_offset = offset;
break;
}
}
// If no matches were found at the current size, don't bother looking for
// longer matches
if (best_match_size < match_size) {
break;
}
}
// Write a backreference if a match was found; otherwise, write a literal
if (best_match_size >= 3) {
pending_control_bits = (pending_control_bits >> 1) | 0x8000;
w.put_u8(best_match_offset & 0xFF); // a1
w.put_u8(((best_match_offset >> 4) & 0xF0) | (best_match_size - 3)); // a2
for (size_t z = 0; z < best_match_size; z++) {
write_memo(r.get_u8());
}
} else {
pending_control_bits = (pending_control_bits >> 1) | 0x8080;
uint8_t v = r.get_u8();
w.put_u8(v);
write_memo(v);
}
// Write the control byte to the output if needed, and reserve space for the
// next one
if (pending_control_bits & 0x0100) {
w.pput_u8(next_control_byte_offset, pending_control_bits & 0xFF);
next_control_byte_offset = w.size();
w.put_u8(0);
pending_control_bits = 0x0000;
}
}
// Write the final control byte to the output if needed. If not needed, then
// there should be an empty reserved space at the end; delete it since none of
// its bits will be used.
if (pending_control_bits & 0xFF00) {
while (!(pending_control_bits & 0x0100)) {
pending_control_bits >>= 1;
}
w.pput_u8(next_control_byte_offset, pending_control_bits & 0xFF);
} else {
if (next_control_byte_offset != w.size() - 1) {
throw logic_error("data written without control bits");
}
w.str().resize(w.str().size() - 1);
}
return move(w.str());
}
// The BC0 decompression implementation in PSO GC is vulnerable to overflow
// attacks - there is no bounds checking on the output buffer. It is unlikely
// that this can be usefully exploited (e.g. for RCE) because the output pointer
// is checked before every byte is written, so we cannot change the output
// pointer to any arbitrary address.
// is loaded from memory before every byte is written, so we cannot change the
// output pointer to any arbitrary address.
string bc0_decompress(const string& data) {
StringReader r(data);
@@ -447,100 +649,3 @@ string bc0_decompress(const string& data) {
return move(w.str());
}
string bc0_compress(const string& data) {
StringReader r(data);
StringWriter w;
parray<uint8_t, 0x1000> memo;
uint16_t memo_offset = 0x0FEE;
size_t next_control_byte_offset = w.size();
w.put_u8(0);
uint16_t pending_control_bits = 0x0000;
parray<uint8_t, 17> match_buf;
while (!r.eof()) {
// Search in the memo for the longest string matching the upcoming data, of
// size 3-17 bytes
size_t best_match_offset = 0;
size_t best_match_size = 0;
size_t max_match_size = min<size_t>(r.remaining(), 17);
r.readx(match_buf.data(), max_match_size, false);
for (size_t match_size = 3; match_size <= max_match_size; match_size++) {
// Forbid matches that span the current memo position, or that cover the
// uninitialized part of the memo when the client decompresses (see
// comment in bc0_decompress about this)
size_t start_offset = (r.where() < 0x12) ? 0 : memo_offset;
size_t end_offset = (memo_offset - match_size + 1) & 0xFFF;
for (size_t offset = start_offset; offset != end_offset; offset = (offset + 1) & 0xFFF) {
bool match_found = true;
for (size_t z = 0; z < match_size; z++) {
if (match_buf[z] != memo[(offset + z) & 0xFFF]) {
match_found = false;
break;
}
}
// If a match was found at this size, don't bother looking for other
// matches of the same size
if (match_found) {
best_match_size = match_size;
best_match_offset = offset;
break;
}
}
// If no matches were found at the current size, don't bother looking for
// longer matches
if (best_match_size < match_size) {
break;
}
}
// Write a backreference if a match was found; otherwise, write a literal
if (best_match_size >= 3) {
pending_control_bits = (pending_control_bits >> 1) | 0x8000;
w.put_u8(best_match_offset & 0xFF); // a1
w.put_u8(((best_match_offset >> 4) & 0xF0) | (best_match_size - 3)); // a2
for (size_t z = 0; z < best_match_size; z++) {
memo[memo_offset] = r.get_u8();
memo_offset = (memo_offset + 1) & 0xFFF;
}
} else {
pending_control_bits = (pending_control_bits >> 1) | 0x8080;
uint8_t v = r.get_u8();
w.put_u8(v);
memo[memo_offset] = v;
memo_offset = (memo_offset + 1) & 0xFFF;
}
// Write the control byte to the output if needed, and reserve space for the
// next one
if (pending_control_bits & 0x0100) {
w.pput_u8(next_control_byte_offset, pending_control_bits & 0xFF);
next_control_byte_offset = w.size();
w.put_u8(0);
pending_control_bits = 0x0000;
}
}
// Write the final control byte to the output if needed. If not needed, then
// there should be an empty reserved space at the end; delete it since none of
// its bits will be used.
if (pending_control_bits & 0xFF00) {
while (!(pending_control_bits & 0x0100)) {
pending_control_bits >>= 1;
}
w.pput_u8(next_control_byte_offset, pending_control_bits & 0xFF);
} else {
if (next_control_byte_offset != w.size() - 1) {
throw logic_error("data written without control bits");
}
w.str().resize(w.str().size() - 1);
}
return move(w.str());
}
+13 -5
View File
@@ -3,6 +3,8 @@
#include <stddef.h>
#include <string>
#include <functional>
#include <deque>
#include "Text.hh"
@@ -15,7 +17,7 @@ class PRSCompressor {
public:
// To use this class, instantiate it, then call .add() one or more times, then
// call .close() and use the returned string as the compressed result.
PRSCompressor();
PRSCompressor(std::function<void(size_t, size_t)> progress_fn = nullptr);
~PRSCompressor() = default;
// Adds more input data to be compressed, which logically comes after all
@@ -39,6 +41,7 @@ private:
void write_control(bool z);
void flush_control();
std::function<void(size_t, size_t)> progress_fn;
bool closed;
size_t control_byte_offset;
@@ -48,6 +51,7 @@ private:
parray<uint8_t, 0x100> forward_log;
size_t compression_offset;
parray<uint8_t, 0x2000> reverse_log;
std::vector<std::deque<size_t>> reverse_log_index;
StringWriter output;
};
@@ -55,8 +59,8 @@ private:
// Compresses data from a single input buffer using PRS and returns the
// compressed result. This is a shortcut for constructing a PRSCompressor,
// calling .add() once, and calling .close().
std::string prs_compress(const void* vdata, size_t size);
std::string prs_compress(const std::string& data);
std::string prs_compress(const void* vdata, size_t size, std::function<void(size_t, size_t)> progress_fn = nullptr);
std::string prs_compress(const std::string& data, std::function<void(size_t, size_t)> progress_fn = nullptr);
// Decompresses PRS-compressed data.
std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0);
@@ -67,6 +71,10 @@ std::string prs_decompress(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);
size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0);
// Decompresses and compresses data using the BC0 algorithm.
// Prints the command stream from a PRS-compressed buffer.
void prs_disassemble(FILE* stream, const void* data, size_t size);
void prs_disassemble(FILE* stream, const std::string& data);
// Compresses and decompresses data using the BC0 algorithm.
std::string bc0_compress(const std::string& data, std::function<void(size_t, size_t)> progress_fn = nullptr);
std::string bc0_decompress(const std::string& data);
std::string bc0_compress(const std::string& data);
+1 -1
View File
@@ -234,7 +234,7 @@ void AssistServer::populate_effects() {
const auto& hes = this->hand_and_equip_states[z];
if (hes) {
uint16_t card_id = hes->assist_card_id == 0xFFFF
? this->card_id_for_card_ref(hes->assist_card_id)
? this->card_id_for_card_ref(hes->assist_card_ref)
: hes->assist_card_id.load();
this->assist_effects[z] = assist_effect_number_for_card_id(card_id);
if (this->assist_effects[z] != AssistEffect::NONE) {
+11 -7
View File
@@ -283,22 +283,26 @@ void BattleRecord::set_battle_end_timestamp() {
BattleRecordPlayer::BattleRecordPlayer(
shared_ptr<const BattleRecord> rec,
shared_ptr<struct event_base> base,
shared_ptr<Lobby> l)
shared_ptr<struct event_base> base)
: record(rec),
event_it(this->record->events.begin()),
play_start_timestamp(0),
base(base),
lobby(l),
next_command_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &BattleRecordPlayer::dispatch_schedule_events, this), event_free) { }
shared_ptr<const BattleRecord> BattleRecordPlayer::get_record() const {
return this->record;
}
void BattleRecordPlayer::set_lobby(std::shared_ptr<Lobby> l) {
this->lobby = l;
}
void BattleRecordPlayer::start() {
this->play_start_timestamp = now();
this->schedule_events();
if (this->play_start_timestamp == 0) {
this->play_start_timestamp = now();
this->schedule_events();
}
}
void BattleRecordPlayer::dispatch_schedule_events(
@@ -348,10 +352,10 @@ void BattleRecordPlayer::schedule_events() {
// This should have been handled before the lobby was even created
break;
case BattleRecord::Event::Type::BATTLE_COMMAND:
send_command(l, 0xC9, 0x00, ev.data);
send_command(l, (ev.data.size() >= 0x400) ? 0x6C : 0xC9, 0x00, ev.data);
break;
case BattleRecord::Event::Type::GAME_COMMAND:
send_command(l, 0x60, 0x00, ev.data);
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);
+3 -2
View File
@@ -98,11 +98,12 @@ class BattleRecordPlayer {
public:
BattleRecordPlayer(
std::shared_ptr<const BattleRecord> rec,
std::shared_ptr<struct event_base> base,
std::shared_ptr<Lobby> l);
std::shared_ptr<struct event_base> base);
~BattleRecordPlayer() = default;
std::shared_ptr<const BattleRecord> get_record() const;
void set_lobby(std::shared_ptr<Lobby> l);
void start();
private:
+10 -3
View File
@@ -470,7 +470,7 @@ void Card::execute_attack(shared_ptr<Card> attacker_card) {
if (!(this->card_flags & 2) &&
(!attacker_card || !(attacker_card->card_flags & 2))) {
this->server()->card_special->unknown_80244E20(
this->server()->card_special->check_for_defense_interference(
attacker_card, this->shared_from_this(), &preliminary_damage);
}
@@ -1091,7 +1091,14 @@ void Card::unknown_8023813C() {
cond.remaining_turns = 1;
}
if (cond.remaining_turns < 99) {
cond.remaining_turns--;
// Note: There is at least one case in the original implementation where
// remaining_turns can go negative: Creinu's HP Assist. The condition is
// applied with remaining_turns=0 to all affected cards (so it should be
// immediately removed here). But since remaining_turns is unsigned in
// our implementation, we have to check for underflow here.
if (cond.remaining_turns > 0) {
cond.remaining_turns--;
}
if (cond.remaining_turns < 1) {
this->server()->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond(
cond, this->shared_from_this());
@@ -1208,7 +1215,7 @@ void Card::unknown_80237734() {
}
if (!(this->card_flags & 2)) {
this->compute_action_chain_results(1, 0);
this->server()->card_special->unknown_8024504C(this->shared_from_this());
this->server()->card_special->check_for_attack_interference(this->shared_from_this());
}
this->compute_action_chain_results(1, 0);
this->unknown_80236374(this->shared_from_this(), nullptr);
+36 -20
View File
@@ -257,6 +257,8 @@ bool CardSpecial::apply_defense_condition(
if ((when == 2) && (defender_cond->type == ConditionType::GUOM) && (flags & 4)) {
CardShortStatus stat = defender_card->get_short_status();
if (stat.card_flags & 4) {
this->server()->base()->log.debug("(when=2) @%04hX clearing GUOM from @%04hX",
attacker_card_ref, defender_card->get_card_ref());
G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd;
cmd.effect.flags = 0x04;
cmd.effect.attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid(attacker_card_ref, 0x0E);
@@ -276,6 +278,8 @@ bool CardSpecial::apply_defense_condition(
(defender_cond->type == ConditionType::ACID)) {
int16_t hp = defender_card->get_current_hp();
if (hp > 0) {
this->server()->base()->log.debug("(when=2) @%04hX has ACID; removing 1 HP",
defender_cond->card_ref.load());
this->send_6xB4x06_for_stat_delta(
defender_card, defender_cond->card_ref, 0x20, -1, 0, 1);
defender_card->set_current_hp(hp - 1);
@@ -3366,10 +3370,15 @@ void CardSpecial::unknown_80244AA8(shared_ptr<Card> card) {
this->unknown_8024C2B0(0x13, card->get_card_ref(), as, 0xFFFF);
}
void CardSpecial::unknown_80244E20(
void CardSpecial::check_for_defense_interference(
shared_ptr<const Card> attacker_card,
shared_ptr<Card> target_card,
int16_t* inout_unknown_p4) {
// Note: This check is not part of the original implementation.
if (this->server()->base()->data_index->behavior_flags & BehaviorFlag::DISABLE_INTERFERENCE) {
return;
}
if (!inout_unknown_p4) {
return;
}
@@ -3420,12 +3429,9 @@ void CardSpecial::unknown_80244E20(
if (target_ps->unknown_a17 >= 1) {
return;
}
auto entry = unknown_8024DAFC(target_card_id, ally_sc_card_id, false);
if (!entry) {
return;
}
uint8_t rand_v = this->server()->get_random(99);
if (rand_v >= entry->unknown_v2) {
auto entry = get_interference_probability_entry(
target_card_id, ally_sc_card_id, false);
if (!entry || (this->server()->get_random(99) >= entry->defense_probability)) {
return;
}
@@ -3500,6 +3506,13 @@ void CardSpecial::unknown_8024C2B0(
continue;
}
{
string as_s = as.str();
string eff_s = card_effect.str();
this->server()->base()->log.debug("(when=%" PRIu32 ") set=@%04hX sc=@%04hX as=%s att=@%04hX eff=%s",
when, set_card_ref, sc_card_ref, as_s.c_str(), as_attacker_card_ref, eff_s.c_str());
}
int16_t arg3_value = atoi(&card_effect.arg3[1]);
auto targeted_cards = this->get_targeted_cards_for_condition(
set_card_ref, def_effect_index, sc_card_ref, as, arg3_value, 1);
@@ -3568,6 +3581,7 @@ void CardSpecial::unknown_8024C2B0(
card_effect, target_card, dice_cmd.effect.target_card_ref, sc_card_ref)) {
applied_cond_index = target_card->apply_abnormal_condition(
card_effect, def_effect_index, dice_cmd.effect.target_card_ref, sc_card_ref, value, dice_roll.value, random_percent);
// This debug_print call is in the original code.
// this->debug_print(when, 4, &env_stats, "!set_abnormal..", target_card, card_effect.type);
}
@@ -3694,12 +3708,11 @@ void CardSpecial::clear_invalid_conditions_on_card(
}
}
const UnknownMatrixEntry* unknown_8024DAFC(
const InterferenceProbabilityEntry* get_interference_probability_entry(
uint16_t row_card_id,
uint16_t column_card_id,
bool use_entry_v1,
size_t* out_entry_index) {
static const UnknownMatrixEntry entries[] = {
bool is_attack) {
static const InterferenceProbabilityEntry entries[] = {
{0x0004, 0xFF, 0xFF},
{0x0002, 0x04, 0x00},
{0x0002, 0x00, 0x0F},
@@ -3849,23 +3862,20 @@ const UnknownMatrixEntry* unknown_8024DAFC(
};
constexpr size_t num_entries = sizeof(entries) / sizeof(entries[0]);
const UnknownMatrixEntry* ret_entry = nullptr;
const InterferenceProbabilityEntry* ret_entry = nullptr;
int16_t current_max = -1;
size_t logical_index = 0;
uint16_t current_row_card_id = 0xFFFF;
for (size_t z = 0; z < num_entries; z++) {
const auto& entry = entries[z];
uint16_t current_column_card_id = entry.card_id;
if ((entry.unknown_v1 != 0xFF) || (entry.unknown_v2 != 0xFF)) {
if ((entry.attack_probability != 0xFF) || (entry.defense_probability != 0xFF)) {
if ((row_card_id == current_row_card_id) &&
(column_card_id == current_column_card_id)) {
uint8_t v = use_entry_v1 ? entry.unknown_v1 : entry.unknown_v2;
uint8_t v = is_attack ? entry.attack_probability : entry.defense_probability;
if (current_max <= v) {
ret_entry = &entry;
current_max = v;
if (out_entry_index) {
*out_entry_index = logical_index;
}
}
}
logical_index++;
@@ -4325,7 +4335,12 @@ void CardSpecial::unknown_8024A9D8(const ActionState& pa, uint16_t action_card_r
}
}
void CardSpecial::unknown_8024504C(shared_ptr<Card> unknown_p2) {
void CardSpecial::check_for_attack_interference(shared_ptr<Card> unknown_p2) {
// Note: This check is not part of the original implementation.
if (this->server()->base()->data_index->behavior_flags & BehaviorFlag::DISABLE_INTERFERENCE) {
return;
}
if (unknown_p2->action_chain.chain.damage <= 0) {
return;
}
@@ -4376,8 +4391,9 @@ void CardSpecial::unknown_8024504C(shared_ptr<Card> unknown_p2) {
return;
}
const auto* entry = unknown_8024DAFC(row_card_id, ally_sc_card_id, true);
if (!entry || (this->server()->get_random(99) >= entry->unknown_v1)) {
const auto* entry = get_interference_probability_entry(
row_card_id, ally_sc_card_id, true);
if (!entry || (this->server()->get_random(99) >= entry->attack_probability)) {
return;
}
+7 -8
View File
@@ -11,17 +11,16 @@ namespace Episode3 {
struct UnknownMatrixEntry {
struct InterferenceProbabilityEntry {
uint16_t card_id;
uint8_t unknown_v1;
uint8_t unknown_v2;
uint8_t attack_probability;
uint8_t defense_probability;
};
const UnknownMatrixEntry* unknown_8024DAFC(
const InterferenceProbabilityEntry* get_interference_probability_entry(
uint16_t row_card_id,
uint16_t column_card_id,
bool use_entry_v1,
size_t* out_entry_index = nullptr);
bool is_attack);
@@ -276,7 +275,7 @@ public:
void update_condition_orders(std::shared_ptr<Card> card);
int16_t max_all_attack_bonuses(size_t* out_count) const;
void unknown_80244AA8(std::shared_ptr<Card> card);
void unknown_80244E20(
void check_for_defense_interference(
std::shared_ptr<const Card> attacker_card,
std::shared_ptr<Card> target_card,
int16_t* inout_unknown_p4);
@@ -324,7 +323,7 @@ public:
void unknown_8024966C(std::shared_ptr<Card> unknown_p2, const ActionState* existing_as);
static std::shared_ptr<Card> sc_card_for_card(std::shared_ptr<Card> unknown_p2);
void unknown_8024A9D8(const ActionState& pa, uint16_t action_card_ref);
void unknown_8024504C(std::shared_ptr<Card> unknown_p2);
void check_for_attack_interference(std::shared_ptr<Card> unknown_p2);
template <uint8_t When1, uint8_t When2, uint8_t When3, uint8_t When4>
void unknown_t2(std::shared_ptr<Card> unknown_p2);
void unknown_8024997C(std::shared_ptr<Card> card);
+362 -162
View File
@@ -5,6 +5,7 @@
#include <array>
#include <deque>
#include <phosg/Filesystem.hh>
#include <phosg/Random.hh>
#include <phosg/Time.hh>
#include "../Loggers.hh"
@@ -17,6 +18,25 @@ namespace Episode3 {
const char* name_for_attack_medium(AttackMedium medium) {
switch (medium) {
case AttackMedium::UNKNOWN:
return "UNKNOWN";
case AttackMedium::PHYSICAL:
return "PHYSICAL";
case AttackMedium::TECH:
return "TECH";
case AttackMedium::UNKNOWN_03:
return "UNKNOWN_03";
case AttackMedium::INVALID_FF:
return "INVALID_FF";
default:
return "__INVALID__";
}
}
Location::Location() : Location(0, 0) { }
Location::Location(uint8_t x, uint8_t y) : Location(x, y, Direction::RIGHT) { }
Location::Location(uint8_t x, uint8_t y, Direction direction)
@@ -32,6 +52,11 @@ bool Location::operator!=(const Location& other) const {
return !this->operator==(other);
}
std::string Location::str() const {
return string_printf("Location[x=%hhu, y=%hhu, dir=%s, u=%hhu]",
this->x, this->y, name_for_direction(this->direction), this->unused);
}
void Location::clear() {
this->x = 0;
this->y = 0;
@@ -106,7 +131,7 @@ const char* name_for_direction(Direction d) {
case Direction::INVALID_FF:
return "INVALID_FF";
default:
return "__unknown__";
return "__INVALID__";
}
}
@@ -282,134 +307,159 @@ struct ConditionDescription {
};
static const vector<ConditionDescription> description_for_condition_type({
/* 0x00 */ {false, nullptr, nullptr},
/* 0x01 */ {true, "AP Boost", "Temporarily increase AP by N"},
/* 0x02 */ {false, "Rampage", "Rampage"},
/* 0x03 */ {true, "Multi Strike", "Duplicate attack N times"},
/* 0x04 */ {true, "Damage Modifier 1", "Set attack damage / AP to N after action cards applied (step 1)"},
/* 0x05 */ {false, "Immobile", "Give Immobile condition"},
/* 0x06 */ {false, "Hold", "Give Hold condition"},
/* 0x07 */ {false, nullptr, nullptr},
/* 0x08 */ {true, "TP Boost", "Add N TP temporarily during attack"},
/* 0x09 */ {true, "Give Damage", "Cause direct N HP loss"},
/* 0x0A */ {false, "Guom", "Give Guom condition"},
/* 0x0B */ {false, "Paralyze", "Give Paralysis condition"},
/* 0x0C */ {false, nullptr, nullptr},
/* 0x0D */ {false, "A/H Swap", "Swap AP and HP temporarily"},
/* 0x0E */ {false, "Pierce", "Attack SC directly even if they have items equipped"},
/* 0x0F */ {false, nullptr, nullptr},
/* 0x10 */ {true, "Heal", "Increase HP by N"},
/* 0x11 */ {false, "Return to Hand", "Return card to hand"},
/* 0x12 */ {false, nullptr, nullptr},
/* 0x13 */ {false, nullptr, nullptr},
/* 0x14 */ {false, "Acid", "Give Acid condition"},
/* 0x15 */ {false, nullptr, nullptr},
/* 0x16 */ {true, "Mighty Knuckle", "Temporarily increase AP by N, and set ATK dice to zero"},
/* 0x17 */ {true, "Unit Blow", "Temporarily increase AP by N * number of this card set within phase"},
/* 0x18 */ {false, "Curse", "Give Curse condition"},
/* 0x19 */ {false, "Combo (AP)", "Temporarily increase AP by number of this card set within phase"},
/* 0x1A */ {false, "Pierce/Rampage Block", "Block attack if Pierce/Rampage (?)"},
/* 0x1B */ {false, "Ability Trap", "Temporarily disable opponent abilities"},
/* 0x1C */ {false, "Freeze", "Give Freeze condition"},
/* 0x1D */ {false, "Anti-Abnormality", "Cure all conditions"},
/* 0x1E */ {false, nullptr, nullptr},
/* 0x1F */ {false, "Explosion", "Damage all SCs and FCs by number of this same card set * 2"},
/* 0x20 */ {false, nullptr, nullptr},
/* 0x21 */ {false, nullptr, nullptr},
/* 0x22 */ {false, nullptr, nullptr},
/* 0x23 */ {false, "Return to Deck", "Cancel discard and move to bottom of deck instead"},
/* 0x24 */ {false, "Aerial", "Give Aerial status"},
/* 0x25 */ {true, "AP Loss", "Make attacker temporarily lose N AP during defense"},
/* 0x26 */ {true, "Bonus From Leader", "Gain AP equal to the number of cards of type N on the field"},
/* 0x27 */ {false, "Free Maneuver", "Enable movement over occupied tiles"},
/* 0x28 */ {false, "Haste", "Make move actions free"},
/* 0x29 */ {true, "Clone", "Make setting this card free if at least one card of type N is already on the field"},
/* 0x2A */ {true, "DEF Disable by Cost", "Disable use of any defense cards costing between (N / 10) and (N % 10) points, inclusive"},
/* 0x2B */ {true, "Filial", "Increase controlling SC\'s HP by N when this card is destroyed"},
/* 0x2C */ {true, "Snatch", "Steal N EXP during attack"},
/* 0x2D */ {true, "Hand Disrupter", "Discard N cards from hand immediately"},
/* 0x2E */ {false, "Drop", "Give Drop condition"},
/* 0x2F */ {false, "Action Disrupter", "Destroy all action cards used by attacker"},
/* 0x30 */ {true, "Set HP", "Set HP to N"},
/* 0x31 */ {false, "Native Shield", "Block attacks from Native creatures"},
/* 0x32 */ {false, "A.Beast Shield", "Block attacks from A.Beast creatures"},
/* 0x33 */ {false, "Machine Shield", "Block attacks from Machine creatures"},
/* 0x34 */ {false, "Dark Shield", "Block attacks from Dark creatures"},
/* 0x35 */ {false, "Sword Shield", "Block attacks from Sword items"},
/* 0x36 */ {false, "Gun Shield", "Block attacks from Gun items"},
/* 0x37 */ {false, "Cane Shield", "Block attacks from Cane items"},
/* 0x38 */ {false, nullptr, nullptr},
/* 0x39 */ {false, nullptr, nullptr},
/* 0x3A */ {false, "Defender", "Make attacks go to setter of this card instead of original target"},
/* 0x3B */ {false, "Survival Decoys", "Redirect damage for multi-sided attack"},
/* 0x3C */ {true, "Give/Take EXP", "Give N EXP, or take if N is negative"},
/* 0x3D */ {false, nullptr, nullptr},
/* 0x3E */ {false, "Death Companion", "If this card has 1 or 2 HP, set its HP to N"},
/* 0x3F */ {true, "EXP Decoy", "If defender has EXP, lose EXP instead of getting damage when attacked"},
/* 0x40 */ {true, "Set MV", "Set MV to N"},
/* 0x41 */ {true, "Group", "Temporarily increase AP by N * number of this card on field, excluding itself"},
/* 0x42 */ {false, "Berserk", "User of this card receives the same damage as target, and isn\'t helped by target\'s defense cards"},
/* 0x43 */ {false, "Guard Creature", "Attacks on controlling SC damage this card instead"},
/* 0x44 */ {false, "Tech", "Technique cards cost 1 fewer ATK point"},
/* 0x45 */ {false, "Big Swing", "Increase all attacking ATK costs by 1"},
/* 0x46 */ {false, nullptr, nullptr},
/* 0x47 */ {false, "Shield Weapon", "Limit attacker\'s choice of target to guard items"},
/* 0x48 */ {false, "ATK Dice Boost", "Increase ATK dice roll by 1"},
/* 0x49 */ {false, nullptr, nullptr},
/* 0x4A */ {false, "Major Pierce", "If SC has over half of max HP, attacks target SC instead of equipped items"},
/* 0x4B */ {false, "Heavy Pierce", "If SC has 3 or more items equipped, attacks target SC instead of equipped items"},
/* 0x4C */ {false, "Major Rampage", "If SC has over half of max HP, attacks target SC and all equipped items"},
/* 0x4D */ {false, "Heavy Rampage", "If SC has 3 or more items equipped, attacks target SC and all equipped items"},
/* 0x4E */ {true, "AP Growth", "Permanently increase AP by N"},
/* 0x4F */ {true, "TP Growth", "Permanently increase TP by N"},
/* 0x50 */ {true, "Reborn", "If any card of type N is on the field, this card goes to the hand when destroyed instead of being discarded"},
/* 0x51 */ {true, "Copy", "Temporarily set AP/TP to N percent (or 100% if N is 0) of opponent\'s values"},
/* 0x52 */ {false, nullptr, nullptr},
/* 0x53 */ {true, "Misc. Guards", "Add N to card\'s defense value"},
/* 0x54 */ {true, "AP Override", "Set AP to N temporarily"},
/* 0x55 */ {true, "TP Override", "Set TP to N temporarily"},
/* 0x56 */ {false, "Return", "Return card to hand on destruction instead of discarding"},
/* 0x57 */ {false, "A/T Swap Perm", "Permanently swap AP and TP"},
/* 0x58 */ {false, "A/H Swap Perm", "Permanently swap AP and HP"},
/* 0x59 */ {true, "Slayers/Assassins", "Temporarily increase AP during attack"},
/* 0x5A */ {false, "Anti-Abnormality", "Remove all conditions"},
/* 0x5B */ {false, "Fixed Range", "Use SC\'s range instead of weapon or attack card ranges"},
/* 0x5C */ {false, "Elude", "SC does not lose HP when equipped items are destroyed"},
/* 0x5D */ {false, "Parry", "Forward attack to a random FC within one tile of original target, excluding attacker and original target"},
/* 0x5E */ {false, "Block Attack", "Completely block attack"},
/* 0x5F */ {false, nullptr, nullptr},
/* 0x60 */ {false, nullptr, nullptr},
/* 0x61 */ {true, "Combo (TP)", "Gain TP equal to the number of cards of type N on the field"},
/* 0x62 */ {true, "Misc. AP Bonuses", "Temporarily increase AP by N"},
/* 0x63 */ {true, "Misc. TP Bonuses", "Temporarily increase TP by N"},
/* 0x64 */ {false, nullptr, nullptr},
/* 0x65 */ {true, "Misc. Defense Bonuses", "Decrease damage by N"},
/* 0x66 */ {true, "Mostly Halfguards", "Reduce damage from incoming attack by N"},
/* 0x67 */ {false, "Periodic Field", "Swap immunity to tech or physical attacks"},
/* 0x68 */ {false, "FC Limit by Count", "Change FC limit from 8 ATK points total to 4 FCs total"},
/* 0x69 */ {false, nullptr, nullptr},
/* 0x6A */ {true, "MV Bonus", "Increase MV by N"},
/* 0x6B */ {true, "Forward Damage", "Give N damage back to attacker during defense (?) (TODO)"},
/* 0x6C */ {true, "Weak Spot / Influence", "Temporarily decrease AP by N"},
/* 0x6D */ {true, "Damage Modifier 2", "Set attack damage / AP after action cards applied (step 2)"},
/* 0x6E */ {true, "Weak Hit Block", "Block all attacks of N damage or less"},
/* 0x6F */ {true, "AP Silence", "Temporarily decrease AP of opponent by N"},
/* 0x70 */ {true, "TP Silence", "Temporarily decrease TP of opponent by N"},
/* 0x71 */ {false, "A/T Swap", "Temporarily swap AP and TP"},
/* 0x72 */ {true, "Halfguard", "Halve damage from attacks that would inflict N or more damage"},
/* 0x73 */ {false, nullptr, nullptr},
/* 0x74 */ {true, "Rampage AP Loss", "Temporarily reduce AP by N"},
/* 0x75 */ {false, nullptr, nullptr},
/* 0x76 */ {false, "Reflect", "Generate reverse attack"},
/* 0x77 */ {false, nullptr, nullptr},
/* 0x78 */ {false, nullptr, nullptr}, // Treated as "any condition" in find functions
/* 0x79 */ {false, nullptr, nullptr},
/* 0x7A */ {false, nullptr, nullptr},
/* 0x7B */ {false, nullptr, nullptr},
/* 0x7C */ {false, nullptr, nullptr},
/* 0x7D */ {false, nullptr, nullptr},
/* 0x00 */ {false, "NONE", nullptr},
/* 0x01 */ {true, "AP_BOOST", "Temporarily increase AP by N"},
/* 0x02 */ {false, "RAMPAGE", "Rampage"},
/* 0x03 */ {true, "MULTI_STRIKE", "Duplicate attack N times"},
/* 0x04 */ {true, "DAMAGE_MOD_1", "Set attack damage / AP to N after action cards applied (step 1)"},
/* 0x05 */ {false, "IMMOBILE", "Give Immobile condition"},
/* 0x06 */ {false, "HOLD", "Give Hold condition"},
/* 0x07 */ {false, "UNKNOWN_07", nullptr},
/* 0x08 */ {true, "TP_BOOST", "Add N TP temporarily during attack"},
/* 0x09 */ {true, "GIVE_DAMAGE", "Cause direct N HP loss"},
/* 0x0A */ {false, "GUOM", "Give Guom condition"},
/* 0x0B */ {false, "PARALYZE", "Give Paralysis condition"},
/* 0x0C */ {false, "UNKNOWN_0C", nullptr},
/* 0x0D */ {false, "A_H_SWAP", "Swap AP and HP temporarily"},
/* 0x0E */ {false, "PIERCE", "Attack SC directly even if they have items equipped"},
/* 0x0F */ {false, "UNKNOWN_0F", nullptr},
/* 0x10 */ {true, "HEAL", "Increase HP by N"},
/* 0x11 */ {false, "RETURN_TO_HAND", "Return card to hand"},
/* 0x12 */ {false, "UNKNOWN_12", nullptr},
/* 0x13 */ {false, "UNKNOWN_13", nullptr},
/* 0x14 */ {false, "ACID", "Give Acid condition"},
/* 0x15 */ {false, "UNKNOWN_15", nullptr},
/* 0x16 */ {true, "MIGHTY_KNUCKLE", "Temporarily increase AP by N, and set ATK dice to zero"},
/* 0x17 */ {true, "UNIT_BLOW", "Temporarily increase AP by N * number of this card set within phase"},
/* 0x18 */ {false, "CURSE", "Give Curse condition"},
/* 0x19 */ {false, "COMBO_AP", "Temporarily increase AP by number of this card set within phase"},
/* 0x1A */ {false, "PIERCE_RAMPAGE_BLOCK", "Block attack if Pierce/Rampage (?)"},
/* 0x1B */ {false, "ABILITY_TRAP", "Temporarily disable opponent abilities"},
/* 0x1C */ {false, "FREEZE", "Give Freeze condition"},
/* 0x1D */ {false, "ANTI_ABNORMALITY_1", "Cure all conditions"},
/* 0x1E */ {false, "UNKNOWN_1E", nullptr},
/* 0x1F */ {false, "EXPLOSION", "Damage all SCs and FCs by number of this same card set * 2"},
/* 0x20 */ {false, "UNKNOWN_20", nullptr},
/* 0x21 */ {false, "UNKNOWN_21", nullptr},
/* 0x22 */ {false, "UNKNOWN_22", nullptr},
/* 0x23 */ {false, "RETURN_TO_DECK", "Cancel discard and move to bottom of deck instead"},
/* 0x24 */ {false, "AERIAL", "Give Aerial status"},
/* 0x25 */ {true, "AP_LOSS", "Make attacker temporarily lose N AP during defense"},
/* 0x26 */ {true, "BONUS_FROM_LEADER", "Gain AP equal to the number of cards of type N on the field"},
/* 0x27 */ {false, "FREE_MANEUVER", "Enable movement over occupied tiles"},
/* 0x28 */ {false, "HASTE", "Make move actions free"},
/* 0x29 */ {true, "CLONE", "Make setting this card free if at least one card of type N is already on the field"},
/* 0x2A */ {true, "DEF_DISABLE_BY_COST", "Disable use of any defense cards costing between (N / 10) and (N % 10) points, inclusive"},
/* 0x2B */ {true, "FILIAL", "Increase controlling SC\'s HP by N when this card is destroyed"},
/* 0x2C */ {true, "SNATCH", "Steal N EXP during attack"},
/* 0x2D */ {true, "HAND_DISRUPTER", "Discard N cards from hand immediately"},
/* 0x2E */ {false, "DROP", "Give Drop condition"},
/* 0x2F */ {false, "ACTION_DISRUPTER", "Destroy all action cards used by attacker"},
/* 0x30 */ {true, "SET_HP", "Set HP to N"},
/* 0x31 */ {false, "NATIVE_SHIELD", "Block attacks from Native creatures"},
/* 0x32 */ {false, "A_BEAST_SHIELD", "Block attacks from A.Beast creatures"},
/* 0x33 */ {false, "MACHINE_SHIELD", "Block attacks from Machine creatures"},
/* 0x34 */ {false, "DARK_SHIELD", "Block attacks from Dark creatures"},
/* 0x35 */ {false, "SWORD_SHIELD", "Block attacks from Sword items"},
/* 0x36 */ {false, "GUN_SHIELD", "Block attacks from Gun items"},
/* 0x37 */ {false, "CANE_SHIELD", "Block attacks from Cane items"},
/* 0x38 */ {false, "UNKNOWN_38", nullptr},
/* 0x39 */ {false, "UNKNOWN_39", nullptr},
/* 0x3A */ {false, "DEFENDER", "Make attacks go to setter of this card instead of original target"},
/* 0x3B */ {false, "SURVIVAL_DECOYS", "Redirect damage for multi-sided attack"},
/* 0x3C */ {true, "GIVE_OR_TAKE_EXP", "Give N EXP, or take if N is negative"},
/* 0x3D */ {false, "UNKNOWN_3D", nullptr},
/* 0x3E */ {false, "DEATH_COMPANION", "If this card has 1 or 2 HP, set its HP to N"},
/* 0x3F */ {true, "EXP_DECOY", "If defender has EXP, lose EXP instead of getting damage when attacked"},
/* 0x40 */ {true, "SET_MV", "Set MV to N"},
/* 0x41 */ {true, "GROUP", "Temporarily increase AP by N * number of this card on field, excluding itself"},
/* 0x42 */ {false, "BERSERK", "User of this card receives the same damage as target, and isn\'t helped by target\'s defense cards"},
/* 0x43 */ {false, "GUARD_CREATURE", "Attacks on controlling SC damage this card instead"},
/* 0x44 */ {false, "TECH", "Technique cards cost 1 fewer ATK point"},
/* 0x45 */ {false, "BIG_SWING", "Increase all attacking ATK costs by 1"},
/* 0x46 */ {false, "UNKNOWN_46", nullptr},
/* 0x47 */ {false, "SHIELD_WEAPON", "Limit attacker\'s choice of target to guard items"},
/* 0x48 */ {false, "ATK_DICE_BOOST", "Increase ATK dice roll by 1"},
/* 0x49 */ {false, "UNKNOWN_49", nullptr},
/* 0x4A */ {false, "MAJOR_PIERCE", "If SC has over half of max HP, attacks target SC instead of equipped items"},
/* 0x4B */ {false, "HEAVY_PIERCE", "If SC has 3 or more items equipped, attacks target SC instead of equipped items"},
/* 0x4C */ {false, "MAJOR_RAMPAGE", "If SC has over half of max HP, attacks target SC and all equipped items"},
/* 0x4D */ {false, "HEAVY_RAMPAGE", "If SC has 3 or more items equipped, attacks target SC and all equipped items"},
/* 0x4E */ {true, "AP_GROWTH", "Permanently increase AP by N"},
/* 0x4F */ {true, "TP_GROWTH", "Permanently increase TP by N"},
/* 0x50 */ {true, "REBORN", "If any card of type N is on the field, this card goes to the hand when destroyed instead of being discarded"},
/* 0x51 */ {true, "COPY", "Temporarily set AP/TP to N percent (or 100% if N is 0) of opponent\'s values"},
/* 0x52 */ {false, "UNKNOWN_52", nullptr},
/* 0x53 */ {true, "MISC_GUARDS", "Add N to card\'s defense value"},
/* 0x54 */ {true, "AP_OVERRIDE", "Set AP to N temporarily"},
/* 0x55 */ {true, "TP_OVERRIDE", "Set TP to N temporarily"},
/* 0x56 */ {false, "RETURN", "Return card to hand on destruction instead of discarding"},
/* 0x57 */ {false, "A_T_SWAP_PERM", "Permanently swap AP and TP"},
/* 0x58 */ {false, "A_H_SWAP_PERM", "Permanently swap AP and HP"},
/* 0x59 */ {true, "SLAYERS_ASSASSINS", "Temporarily increase AP during attack"},
/* 0x5A */ {false, "ANTI_ABNORMALITY_2", "Remove all conditions"},
/* 0x5B */ {false, "FIXED_RANGE", "Use SC\'s range instead of weapon or attack card ranges"},
/* 0x5C */ {false, "ELUDE", "SC does not lose HP when equipped items are destroyed"},
/* 0x5D */ {false, "PARRY", "Forward attack to a random FC within one tile of original target, excluding attacker and original target"},
/* 0x5E */ {false, "BLOCK_ATTACK", "Completely block attack"},
/* 0x5F */ {false, "UNKNOWN_5F", nullptr},
/* 0x60 */ {false, "UNKNOWN_60", nullptr},
/* 0x61 */ {true, "COMBO_TP", "Gain TP equal to the number of cards of type N on the field"},
/* 0x62 */ {true, "MISC_AP_BONUSES", "Temporarily increase AP by N"},
/* 0x63 */ {true, "MISC_TP_BONUSES", "Temporarily increase TP by N"},
/* 0x64 */ {false, "UNKNOWN_64", nullptr},
/* 0x65 */ {true, "MISC_DEFENSE_BONUSES", "Decrease damage by N"},
/* 0x66 */ {true, "MOSTLY_HALFGUARDS", "Reduce damage from incoming attack by N"},
/* 0x67 */ {false, "PERIODIC_FIELD", "Swap immunity to tech or physical attacks"},
/* 0x68 */ {false, "FC_LIMIT_BY_COUNT", "Change FC limit from 8 ATK points total to 4 FCs total"},
/* 0x69 */ {false, "UNKNOWN_69", nullptr},
/* 0x6A */ {true, "MV_BONUS", "Increase MV by N"},
/* 0x6B */ {true, "FORWARD_DAMAGE", "Give N damage back to attacker during defense (?) (TODO)"},
/* 0x6C */ {true, "WEAK_SPOT_INFLUENCE", "Temporarily decrease AP by N"},
/* 0x6D */ {true, "DAMAGE_MODIFIER_2", "Set attack damage / AP after action cards applied (step 2)"},
/* 0x6E */ {true, "WEAK_HIT_BLOCK", "Block all attacks of N damage or less"},
/* 0x6F */ {true, "AP_SILENCE", "Temporarily decrease AP of opponent by N"},
/* 0x70 */ {true, "TP_SILENCE", "Temporarily decrease TP of opponent by N"},
/* 0x71 */ {false, "A_T_SWAP", "Temporarily swap AP and TP"},
/* 0x72 */ {true, "HALFGUARD", "Halve damage from attacks that would inflict N or more damage"},
/* 0x73 */ {false, "UNKNOWN_73", nullptr},
/* 0x74 */ {true, "RAMPAGE_AP_LOSS", "Temporarily reduce AP by N"},
/* 0x75 */ {false, "UNKNOWN_75", nullptr},
/* 0x76 */ {false, "REFLECT", "Generate reverse attack"},
/* 0x77 */ {false, "UNKNOWN_77", nullptr},
/* 0x78 */ {false, "ANY", nullptr}, // Treated as "any condition" in find functions
/* 0x79 */ {false, "UNKNOWN_79", nullptr},
/* 0x7A */ {false, "UNKNOWN_7A", nullptr},
/* 0x7B */ {false, "UNKNOWN_7B", nullptr},
/* 0x7C */ {false, "UNKNOWN_7C", nullptr},
/* 0x7D */ {false, "UNKNOWN_7D", nullptr},
});
const char* name_for_condition_type(ConditionType cond_type) {
try {
return description_for_condition_type.at(static_cast<size_t>(cond_type)).name;
} catch (const out_of_range&) {
return "__INVALID__";
}
}
const char* name_for_action_subphase(ActionSubphase subphase) {
switch (subphase) {
case ActionSubphase::ATTACK:
return "ATTACK";
case ActionSubphase::DEFENSE:
return "DEFENSE";
case ActionSubphase::INVALID_FF:
return "INVALID_FF";
default:
return "__INVALID__";
}
}
void CardDefinition::Stat::decode_code() {
this->type = static_cast<Type>(this->code / 1000);
int16_t value = this->code - (this->type * 1000);
@@ -791,6 +841,50 @@ Rules::Rules() {
this->clear();
}
Rules::Rules(shared_ptr<const JSONObject> json) {
auto dict = json->as_dict();
this->overall_time_limit = dict.at("overall_time_limit")->as_int();
this->phase_time_limit = dict.at("phase_time_limit")->as_int();
this->allowed_cards = static_cast<AllowedCards>(dict.at("allowed_cards")->as_int());
this->min_dice = dict.at("min_dice")->as_int();
this->max_dice = dict.at("max_dice")->as_int();
this->disable_deck_shuffle = dict.at("disable_deck_shuffle")->as_int();
this->disable_deck_loop = dict.at("disable_deck_loop")->as_int();
this->char_hp = dict.at("char_hp")->as_int();
this->hp_type = static_cast<HPType>(dict.at("hp_type")->as_int());
this->no_assist_cards = dict.at("no_assist_cards")->as_int();
this->disable_dialogue = dict.at("disable_dialogue")->as_int();
this->dice_exchange_mode = static_cast<DiceExchangeMode>(dict.at("dice_exchange_mode")->as_int());
this->disable_dice_boost = dict.at("disable_dice_boost")->as_int();
}
shared_ptr<JSONObject> Rules::json() const {
unordered_map<string, shared_ptr<JSONObject>> dict;
dict.emplace("overall_time_limit", make_json_int(this->overall_time_limit));
dict.emplace("phase_time_limit", make_json_int(this->phase_time_limit));
dict.emplace("allowed_cards", make_json_int(static_cast<uint8_t>(this->allowed_cards)));
dict.emplace("min_dice", make_json_int(this->min_dice));
dict.emplace("max_dice", make_json_int(this->max_dice));
dict.emplace("disable_deck_shuffle", make_json_int(this->disable_deck_shuffle));
dict.emplace("disable_deck_loop", make_json_int(this->disable_deck_loop));
dict.emplace("char_hp", make_json_int(this->char_hp));
dict.emplace("hp_type", make_json_int(static_cast<uint8_t>(this->hp_type)));
dict.emplace("no_assist_cards", make_json_int(this->no_assist_cards));
dict.emplace("disable_dialogue", make_json_int(this->disable_dialogue));
dict.emplace("dice_exchange_mode", make_json_int(static_cast<uint8_t>(this->dice_exchange_mode)));
dict.emplace("disable_dice_boost", make_json_int(this->disable_dice_boost));
return shared_ptr<JSONObject>(new JSONObject(move(dict)));
}
void Rules::set_defaults() {
this->clear();
this->overall_time_limit = 24; // 2 hours
this->phase_time_limit = 30;
this->min_dice = 1;
this->max_dice = 6;
this->char_hp = 15;
}
void Rules::clear() {
this->overall_time_limit = 0;
this->phase_time_limit = 0;
@@ -949,7 +1043,7 @@ string MapDefinition::str(const DataIndex* data_index) const {
lines.emplace_back(string_printf("Map %08" PRIX32 ": %hhux%hhu",
this->map_number.load(), this->width, this->height));
lines.emplace_back(string_printf(" a1=%08" PRIX32, this->unknown_a1.load()));
lines.emplace_back(string_printf(" scene_data2=%02hhX", this->scene_data2));
lines.emplace_back(string_printf(" environment_number=%02hhX", this->environment_number));
lines.emplace_back(string_printf(" num_alt_maps=%02hhX", this->num_alt_maps));
lines.emplace_back(string_printf(" num_alt_maps=%02hhX", this->num_alt_maps));
lines.emplace_back(" tiles:");
@@ -980,10 +1074,11 @@ string MapDefinition::str(const DataIndex* data_index) const {
}
}
for (size_t w = 0; w < 3; w++) {
for (size_t z = 0; z < 0x24; z += 4) {
lines.emplace_back(string_printf(" a5[%zu][0x%02zX:0x%02zX]=%g %g %g %g", w, z, z + 4,
this->unknown_a5[w][z + 0].load(), this->unknown_a5[w][z + 1].load(),
this->unknown_a5[w][z + 2].load(), this->unknown_a5[w][z + 3].load()));
for (size_t z = 0; z < 0x24; z += 3) {
lines.emplace_back(string_printf(" a5[%zu][0x%02zX:0x%02zX]=%g %g %g", w, z, z + 3,
this->unknown_a5[w][z + 0].load(),
this->unknown_a5[w][z + 1].load(),
this->unknown_a5[w][z + 2].load()));
}
}
lines.emplace_back(" modification tiles:");
@@ -1014,7 +1109,8 @@ string MapDefinition::str(const DataIndex* data_index) const {
this->npc_chars[z].unknown_a2[2], this->npc_chars[z].unknown_a2[3]));
lines.emplace_back(" name: " + string(this->npc_chars[z].name));
for (size_t w = 0; w < 0x78; w += 0x08) {
lines.emplace_back(string_printf(" a3[0x%02zX:0x%02zX]=%04hX %04hX %04hX %04hX %04hX %04hX %04hX %04hX", z, z + 0x08,
lines.emplace_back(string_printf(" a3[0x%02zX:0x%02zX]=%04hX %04hX %04hX %04hX %04hX %04hX %04hX %04hX",
w, w + 0x08,
this->npc_chars[z].unknown_a3[w + 0x00].load(), this->npc_chars[z].unknown_a3[w + 0x01].load(),
this->npc_chars[z].unknown_a3[w + 0x02].load(), this->npc_chars[z].unknown_a3[w + 0x03].load(),
this->npc_chars[z].unknown_a3[w + 0x04].load(), this->npc_chars[z].unknown_a3[w + 0x05].load(),
@@ -1080,11 +1176,53 @@ string MapDefinition::str(const DataIndex* data_index) const {
}
lines.emplace_back(" a9=" + format_data_string(this->unknown_a9.data(), this->unknown_a9.bytes()));
lines.emplace_back(" a11=" + format_data_string(this->unknown_a11.data(), this->unknown_a11.bytes()));
lines.emplace_back(" unavailable_sc_cards=" + format_data_string(this->unavailable_sc_cards.data(), this->unavailable_sc_cards.bytes()));
for (size_t z = 0; z < 4; z++) {
string player_type;
switch (this->entry_states[z].player_type) {
case 0x00:
player_type = "Player";
break;
case 0x01:
player_type = "Player/COM";
break;
case 0x02:
player_type = "COM";
break;
case 0x03:
player_type = "FIXED_COM";
break;
case 0x04:
player_type = "NONE";
break;
case 0xFF:
player_type = "FREE";
break;
default:
player_type = string_printf("(%02hhX)", this->entry_states[z].player_type);
break;
}
string deck_type;
switch (this->entry_states[z].deck_type) {
case 0x00:
deck_type = "HERO ONLY";
break;
case 0x01:
deck_type = "DARK ONLY";
break;
case 0xFF:
deck_type = "any deck allowed";
break;
default:
deck_type = string_printf("(%02hhX)", this->entry_states[z].deck_type);
break;
}
lines.emplace_back(string_printf(
" entry_states[%zu] = %s / %s", z, player_type.c_str(), deck_type.c_str()));
}
return join(lines, "\n");
}
bool Rules::check_invalid_fields() const {
Rules t = *this;
return t.check_and_reset_invalid_fields();
@@ -1166,7 +1304,12 @@ DataIndex::DataIndex(const string& directory, uint32_t behavior_flags)
StringReader r(data);
while (!r.eof()) {
uint32_t card_id = stoul(r.get_cstr());
string card_id_str = r.get_cstr();
if (card_id_str.empty() || (static_cast<uint8_t>(card_id_str[0]) == 0xFF)) {
break;
}
strip_leading_whitespace(card_id_str);
uint32_t card_id = stoul(card_id_str);
// Read all pages for this card
string text;
@@ -1248,9 +1391,11 @@ DataIndex::DataIndex(const string& directory, uint32_t behavior_flags)
try {
string decompressed_data;
if (isfile(directory + "/card-definitions.mnrd")) {
this->mtime_for_card_definitions = stat(directory + "/card-definitions.mnrd").st_mtime;
decompressed_data = load_file(directory + "/card-definitions.mnrd");
this->compressed_card_definitions.clear();
} else {
this->mtime_for_card_definitions = stat(directory + "/card-definitions.mnr").st_mtime;
this->compressed_card_definitions = load_file(directory + "/card-definitions.mnr");
decompressed_data = prs_decompress(this->compressed_card_definitions);
}
@@ -1319,36 +1464,62 @@ DataIndex::DataIndex(const string& directory, uint32_t behavior_flags)
static_game_data_log.warning("Failed to load Episode 3 card update: %s", e.what());
}
for (const auto& filename : list_directory(directory)) {
try {
shared_ptr<MapEntry> entry;
auto add_maps_from_dir = [&](const string& dir, bool is_quest) -> void {
for (const auto& filename : list_directory(dir)) {
try {
shared_ptr<MapEntry> entry;
if (ends_with(filename, ".mnmd")) {
entry.reset(new MapEntry(load_object_file<MapDefinition>(directory + "/" + filename)));
} else if (ends_with(filename, ".mnm")) {
entry.reset(new MapEntry(load_file(directory + "/" + filename)));
}
if (entry.get()) {
if (!this->maps.emplace(entry->map.map_number, entry).second) {
throw runtime_error("duplicate map number");
if (ends_with(filename, ".mnmd") || ends_with(filename, ".bind")) {
entry.reset(new MapEntry(load_object_file<MapDefinition>(dir + "/" + filename), is_quest));
} else if (ends_with(filename, ".mnm") || ends_with(filename, ".bin")) {
entry.reset(new MapEntry(load_file(dir + "/" + filename), is_quest));
}
string name = entry->map.name;
static_game_data_log.info("Indexed Episode 3 map %s (%08" PRIX32 "; %s)",
filename.c_str(), entry->map.map_number.load(), name.c_str());
}
} catch (const exception& e) {
static_game_data_log.warning("Failed to index Episode 3 map %s: %s",
filename.c_str(), e.what());
if (entry.get()) {
if (!this->maps.emplace(entry->map.map_number, entry).second) {
throw runtime_error("duplicate map number");
}
this->maps_by_name.emplace(entry->map.name, entry);
string name = entry->map.name;
static_game_data_log.info("Indexed Episode 3 %s %s (%08" PRIX32 "; %s)",
is_quest ? "online quest" : "free battle map",
filename.c_str(), entry->map.map_number.load(), name.c_str());
}
} catch (const exception& e) {
static_game_data_log.warning("Failed to index Episode 3 map %s: %s",
filename.c_str(), e.what());
}
}
};
add_maps_from_dir(directory + "/maps-free", false);
add_maps_from_dir(directory + "/maps-quest", true);
try {
auto json = JSONObject::parse(load_file(directory + "/com-decks.json"));
for (const auto& def_json : json->as_list()) {
auto& def = this->com_decks.emplace_back(new COMDeckDefinition());
def->index = this->com_decks.size() - 1;
def->player_name = def_json->at(0)->as_string();
def->deck_name = def_json->at(1)->as_string();
auto card_ids_json = def_json->at(2)->as_list();
for (size_t z = 0; z < 0x1F; z++) {
def->card_ids[z] = card_ids_json.at(z)->as_int();
}
if (!this->com_decks_by_name.emplace(def->deck_name, def).second) {
throw runtime_error("duplicate COM deck name: " + def->deck_name);
}
}
} catch (const exception& e) {
static_game_data_log.warning("Failed to load Episode 3 COM decks: %s", e.what());
}
}
DataIndex::MapEntry::MapEntry(const MapDefinition& map) : map(map) { }
DataIndex::MapEntry::MapEntry(const MapDefinition& map, bool is_quest)
: map(map), is_quest(is_quest) { }
DataIndex::MapEntry::MapEntry(const string& compressed)
: compressed_data(compressed) {
DataIndex::MapEntry::MapEntry(const string& compressed, bool is_quest)
: is_quest(is_quest), compressed_data(compressed) {
string decompressed = prs_decompress(this->compressed_data);
if (decompressed.size() != sizeof(MapDefinition)) {
throw runtime_error(string_printf(
@@ -1390,6 +1561,10 @@ set<uint32_t> DataIndex::all_card_ids() const {
return ret;
}
uint64_t DataIndex::card_definitions_mtime() const {
return this->mtime_for_card_definitions;
}
const string& DataIndex::get_compressed_map_list() const {
if (this->compressed_map_list.empty()) {
// TODO: Write a version of prs_compress that takes iovecs (or something
@@ -1402,7 +1577,7 @@ const string& DataIndex::get_compressed_map_list() const {
const auto& map = map_it.second->map;
e.map_x = map.map_x;
e.map_y = map.map_y;
e.scene_data2 = map.scene_data2;
e.environment_number = map.environment_number;
e.map_number = map.map_number.load();
e.width = map.width;
e.height = map.height;
@@ -1422,7 +1597,7 @@ const string& DataIndex::get_compressed_map_list() const {
strings_w.write(map.description.data(), map.description.len());
strings_w.put_u8(0);
e.unknown_a2 = 0xFF000000;
e.unknown_a1 = map_it.second->is_quest ? 0x00 : 0xFF;
entries_w.put(e);
}
@@ -1442,8 +1617,12 @@ const string& DataIndex::get_compressed_map_list() const {
compressed_w.put_u32b(prs.input_size());
compressed_w.write(prs.close());
this->compressed_map_list = move(compressed_w.str());
static_game_data_log.info("Generated Episode 3 compressed map list (%zu -> %zu bytes)",
this->compressed_map_list.size(), this->compressed_map_list.size());
if (this->compressed_map_list.size() > 0x7BEC) {
throw runtime_error("Episode 3 compressed map list is too large");
}
size_t decompressed_size = sizeof(header) + entries_w.size() + strings_w.size();
static_game_data_log.info("Generated Episode 3 compressed map list (0x%zX -> 0x%zX bytes)",
decompressed_size, this->compressed_map_list.size());
}
return this->compressed_map_list;
}
@@ -1452,6 +1631,11 @@ shared_ptr<const DataIndex::MapEntry> DataIndex::definition_for_map_number(uint3
return this->maps.at(id);
}
shared_ptr<const DataIndex::MapEntry> DataIndex::definition_for_map_name(
const string& name) const {
return this->maps_by_name.at(name);
}
set<uint32_t> DataIndex::all_map_ids() const {
set<uint32_t> ret;
for (const auto& it : this->maps) {
@@ -1460,6 +1644,22 @@ set<uint32_t> DataIndex::all_map_ids() const {
return ret;
}
size_t DataIndex::num_com_decks() const {
return this->com_decks.size();
}
shared_ptr<const COMDeckDefinition> DataIndex::com_deck(size_t which) const {
return this->com_decks.at(which);
}
shared_ptr<const COMDeckDefinition> DataIndex::com_deck(const string& which) const {
return this->com_decks_by_name.at(which);
}
shared_ptr<const COMDeckDefinition> DataIndex::random_com_deck() const {
return this->com_decks[random_object<size_t>() % this->com_decks.size()];
}
} // namespace Episode3
+158 -25
View File
@@ -8,6 +8,7 @@
#include <memory>
#include <unordered_map>
#include <phosg/Encoding.hh>
#include <phosg/JSON.hh>
#include "../Text.hh"
@@ -31,7 +32,8 @@ enum BehaviorFlag {
ENABLE_STATUS_MESSAGES = 0x00000010,
LOAD_CARD_TEXT = 0x00000020,
ENABLE_RECORDING = 0x00000040,
ENABLE_MASKING = 0x00000080,
DISABLE_MASKING = 0x00000080,
DISABLE_INTERFERENCE = 0x00000100,
};
@@ -56,6 +58,8 @@ enum class AttackMedium : uint8_t {
INVALID_FF = 0xFF,
};
const char* name_for_attack_medium(AttackMedium medium);
enum class CriterionCode : uint8_t {
NONE = 0x00,
HU_CLASS_SC = 0x01,
@@ -291,6 +295,8 @@ enum class ConditionType : uint8_t {
ANY_FF = 0xFF, // Used as a wildcard in some search functions
};
const char* name_for_condition_type(ConditionType cond_type);
enum class AssistEffect : uint16_t {
NONE = 0x0000,
DICE_HALF = 0x0001,
@@ -387,6 +393,8 @@ enum class ActionSubphase : uint8_t {
INVALID_FF = 0xFF,
};
const char* name_for_action_subphase(ActionSubphase subphase);
enum class SetupPhase : uint8_t {
REGISTRATION = 0,
STARTER_ROLLS = 1,
@@ -432,6 +440,8 @@ struct Location {
bool operator==(const Location& other) const;
bool operator!=(const Location& other) const;
std::string str() const;
void clear();
void clear_FF();
} __attribute__((packed));
@@ -572,16 +582,50 @@ struct DeckDefinition {
} __attribute__((packed)); // 0x84 bytes in total
struct PlayerConfig {
// Offsets in comments in this struct are relative to start of 61/98 command
/* 0728 */ parray<uint8_t, 0x1434> unknown_a1;
/* 1B5C */ parray<DeckDefinition, 25> decks;
/* 2840 */ uint64_t unknown_a2;
/* 2848 */ be_uint32_t offline_clv_exp; // CLvOff = this / 100
/* 284C */ be_uint32_t online_clv_exp; // CLvOn = this / 100
/* 2850 */ parray<uint8_t, 0x14C> unknown_a3;
/* 299C */ ptext<char, 0x10> name;
// Other records are probably somewhere in here - e.g. win/loss, play time, etc.
/* 29AC */ parray<uint8_t, 0xCC> unknown_a4;
// The first offsets in the comments in this struct are relative to start of
// 61/98 command; the second are relative to the start of the
// Ep3PlayerDataSegment structure in the reverse-engineering project.
// TODO: Fill in the unknown fields here by looking around callsites of
// get_player_data_segment
/* 0728:---- */ parray<uint8_t, 0x154> unknown_a1;
/* 087C:0000 */ uint8_t is_encrypted;
/* 087D:0001 */ uint8_t basis;
/* 087E:0002 */ parray<uint8_t, 2> unused;
// The following fields (here through the beginning of decks) are encrypted
// using the trivial algorithm, with the basis specified above, if
// is_encrypted is equal to 1.
// It appears the card counts field in this structure is actually 1000 (0x3E8)
// bytes long, even though in every other place the counts array appears it's
// 0x2F0 bytes long. They presumably did this because of the checksum logic.
/* 0880:0004 */ parray<uint8_t, 1000> card_counts;
// These appear to be an attempt at checksumming the card counts array, but
// the algorithm don't cover the entire array and instead reads from later
// parts of this structure. This appears to be due to a copy/paste error in
// the original code. The algorithm sums card_counts [0] through [19] and puts
// the result in card_count_checksums[0], then sums card counts [50] through
// [69] and puts the result in card_count_checksums[1], etc. Presumably they
// intended to use 20 as the stride instead of 50, which would have exactly
// covered the entire card_counts array.
/* 0C68:03EC */ parray<be_uint16_t, 50> card_count_checksums;
// Yes, these are actually 64-bit integers. They include card IDs and some
// other data, encoded in a way I don't fully understand yet.
/* 0CCC:0450 */ parray<be_uint64_t, 0x1C2> unknown_a4;
/* 1ADC:1260 */ parray<uint8_t, 0x80> unknown_a7;
/* 1B5C:12E0 */ parray<DeckDefinition, 25> decks;
/* 2840:1FC4 */ parray<uint8_t, 0x08> unknown_a8;
/* 2848:1FCC */ be_uint32_t offline_clv_exp; // CLvOff = this / 100
/* 284C:1FD0 */ be_uint32_t online_clv_exp; // CLvOn = this / 100
struct PlayerReference {
/* 00 */ be_uint32_t guild_card_number;
/* 04 */ ptext<char, 0x18> player_name;
} __attribute__((packed));
// TODO: What do these player references mean? When are entries added to or
// removed from this list?
/* 2850:1FD4 */ parray<PlayerReference, 9> unknown_a9;
/* 294C:20D0 */ parray<uint8_t, 0x50> unknown_a10;
/* 299C:2120 */ ptext<char, 0x10> name;
/* 29AC:2130 */ parray<uint8_t, 0xCC> unknown_a11;
/* 2A78:21FC */
} __attribute__((packed));
enum class HPType : uint8_t {
@@ -626,9 +670,12 @@ struct Rules {
parray<uint8_t, 3> unused;
Rules();
explicit Rules(std::shared_ptr<const JSONObject> json);
std::shared_ptr<JSONObject> json() const;
bool operator==(const Rules& other) const;
bool operator!=(const Rules& other) const;
void clear();
void set_defaults();
bool check_invalid_fields() const;
bool check_and_reset_invalid_fields();
@@ -668,7 +715,7 @@ struct MapList {
struct Entry { // Should be 0x220 bytes in total
be_uint16_t map_x;
be_uint16_t map_y;
be_uint16_t scene_data2;
be_uint16_t environment_number;
be_uint16_t map_number;
// Text offsets are from the beginning of the strings block after all map
// entries (that is, add strings_offset to them to get the string offset)
@@ -680,7 +727,10 @@ struct MapList {
be_uint16_t height;
parray<parray<uint8_t, 0x10>, 0x10> map_tiles;
parray<parray<uint8_t, 0x10>, 0x10> modification_tiles;
be_uint32_t unknown_a2; // Seems to always be 0xFF000000
// This appears to be 0xFF for free battle maps, and 0 for quests.
// TODO: Figure out what this field's meaning actually is
uint8_t unknown_a1;
parray<uint8_t, 3> unused;
} __attribute__((packed));
// Variable-length fields:
@@ -699,19 +749,46 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
/* 0004 */ be_uint32_t map_number;
/* 0008 */ uint8_t width;
/* 0009 */ uint8_t height;
/* 000A */ uint8_t scene_data2; // TODO: What is this?
// The environment number specifies several things:
// - The model to load for the main battle stage
// - The music to play during the main battle
// - The color of the battle tile outlines (probably)
// - The preview image to show in the upper-left corner in the map select menu
// The environment numbers are:
// 00 - Unguis Lapis
// 01 - Nebula Montana (1)
// 02 - Lupus Silva (1)
// 03 - Lupus Silva (2)
// 04 - Molae Venti
// 05 - Nebula Montana (2)
// 06 - Tener Sinus
// 07 - Mortis Fons
// 08 - Morgue (destroyed)
// 09 - Tower of Caelum
// 0A = ??? (referred to as "^mapname"; crashes)
// 0B = Cyber
// 0C = Morgue (not destroyed)
// 0D = (Castor/Pollux map)
// 0E - Dolor Odor
// 0F = Ravum Aedes Sacra
// 10 - (Amplum Umbla map)
// 11 - Via Tubus
// 12 = Morgue (same as 08?)
// 13 = ??? (crashes)
// Environment numbers beyond 13 are not used in any known quests or maps.
/* 000A */ uint8_t environment_number;
// All alt_maps fields (including the floats) past num_alt_maps are filled in
// with FF. For example, if num_alt_maps == 8, the last two fields in each
// alt_maps array are filled with FF.
/* 000B */ uint8_t num_alt_maps; // TODO: What are the alt maps for?
// In the map_tiles array, the values are:
// 00 = not a valid tile
// 01 = valid tile unless punched out (later)
// In the map_tiles array, the values are usually:
// 00 = not a valid tile (blocked)
// 01 = valid tile unless modified out (via modification_tiles)
// 02 = team A start (1v1)
// 03, 04 = team A start (2v2)
// 05 = ???
// 06, 07 = team B start (2v2)
// 08 = team B start (1v1)
// These values can be redefined by start_tile_definitions below, however.
// Note that the game displays the map reversed vertically in the preview
// window. For example, player 1 is on team A, which usually starts at the top
// of the map as defined in this struct, or at the bottom as shown in the
@@ -728,13 +805,13 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
/* 1518 */ parray<be_float, 0x12> alt_maps_unknown_a3[2][0x0A];
/* 1AB8 */ parray<be_float, 0x24> unknown_a5[3];
// In the modification_tiles array, the values are:
// 10 = blocked (as if the corresponding map_tiles value was 00)
// 20 = blocked (maybe one of 10 or 20 are passable by Aerial characters)
// 10 = blocked by rock (as if the corresponding map_tiles value was 00)
// 20 = blocked by fence (as if the corresponding map_tiles value was 00)
// 30-34 = teleporters (2 of each value may be present)
// 40-44 = traps (one of each type is chosen at random to be a real trap at
// battle start time)
// 50 = appears as improperly-z-buffered teal cube in preview, behaves as a
// blocked tile (like 10 and 20)
// 50 = blocked by metal box (appears as improperly-z-buffered teal cube in
// preview; behaves like 10 and 20 in game)
/* 1C68 */ parray<parray<uint8_t, 0x10>, 0x10> modification_tiles;
/* 1D68 */ parray<uint8_t, 0x74> unknown_a6;
/* 1DDC */ Rules default_rules;
@@ -770,7 +847,40 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
/* 59B0 */ parray<be_uint16_t, 0x10> reward_card_ids;
/* 59D0 */ parray<uint8_t, 0x0C> unknown_a9;
/* 59DC */ uint8_t unknown_a10;
/* 59DD */ parray<uint8_t, 0x3B> unknown_a11;
/* 59DD */ parray<uint8_t, 3> unknown_a11;
// This array specifies which SC characters can't participate in the quest
// (that is, the player is not allowed to choose decks with these SC cards).
// The values in this array don't match the SC card IDs, however:
// 0000 => Guykild (0005) 000C => Hyze (0117)
// 0001 => Kylria (0006) 000D => Rufina (0118)
// 0002 => Saligun (0110) 000E => Peko (0119)
// 0003 => Relmitos (0111) 000F => Creinu (011A)
// 0004 => Kranz (0002) 0010 => Reiz (011B)
// 0005 => Sil'fer (0004) 0011 => Lura (0007)
// 0006 => Ino'lis (0003) 0012 => Break (0008)
// 0007 => Viviana (0112) 0013 => Rio (011C)
// 0008 => Teifu (0113) 0014 => Endu (0116)
// 0009 => Orland (0001) 0015 => Memoru (011D)
// 000A => Stella (0114) 0016 => K.C. (011E)
// 000B => Glustar (0115) 0017 => Ohgun (011F)
// Unused entries in this array should be set to FFFF.
/* 59E0 */ parray<be_uint16_t, 0x18> unavailable_sc_cards;
struct EntryState {
// Values for player_type:
// 00 = Player (selectable by player, COM decks not allowed)
// 01 = Player/COM (selectable by player)
// 02 = COM (selectable by player, player decks not allowed)
// 03 = COM (not selectable by player; uses NPC deck)
// 04 = NONE (not selectable by player)
// FF = FREE (same as Player/COM, used in free battle mode)
uint8_t player_type;
// Values for deck_type:
// 00 = HERO ONLY
// 01 = DARK ONLY
// FF = any deck allowed
uint8_t deck_type;
} __attribute__((packed));
/* 5A10 */ parray<EntryState, 4> entry_states;
/* 5A18 */
std::string str(const DataIndex* data_index = nullptr) const;
@@ -778,6 +888,15 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
struct COMDeckDefinition {
size_t index;
std::string player_name;
std::string deck_name;
parray<le_uint16_t, 0x1F> card_ids;
};
class DataIndex {
public:
DataIndex(const std::string& directory, uint32_t behavior_flags);
@@ -791,9 +910,10 @@ public:
class MapEntry {
public:
MapDefinition map;
bool is_quest;
MapEntry(const MapDefinition& map);
MapEntry(const std::string& compressed_data);
MapEntry(const MapDefinition& map, bool is_quest);
MapEntry(const std::string& compressed_data, bool is_quest);
std::string compressed() const;
@@ -806,17 +926,26 @@ public:
std::shared_ptr<const CardEntry> definition_for_card_name(
const std::string& name) const;
std::set<uint32_t> all_card_ids() const;
uint64_t card_definitions_mtime() const;
const std::string& get_compressed_map_list() const;
std::shared_ptr<const MapEntry> definition_for_map_number(uint32_t id) const;
std::shared_ptr<const MapEntry> definition_for_map_name(
const std::string& name) const;
std::set<uint32_t> all_map_ids() const;
size_t num_com_decks() const;
std::shared_ptr<const COMDeckDefinition> com_deck(size_t which) const;
std::shared_ptr<const COMDeckDefinition> com_deck(const std::string& name) const;
std::shared_ptr<const COMDeckDefinition> random_com_deck() const;
const uint32_t behavior_flags;
private:
std::string compressed_card_definitions;
std::unordered_map<uint32_t, std::shared_ptr<CardEntry>> card_definitions;
std::unordered_map<std::string, std::shared_ptr<CardEntry>> card_definitions_by_name;
uint64_t mtime_for_card_definitions;
// The compressed map list is generated on demand from the maps map below.
// It's marked mutable because the logical consistency of the DataIndex object
@@ -824,6 +953,10 @@ private:
// compressed map list at load time.
mutable std::string compressed_map_list;
std::map<uint32_t, std::shared_ptr<MapEntry>> maps;
std::unordered_map<std::string, std::shared_ptr<MapEntry>> maps_by_name;
std::vector<std::shared_ptr<COMDeckDefinition>> com_decks;
std::unordered_map<std::string, std::shared_ptr<COMDeckDefinition>> com_decks_by_name;
};
+2 -2
View File
@@ -27,8 +27,8 @@ void DeckEntry::clear() {
this->team_id = 0xFFFFFFFF;
this->god_whim_flag = 3;
this->unused1 = 0;
this->unused2 = 0;
this->unused3 = 0;
this->player_level = 0;
this->unused2.clear(0);
this->card_ids.clear(0xFFFF);
}
+2 -2
View File
@@ -31,8 +31,8 @@ struct DeckEntry {
// always sets this to 3, and it's not clear why this even exists.
uint8_t god_whim_flag;
uint8_t unused1;
le_uint16_t unused2;
be_uint16_t unused3;
le_uint16_t player_level;
parray<uint8_t, 2> unused2;
DeckEntry();
void clear();
+3 -3
View File
@@ -43,8 +43,8 @@ PlayerState::PlayerState(uint8_t client_id, shared_ptr<Server> server)
void PlayerState::init() {
if (this->server()->player_states[this->client_id].get() != this) {
// TODO: The original code handles this, but we don't. Figure out if this is
// actually needed and implement it if so.
// Note: The original code handles this, but we don't. This appears not to
// ever happen, so we didn't bother implementing it.
throw logic_error("replacing a player state object is not permitted");
}
@@ -1444,7 +1444,7 @@ void PlayerState::set_random_assist_card_from_hand_for_free() {
}
void PlayerState::send_6xB4x04_if_needed(bool always_send) {
G_UpdateStats_GC_Ep3_6xB4x04 cmd;
G_UpdateShortStatuses_GC_Ep3_6xB4x04 cmd;
cmd.client_id = this->client_id;
// Note: The original code calls memset to clear all the short status structs
// at once. We don't do this because the default constructor has already
+199
View File
@@ -8,6 +8,24 @@ namespace Episode3 {
template <size_t Count>
std::string string_for_refs(const parray<le_uint16_t, Count>& card_refs) {
string ret = "[";
for (size_t z = 0; z < Count; z++) {
if (card_refs[z] != 0xFFFF) {
ret += string_printf("%zu:@$%04X ", z, card_refs[z].load());
}
}
if (!ret.empty()) {
ret.back() = ']'; // Replace the ' ' from the last added item
} else {
ret.push_back(']');
}
return ret;
}
Condition::Condition() {
this->clear();
}
@@ -63,6 +81,26 @@ void Condition::clear_FF() {
this->unknown_a8 = 0xFF;
}
std::string Condition::str() const {
return string_printf(
"Condition[type=%s, turns=%hhu, a_arg=%hhd, dice=%hhu, flags=%02hhX, "
"def_eff_index=%hhu, ref=@%04hX, value=%hd, giver_ref=@%04hX "
"percent=%hhu value8=%hd order=%hu a8=%hu]",
name_for_condition_type(this->type),
this->remaining_turns,
this->a_arg_value,
this->dice_roll_value,
this->flags,
this->card_definition_effect_index,
this->card_ref.load(),
this->value.load(),
this->condition_giver_card_ref.load(),
this->random_percent,
this->value8,
this->order,
this->unknown_a8);
}
EffectResult::EffectResult() {
@@ -82,6 +120,23 @@ void EffectResult::clear() {
this->dice_roll_value = 0;
}
std::string EffectResult::str() const {
return string_printf(
"EffectResult[att_ref=@%04hX, target_ref=@%04hX, value=%hhd, "
"cur_hp=%hhd, ap=%hhd, tp=%hhd, flags=%02hhX, op=%hhd, "
"cond_index=%hhu, dice=%hhu]",
this->attacker_card_ref.load(),
this->target_card_ref.load(),
this->value,
this->current_hp,
this->ap,
this->tp,
this->flags,
this->operation,
this->condition_index,
this->dice_roll_value);
}
CardShortStatus::CardShortStatus() {
@@ -101,6 +156,20 @@ bool CardShortStatus::operator!=(const CardShortStatus& other) const {
return !this->operator==(other);
}
std::string CardShortStatus::str() const {
string loc_s = this->loc.str();
return string_printf(
"CardShortStatus[ref=@%04hX, cur_hp=%hd, flags=%08" PRIX32 ", loc=%s, "
"u1=%04hX, max_hp=%hhd, u2=%hhu]",
this->card_ref.load(),
this->current_hp.load(),
this->card_flags.load(),
loc_s.c_str(),
this->unused1.load(),
this->max_hp,
this->unused2);
}
void CardShortStatus::clear() {
this->card_ref = 0xFFFF;
this->current_hp = 0;
@@ -138,6 +207,23 @@ void ActionState::clear() {
this->action_card_refs.clear(0xFFFF);
}
std::string ActionState::str() const {
string target_refs_s = string_for_refs(this->target_card_refs);
string action_refs_s = string_for_refs(this->action_card_refs);
return string_printf(
"ActionState[client=%hu, u=%hhu, facing=%s, attacker_ref=@%04hX, "
"def_ref=@%04hX, target_refs=%s, action_refs=%s, "
"orig_attacker_ref=@%04hX]",
this->client_id.load(),
this->unused,
name_for_direction(this->facing_direction),
this->attacker_card_ref.load(),
this->defense_card_ref.load(),
target_refs_s.c_str(),
action_refs_s.c_str(),
this->original_attacker_card_ref.load());
}
ActionChain::ActionChain() {
@@ -171,6 +257,40 @@ bool ActionChain::operator!=(const ActionChain& other) const {
return !this->operator==(other);
}
std::string ActionChain::str() const {
string attack_action_card_refs_s = string_for_refs(this->attack_action_card_refs);
string target_card_refs_s = string_for_refs(this->target_card_refs);
return string_printf(
"ActionChain[eff_ap=%hhd, eff_tp=%hhd, ap_bonus=%hhd, damage=%hhd, "
"acting_ref=@%04hX, unknown_ref_a3=@%04hX, "
"attack_action_refs=%s, attack_action_ref_count=%hhu, "
"medium=%s, target_ref_count=%hhu, subphase=%s, "
"strikes=%hhu, damage_mult=%hhd, attack_num=%hhu, "
"tp_bonus=%hhd, u1=%hhu, u2=%hhu, card_ap=%hhd, "
"card_tp=%hhd, flags=%08" PRIX32 ", target_refs=%s]",
this->effective_ap,
this->effective_tp,
this->ap_effect_bonus,
this->damage,
this->acting_card_ref.load(),
this->unknown_card_ref_a3.load(),
attack_action_card_refs_s.c_str(),
this->attack_action_card_ref_count,
name_for_attack_medium(this->attack_medium),
this->target_card_ref_count,
name_for_action_subphase(this->action_subphase),
this->strike_count,
this->damage_multiplier,
this->attack_number,
this->tp_effect_bonus,
this->unused1,
this->unused2,
this->card_ap,
this->card_tp,
this->flags.load(),
target_card_refs_s.c_str());
}
void ActionChain::clear() {
this->effective_ap = 0;
this->effective_tp = 0;
@@ -232,6 +352,23 @@ bool ActionChainWithConds::operator!=(const ActionChainWithConds& other) const {
return !this->operator==(other);
}
std::string ActionChainWithConds::str() const {
string ret = "ActionChainWithConds[chain=";
ret += this->chain.str();
ret += ", conds=[";
for (size_t z = 0; z < this->conditions.size(); z++) {
if (this->conditions[z].type != ConditionType::NONE) {
if (ret.back() != '=') {
ret += ", ";
}
ret += string_printf("%zu:", z);
ret += this->conditions[z].str();
}
}
ret += "]]";
return ret;
}
void ActionChainWithConds::clear() {
this->chain.effective_ap = 0;
this->chain.effective_tp = 0;
@@ -381,6 +518,28 @@ bool ActionMetadata::operator!=(const ActionMetadata& other) const {
return !this->operator==(other);
}
std::string ActionMetadata::str() const {
string target_card_refs_s = string_for_refs(this->target_card_refs);
string defense_card_refs_s = string_for_refs(this->defense_card_refs);
string original_attacker_card_refs_s = string_for_refs(this->original_attacker_card_refs);
return string_printf(
"ActionMetadata[ref=@%04hX, target_ref_count=%hhu, def_ref_count=%hhu, "
"subphase=%s, def_power=%hhd, def_bonus=%hhd, "
"att_bonus=%hhd, flags=%08" PRIX32 ", target_refs=%s, "
"defense_refs=%s, original_attacker_refs=%s]",
this->card_ref.load(),
this->target_card_ref_count,
this->defense_card_ref_count,
name_for_action_subphase(this->action_subphase),
this->defense_power,
this->defense_bonus,
this->attack_bonus,
this->flags.load(),
target_card_refs_s.c_str(),
defense_card_refs_s.c_str(),
original_attacker_card_refs_s.c_str());
}
void ActionMetadata::clear() {
this->card_ref = 0xFFFF;
this->target_card_ref_count = 0;
@@ -457,6 +616,46 @@ HandAndEquipState::HandAndEquipState() {
this->clear();
}
std::string HandAndEquipState::str() const {
string hand_card_refs_s = string_for_refs(this->hand_card_refs);
string set_card_refs_s = string_for_refs(this->set_card_refs);
string hand_card_refs2_s = string_for_refs(this->hand_card_refs2);
string set_card_refs2_s = string_for_refs(this->set_card_refs2);
return string_printf(
"HandAndEquipState[dice=[%hhu, %hhu], atk=%hhu, def=%hhu, atk2=%hhu, "
"a1=%hhu, total_set_cost=%hhu, is_cpu=%hhu, "
"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_turns=%hhu, assit_dely=%hhu, atk_bonus=%hhu, "
"def_bonus=%hhu, u2=[%hhu, %hhu]]",
this->dice_results[0],
this->dice_results[1],
this->atk_points,
this->def_points,
this->atk_points2,
this->unknown_a1,
this->total_set_cards_cost,
this->is_cpu_player,
this->assist_flags.load(),
hand_card_refs_s.c_str(),
this->assist_card_ref.load(),
set_card_refs_s.c_str(),
this->sc_card_ref.load(),
hand_card_refs2_s.c_str(),
set_card_refs2_s.c_str(),
this->assist_card_ref2.load(),
this->assist_card_set_number.load(),
this->assist_card_id.load(),
this->assist_remaining_turns,
this->assist_delay_turns,
this->atk_bonuses,
this->def_bonuses,
this->unused2[0],
this->unused2[1]);
}
void HandAndEquipState::clear() {
this->dice_results.clear(0);
this->atk_points = 0;
+16
View File
@@ -36,6 +36,8 @@ struct Condition {
void clear();
void clear_FF();
std::string str() const;
} __attribute__((packed));
struct EffectResult {
@@ -54,6 +56,8 @@ struct EffectResult {
bool operator==(const EffectResult& other) const;
bool operator!=(const EffectResult& other) const;
std::string str() const;
void clear();
} __attribute__((packed));
@@ -72,6 +76,8 @@ struct CardShortStatus {
void clear();
void clear_FF();
std::string str() const;
} __attribute__((packed));
struct ActionState {
@@ -89,6 +95,8 @@ struct ActionState {
bool operator!=(const ActionState& other) const;
void clear();
std::string str() const;
} __attribute__((packed));
struct ActionChain {
@@ -120,6 +128,8 @@ struct ActionChain {
void clear();
void clear_FF();
std::string str() const;
} __attribute__((packed));
struct ActionChainWithConds {
@@ -153,6 +163,8 @@ struct ActionChainWithConds {
void set_action_subphase_from_card(std::shared_ptr<const Card> card);
bool unknown_8024DEC4() const;
std::string str() const;
} __attribute__((packed));
struct ActionMetadata {
@@ -172,6 +184,8 @@ struct ActionMetadata {
bool operator==(const ActionMetadata& other) const;
bool operator!=(const ActionMetadata& other) const;
std::string str() const;
void clear();
void clear_FF();
@@ -218,6 +232,8 @@ struct HandAndEquipState {
void clear();
void clear_FF();
std::string str() const;
} __attribute__((packed));
struct PlayerStats {
+137 -61
View File
@@ -14,8 +14,7 @@ namespace Episode3 {
// These strings in the original implementation did not contain the semicolons
// (or anything after them).
static const char* VERSION_SIGNATURE =
"[V1][FINAL2.0] 03/09/13 15:30 by K.Toya; newserv Ep3 engine";
static const char* SIGNATURE_DATE = "Jan 21 2004 18:36:47; updated 2022";
"newserv Ep3 based on [V1][FINAL2.0] 03/09/13 15:30 by K.Toya";
@@ -34,10 +33,14 @@ void ServerBase::PresenceEntry::clear() {
ServerBase::ServerBase(
shared_ptr<Lobby> lobby,
shared_ptr<const DataIndex> data_index,
uint32_t random_seed)
uint32_t random_seed,
shared_ptr<const DataIndex::MapEntry> map_if_tournament)
: lobby(lobby),
data_index(data_index),
random_seed(random_seed) { }
log(lobby->log.prefix + "[Ep3::Server] "),
random_seed(random_seed),
is_tournament(!!map_if_tournament),
last_chosen_map(map_if_tournament) { }
void ServerBase::init() {
this->reset();
@@ -96,7 +99,7 @@ Server::Server(shared_ptr<ServerBase> base)
team_num_ally_fcs_destroyed(0),
team_num_cards_destroyed(0),
hard_reset_flag(false),
tournament_flag(0),
tournament_flag(base->is_tournament ? 1 : 0),
num_trap_tiles_of_type(0),
chosen_trap_tile_index_of_type(0),
has_done_pb(0),
@@ -120,10 +123,7 @@ void Server::init() {
this->ruler_server->link_objects(
this->base()->map_and_rules1, this->state_flags, this->assist_server);
G_ServerVersionStrings_GC_Ep3_6xB4x46 cmd;
cmd.version_signature = VERSION_SIGNATURE;
cmd.date_str1 = SIGNATURE_DATE;
this->send(cmd);
this->send_6xB4x46();
}
shared_ptr<ServerBase> Server::base() {
@@ -142,14 +142,50 @@ shared_ptr<const ServerBase> Server::base() const {
return s;
}
int8_t Server::get_winner_team_id() const {
// Note: This function is not part of the original implementation.
parray<size_t, 2> team_player_counts(0);
parray<size_t, 2> team_win_flag_counts(0);
for (size_t client_id = 0; client_id < 4; client_id++) {
auto ps = this->player_states[client_id];
if (!ps) {
continue;
}
uint8_t team_id = ps->get_team_id();
team_player_counts[team_id]++;
if (ps->assist_flags & 4) {
team_win_flag_counts[team_id]++;
}
}
if (!team_player_counts[0] || !team_player_counts[1]) {
throw logic_error("at least one team has no players");
}
if (team_win_flag_counts[0] && team_win_flag_counts[1]) {
throw logic_error("both teams have winning players");
}
for (int8_t z = 0; z < 2; z++) {
if (!team_win_flag_counts[z]) {
continue;
}
if (team_win_flag_counts[z] != team_player_counts[z]) {
throw logic_error("only some players on team 0 have won");
}
return z;
}
return -1; // No team has won (yet)
}
void Server::send(const void* data, size_t size) const {
// Note: This function is (obviously) not part of the original implementation.
auto l = this->base()->lobby.lock();
if (!l) {
throw runtime_error("lobby is deleted");
}
string masked_data;
if (this->base()->data_index->behavior_flags & BehaviorFlag::ENABLE_MASKING) {
if (!(this->base()->data_index->behavior_flags & BehaviorFlag::DISABLE_MASKING)) {
if (size >= 8) {
masked_data.assign(reinterpret_cast<const char*>(data), size);
uint8_t mask_key = (random_object<uint32_t>() % 0xFF) + 1;
@@ -159,9 +195,13 @@ void Server::send(const void* data, size_t size) const {
}
}
// Note: Sega's servers sent battle commands with the 60 command. The handlers
// for 60, 62, and C9 on the client are identical, so we choose to use C9
// instead because it's unique to Episode 3, and therefore seems more
// appropriate to convey battle commands.
send_command(l, 0xC9, 0x00, data, size);
for (auto watcher_l : l->watcher_lobbies) {
send_command(watcher_l, 0xC9, 0x00, data, size);
send_command_if_not_loading(watcher_l, 0xC9, 0x00, data, size);
}
if (l->battle_record && l->battle_record->writable()) {
l->battle_record->add_command(
@@ -169,6 +209,22 @@ void Server::send(const void* data, size_t size) const {
}
}
void Server::send_6xB4x46() const {
// Note: This function is not part of the original implementation; it was
// factored out from its callsites in this file and the strings were changed.
auto l = this->base()->lobby.lock();
if (!l) {
throw runtime_error("lobby is deleted");
}
G_ServerVersionStrings_GC_Ep3_6xB4x46 cmd46;
cmd46.version_signature = VERSION_SIGNATURE;
cmd46.date_str1 = format_time(this->base()->data_index->card_definitions_mtime() * 1000000);
cmd46.date_str2 = string_printf("Lobby/%08" PRIX32 " random %08" PRIX32,
l->lobby_id, l->random_seed);
this->send(cmd46);
}
string Server::prepare_6xB6x41_map_definition(
shared_ptr<const DataIndex::MapEntry> map) {
const auto& compressed = map->compressed();
@@ -191,19 +247,29 @@ void Server::send_commands_for_joining_spectator(Channel& c) const {
}
}
if (this->last_chosen_map) {
string data = this->prepare_6xB6x41_map_definition(this->last_chosen_map);
auto map = this->base()->last_chosen_map;
if (map) {
string data = this->prepare_6xB6x41_map_definition(map);
c.send(0x6C, 0x00, data);
}
if (should_send_state) {
// Note: Some servers send the commented-out commands here. Is there a
// situation where we should send them too?
c.send(0xC9, 0x00, this->prepare_6xB4x07_decks_update());
c.send(0xC9, 0x00, this->prepare_6xB4x1C_names_update());
// 6xB4x3B - unknown
G_Unknown_GC_Ep3_6xB4x3B cmd_3B;
c.send(0xC9, 0x00, &cmd_3B, sizeof(cmd_3B));
c.send(0xC9, 0x00, this->prepare_6xB4x50_trap_tile_locations());
// 6xB4x52 - unknown
}
}
__attribute__((format(printf, 2, 3)))
void Server::log_debug(const char* fmt, ...) const {
auto l = this->base()->lobby.lock();
if (l && (this->base()->data_index->behavior_flags & Episode3::BehaviorFlag::ENABLE_STATUS_MESSAGES)) {
va_list va;
va_start(va, fmt);
this->base()->log.info_v(fmt, va);
va_end(va);
}
}
@@ -235,10 +301,12 @@ void Server::send_info_message_printf(const char* fmt, ...) const {
void Server::send_debug_command_received_message(
uint8_t client_id, uint8_t subsubcommand, const char* description) const {
this->log_debug("%hhu/CAx%02hhX %s", client_id, subsubcommand, description);
this->send_debug_message_printf("$C5%hhu/CAx%02hhX %s", client_id, subsubcommand, description);
}
void Server::send_debug_command_received_message(uint8_t subsubcommand, const char* description) const {
this->log_debug("*/CAx%02hhX %s", subsubcommand, description);
this->send_debug_message_printf("$C5*/CAx%02hhX %s", subsubcommand, description);
}
@@ -418,7 +486,7 @@ bool Server::check_for_battle_end() {
}
} else { // Both teams defeated?? I guess this is technically possible
ret = true;
this->unknown_8023D4E0(0x4000);
this->compute_losing_team_id_and_add_winner_flags(0x4000);
}
} else { // Not DEFEAT_TEAM
@@ -449,7 +517,7 @@ bool Server::check_for_battle_end() {
}
} else {
ret = true;
this->unknown_8023D4E0(0x4000);
this->compute_losing_team_id_and_add_winner_flags(0x4000);
}
}
@@ -649,7 +717,7 @@ void Server::draw_phase_after() {
}
}
if (unknown_v1) {
this->unknown_8023D4E0(0);
this->compute_losing_team_id_and_add_winner_flags(0);
}
this->round_num--;
this->set_battle_ended();
@@ -1374,10 +1442,7 @@ void Server::setup_and_start_battle() {
this->battle_start_usecs = now();
G_ServerVersionStrings_GC_Ep3_6xB4x46 cmd46;
cmd46.version_signature = VERSION_SIGNATURE;
cmd46.date_str1 = SIGNATURE_DATE;
this->send(cmd46);
this->send_6xB4x46();
}
void Server::update_battle_state_flags_and_send_6xB4x03_if_needed(
@@ -1528,7 +1593,7 @@ void Server::handle_6xB3x0B_mulligan_hand(const string& data) {
}
G_ActionResult_GC_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.sequence_num.load();
out_cmd.sequence_num = in_cmd.header.sequence_num.load();
out_cmd.error_code = error_code;
this->send(out_cmd);
@@ -1551,7 +1616,7 @@ void Server::handle_6xB3x0C_end_mulligan_phase(const string& data) {
}
G_ActionResult_GC_Ep3_6xB4x1E out_cmd_ack;
out_cmd_ack.sequence_num = in_cmd.sequence_num;
out_cmd_ack.sequence_num = in_cmd.header.sequence_num;
out_cmd_ack.response_phase = 1;
this->send(out_cmd_ack);
@@ -1579,10 +1644,12 @@ void Server::handle_6xB3x0C_end_mulligan_phase(const string& data) {
}
G_ActionResult_GC_Ep3_6xB4x1E out_cmd_fin;
out_cmd_fin.sequence_num = in_cmd.sequence_num;
out_cmd_fin.sequence_num = in_cmd.header.sequence_num;
out_cmd_fin.response_phase = 2;
out_cmd_fin.error_code = error_code;
this->send(out_cmd_fin);
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, error_code);
}
void Server::handle_6xB3x0D_end_non_action_phase(const string& data) {
@@ -1591,14 +1658,14 @@ void Server::handle_6xB3x0D_end_non_action_phase(const string& data) {
in_cmd.client_id, in_cmd.header.subsubcommand, "END PHASE");
G_ActionResult_GC_Ep3_6xB4x1E out_cmd_ack;
out_cmd_ack.sequence_num = in_cmd.sequence_num;
out_cmd_ack.sequence_num = in_cmd.header.sequence_num;
out_cmd_ack.response_phase = 1;
this->send(out_cmd_ack);
this->set_client_id_ready_to_advance_phase(in_cmd.client_id);
G_ActionResult_GC_Ep3_6xB4x1E out_cmd_fin;
out_cmd_fin.sequence_num = in_cmd.sequence_num;
out_cmd_fin.sequence_num = in_cmd.header.sequence_num;
out_cmd_fin.response_phase = 2;
this->send(out_cmd_fin);
}
@@ -1632,7 +1699,7 @@ void Server::handle_6xB3x0E_discard_card_from_hand(const string& data) {
}
G_ActionResult_GC_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.sequence_num;
out_cmd.sequence_num = in_cmd.header.sequence_num;
out_cmd.error_code = error_code;
this->send(out_cmd);
@@ -1671,7 +1738,7 @@ void Server::handle_6xB3x0F_set_card_from_hand(const string& data) {
}
G_ActionResult_GC_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.sequence_num;
out_cmd.sequence_num = in_cmd.header.sequence_num;
out_cmd.error_code = this->ruler_server->error_code1;
this->send(out_cmd);
@@ -1706,7 +1773,7 @@ void Server::handle_6xB3x10_move_fc_to_location(const string& data) {
}
G_ActionResult_GC_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.sequence_num;
out_cmd.sequence_num = in_cmd.header.sequence_num;
out_cmd.error_code = this->ruler_server->error_code2;
this->send(out_cmd);
@@ -1739,7 +1806,7 @@ void Server::handle_6xB3x11_enqueue_attack_or_defense(const string& data) {
}
G_ActionResult_GC_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.sequence_num;
out_cmd.sequence_num = in_cmd.header.sequence_num;
out_cmd.error_code = this->ruler_server->error_code3;
this->send(out_cmd);
@@ -1760,8 +1827,10 @@ void Server::handle_6xB3x12_end_attack_list(const string& data) {
}
G_ActionResult_GC_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.sequence_num;
out_cmd.sequence_num = in_cmd.header.sequence_num;
this->send(out_cmd);
this->send_debug_message_if_error_code_nonzero(in_cmd.client_id, error_code);
}
void Server::handle_6xB3x13_update_map_during_setup(const string& data) {
@@ -1842,7 +1911,7 @@ void Server::handle_6xB3x14_update_deck_during_setup(const string& data) {
}
void Server::handle_6xB3x15_unused_hard_reset_server_state(const string& data) {
const auto& in_cmd = check_size_t<G_HardResetServerState_GC_Ep3_6xB3x15>(data);
const auto& in_cmd = check_size_t<G_HardResetServerState_GC_Ep3_6xB3x15_CAx15>(data);
this->send_debug_command_received_message(
in_cmd.header.subsubcommand, "HARD RESET");
this->hard_reset_flag = true;
@@ -1912,7 +1981,7 @@ void Server::handle_6xB3x28_end_defense_list(const string& data) {
in_cmd.client_id, in_cmd.header.subsubcommand, "END DEF LIST");
G_ActionResult_GC_Ep3_6xB4x1E out_cmd_ack;
out_cmd_ack.sequence_num = in_cmd.sequence_num;
out_cmd_ack.sequence_num = in_cmd.header.sequence_num;
out_cmd_ack.response_phase = 1;
this->send(out_cmd_ack);
@@ -1951,7 +2020,7 @@ void Server::handle_6xB3x28_end_defense_list(const string& data) {
}
G_ActionResult_GC_Ep3_6xB4x1E out_cmd_fin;
out_cmd_fin.sequence_num = in_cmd.sequence_num;
out_cmd_fin.sequence_num = in_cmd.header.sequence_num;
out_cmd_fin.response_phase = 2;
this->send(out_cmd_fin);
}
@@ -2036,7 +2105,7 @@ void Server::handle_6xB3x34_subtract_ally_atk_points(const string& data) {
}
void Server::handle_6xB3x37_client_ready_to_advance_from_starter_roll_phase(const string& data) {
const auto& in_cmd = check_size_t<G_AdvanceFromStartingRollsPhase_GC_Ep3_6xB3x37>(data);
const auto& in_cmd = check_size_t<G_AdvanceFromStartingRollsPhase_GC_Ep3_6xB3x37_CAx37>(data);
this->send_debug_command_received_message(
in_cmd.client_id, in_cmd.header.subsubcommand, "SETUP ADV 1");
@@ -2082,6 +2151,9 @@ void Server::handle_6xB3x40_map_list_request(const string& data) {
G_MapList_GC_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, list_data.size(), 0});
w.write(list_data);
send_command(l, 0x6C, 0x00, w.str());
for (auto watcher_l : l->watcher_lobbies) {
send_command_if_not_loading(watcher_l, 0x6C, 0x00, w.str());
}
if (l->battle_record && l->battle_record->writable()) {
l->battle_record->add_command(
@@ -2094,14 +2166,18 @@ void Server::handle_6xB3x41_map_request(const string& data) {
this->send_debug_command_received_message(
cmd.header.subsubcommand, "MAP DATA");
auto l = this->base()->lobby.lock();
auto base = this->base();
auto l = base->lobby.lock();
if (!l) {
throw runtime_error("lobby is deleted");
}
this->last_chosen_map = this->base()->data_index->definition_for_map_number(cmd.map_number);
auto out_cmd = this->prepare_6xB6x41_map_definition(this->last_chosen_map);
base->last_chosen_map = base->data_index->definition_for_map_number(cmd.map_number);
auto out_cmd = this->prepare_6xB6x41_map_definition(base->last_chosen_map);
send_command(l, 0x6C, 0x00, out_cmd);
for (auto watcher_l : l->watcher_lobbies) {
send_command_if_not_loading(watcher_l, 0x6C, 0x00, out_cmd);
}
if (l->battle_record && l->battle_record->writable()) {
l->battle_record->add_command(
@@ -2120,7 +2196,7 @@ void Server::handle_6xB3x48_end_turn(const string& data) {
}
G_ActionResult_GC_Ep3_6xB4x1E out_cmd;
out_cmd.sequence_num = in_cmd.sequence_num;
out_cmd.sequence_num = in_cmd.header.sequence_num;
this->send(out_cmd);
}
@@ -2136,7 +2212,7 @@ void Server::handle_6xB3x49_card_counts(const string& data) {
decrypt_trivial_gci_data(dest_counts.data(), dest_counts.bytes(), in_cmd.basis);
}
void Server::unknown_8023D4E0(uint32_t flags) {
void Server::compute_losing_team_id_and_add_winner_flags(uint32_t flags) {
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
if (ps) {
@@ -2146,8 +2222,8 @@ void Server::unknown_8023D4E0(uint32_t flags) {
uint32_t flags_to_add = flags | 0x804;
// First, check which team has fewer surviving SCs
int8_t team_id = -1;
// First, check which team has more dead SCs
int8_t losing_team_id = -1;
uint32_t team_counts[2] = {0, 0};
for (size_t z = 0; z < 4; z++) {
auto ps = this->player_states[z];
@@ -2160,13 +2236,13 @@ void Server::unknown_8023D4E0(uint32_t flags) {
}
}
if (team_counts[1] < team_counts[0]) {
team_id = 0;
losing_team_id = 0;
} else if (team_counts[0] < team_counts[1]) {
team_id = 1;
losing_team_id = 1;
}
// If the SC counts match, break ties by remaining SC HP
if (team_id == -1) {
if (losing_team_id == -1) {
team_counts[0] = 0;
team_counts[1] = 0;
for (size_t z = 0; z < 4; z++) {
@@ -2180,14 +2256,14 @@ void Server::unknown_8023D4E0(uint32_t flags) {
}
}
if (team_counts[0] < team_counts[1]) {
team_id = 0;
losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) {
team_id = 1;
losing_team_id = 1;
}
}
// If still tied, break ties by number of opponent cards destroyed
if (team_id == -1) {
if (losing_team_id == -1) {
team_counts[0] = 0;
team_counts[1] = 0;
for (size_t z = 0; z < 4; z++) {
@@ -2198,14 +2274,14 @@ void Server::unknown_8023D4E0(uint32_t flags) {
team_counts[ps->get_team_id()] += ps->stats.num_opponent_cards_destroyed;
}
if (team_counts[0] < team_counts[1]) {
team_id = 0;
losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) {
team_id = 1;
losing_team_id = 1;
}
}
// If still tied, break ties by amount of damage given
if (team_id == -1) {
if (losing_team_id == -1) {
team_counts[0] = 0;
team_counts[1] = 0;
for (size_t z = 0; z < 4; z++) {
@@ -2216,15 +2292,15 @@ void Server::unknown_8023D4E0(uint32_t flags) {
team_counts[ps->get_team_id()] += ps->stats.damage_given;
}
if (team_counts[0] < team_counts[1]) {
team_id = 0;
losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) {
team_id = 1;
losing_team_id = 1;
}
}
// If STILL tied, roll dice and arbitrarily make one team the winner
if (team_id == -1) {
while (team_id == -1) {
if (losing_team_id == -1) {
while (losing_team_id == -1) {
team_counts[1] = 0;
team_counts[0] = 0;
for (size_t z = 0; z < 4; z++) {
@@ -2237,9 +2313,9 @@ void Server::unknown_8023D4E0(uint32_t flags) {
team_counts[0] *= this->team_client_count[1];
team_counts[1] *= this->team_client_count[0];
if (team_counts[0] < team_counts[1]) {
team_id = 0;
losing_team_id = 0;
} else if (team_counts[1] < team_counts[0]) {
team_id = 1;
losing_team_id = 1;
}
}
flags_to_add = flags | 0x1004;
@@ -2250,7 +2326,7 @@ void Server::unknown_8023D4E0(uint32_t flags) {
if (!ps) {
continue;
}
if (team_id != ps->get_team_id()) {
if (losing_team_id != ps->get_team_id()) {
ps->assist_flags |= flags_to_add;
}
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
+17 -3
View File
@@ -27,6 +27,10 @@ namespace Episode3 {
* 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.
*
* 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.
@@ -58,7 +62,8 @@ public:
ServerBase(
std::shared_ptr<Lobby> lobby,
std::shared_ptr<const DataIndex> data_index,
uint32_t random_seed);
uint32_t random_seed,
std::shared_ptr<const DataIndex::MapEntry> map_if_tournament);
void init();
void reset();
void recreate_server();
@@ -73,7 +78,10 @@ public:
std::weak_ptr<Lobby> lobby;
std::shared_ptr<const DataIndex> data_index;
PrefixedLogger log;
uint32_t random_seed;
bool is_tournament;
std::shared_ptr<const DataIndex::MapEntry> last_chosen_map;
std::shared_ptr<MapAndRulesState> map_and_rules1;
std::shared_ptr<MapAndRulesState> map_and_rules2;
@@ -94,6 +102,8 @@ public:
std::shared_ptr<ServerBase> base();
std::shared_ptr<const ServerBase> base() const;
int8_t get_winner_team_id() const;
template <typename T>
void send(const T& cmd) const {
if (cmd.header.size != sizeof(cmd) / 4) {
@@ -112,6 +122,9 @@ public:
void send_commands_for_joining_spectator(Channel& ch) const;
__attribute__((format(printf, 2, 3)))
void log_debug(const char* fmt, ...) const;
__attribute__((format(printf, 2, 3)))
void send_debug_message_printf(const char* fmt, ...) const;
__attribute__((format(printf, 2, 3)))
@@ -123,6 +136,8 @@ public:
void send_debug_message_if_error_code_nonzero(
uint8_t client_id, int32_t error_code) const;
void send_6xB4x46() const;
void add_team_exp(uint8_t team_id, int32_t exp);
bool advance_battle_phase();
void action_phase_after();
@@ -200,7 +215,7 @@ public:
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 unknown_8023D4E0(uint32_t flags);
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(
uint16_t card_ref, int16_t negative_value);
@@ -231,7 +246,6 @@ private:
static const std::unordered_map<uint8_t, handler_t> subcommand_handlers;
std::weak_ptr<ServerBase> w_base;
std::shared_ptr<const DataIndex::MapEntry> last_chosen_map;
public:
uint32_t battle_finished;
+773
View File
@@ -0,0 +1,773 @@
#include "Tournament.hh"
#include <phosg/Random.hh>
#include "../CommandFormats.hh"
#include "../SendCommands.hh"
using namespace std;
namespace Episode3 {
Tournament::PlayerEntry::PlayerEntry(uint32_t serial_number)
: serial_number(serial_number), com_deck() { }
Tournament::PlayerEntry::PlayerEntry(
shared_ptr<const COMDeckDefinition> com_deck)
: serial_number(0), com_deck(com_deck) { }
bool Tournament::PlayerEntry::is_com() const {
return (this->com_deck != nullptr);
}
bool Tournament::PlayerEntry::is_human() const {
return (this->serial_number != 0);
}
Tournament::Team::Team(
shared_ptr<Tournament> tournament, size_t index, size_t max_players)
: tournament(tournament),
index(index),
max_players(max_players),
name(""),
password(""),
num_rounds_cleared(0),
is_active(true) { }
string Tournament::Team::str() const {
size_t num_human_players = 0;
size_t num_com_players = 0;
for (const auto& player : this->players) {
num_human_players += player.is_human();
num_com_players += player.is_com();
}
string ret = string_printf("[Team/%zu %s %zuH/%zuC/%zuP name=%s pass=%s rounds=%zu",
this->index, this->is_active ? "active" : "inactive",
num_human_players, num_com_players, this->max_players, this->name.c_str(),
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);
}
}
return ret + "]";
}
void Tournament::Team::register_player(
uint32_t serial_number,
const string& team_name,
const string& password) {
if (this->players.size() >= this->max_players) {
throw runtime_error("team is full");
}
if (!this->name.empty() && (password != this->password)) {
throw runtime_error("incorrect password");
}
auto tournament = this->tournament.lock();
if (!tournament) {
throw runtime_error("tournament has been deleted");
}
if (!tournament->all_player_serial_numbers.emplace(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)) {
throw logic_error("player already registered in team but not in tournament");
}
}
this->players.emplace_back(serial_number);
if (this->name.empty()) {
this->name = team_name;
this->password = password;
}
}
bool Tournament::Team::unregister_player(uint32_t serial_number) {
size_t index;
for (index = 0; index < this->players.size(); index++) {
if (this->players[index].is_human() &&
(this->players[index].serial_number == serial_number)) {
break;
}
}
if (index < this->players.size()) {
this->players.erase(this->players.begin() + index);
if (this->players.empty()) {
this->name.clear();
this->password.clear();
}
auto tournament = this->tournament.lock();
if (!tournament) {
return false;
}
// If the tournament has already started, make the team forfeit their game.
// If any player withdraws from a team after the registration phase, the
// entire team essentially forfeits their entry.
if (tournament->get_state() != Tournament::State::REGISTRATION) {
// Look through the pending matches to see if this team is involved in any
// of them
for (auto match : tournament->pending_matches) {
if (!match->preceding_a || !match->preceding_b) {
throw logic_error("zero-round match is pending after tournament registration phase");
}
if (match->preceding_a->winner_team.get() == this) {
match->set_winner_team(match->preceding_b->winner_team);
break;
} else if (match->preceding_b->winner_team.get() == this) {
match->set_winner_team(match->preceding_a->winner_team);
break;
}
}
// If the tournament has not started yet, just remove the player from the
// team
} else {
if (!tournament->all_player_serial_numbers.erase(serial_number)) {
throw logic_error("player removed from team but not from tournament");
}
}
return true;
} else {
return false;
}
}
bool Tournament::Team::has_any_human_players() const {
for (const auto& player : this->players) {
if (player.is_human()) {
return true;
}
}
return false;
}
size_t Tournament::Team::num_human_players() const {
size_t ret = 0;
for (const auto& player : this->players) {
ret += player.is_human();
}
return ret;
}
size_t Tournament::Team::num_com_players() const {
size_t ret = 0;
for (const auto& player : this->players) {
ret += player.is_com();
}
return ret;
}
Tournament::Match::Match(
shared_ptr<Tournament> tournament,
shared_ptr<Match> preceding_a,
shared_ptr<Match> preceding_b)
: tournament(tournament),
preceding_a(preceding_a),
preceding_b(preceding_b),
winner_team(nullptr),
round_num(0) {
if (this->preceding_a->round_num != this->preceding_b->round_num) {
throw logic_error("preceding matches have different round numbers");
}
this->round_num = this->preceding_a->round_num + 1;
}
Tournament::Match::Match(
shared_ptr<Tournament> tournament,
shared_ptr<Team> winner_team)
: tournament(tournament),
preceding_a(nullptr),
preceding_b(nullptr),
winner_team(winner_team),
round_num(0) { }
string Tournament::Match::str() const {
string winner_str = this->winner_team ? this->winner_team->str() : "(none)";
return string_printf("[Match round=%zu winner=%s]", this->round_num, winner_str.c_str());
}
bool Tournament::Match::resolve_if_no_human_players() {
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 {
return false;
}
}
void Tournament::Match::on_winner_team_set() {
auto tournament = this->tournament.lock();
if (!tournament) {
return;
}
tournament->pending_matches.erase(this->shared_from_this());
// 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()) {
tournament->pending_matches.emplace(following);
}
// If there are no pending matches, then the tournament is complete
if (tournament->pending_matches.empty()) {
tournament->current_state = Tournament::State::COMPLETE;
}
}
void Tournament::Match::set_winner_team_without_triggers(shared_ptr<Team> team) {
if (!this->preceding_a || !this->preceding_b) {
throw logic_error("set_winner_team called on zero-round match");
}
if ((team != this->preceding_a->winner_team) &&
(team != this->preceding_b->winner_team)) {
throw logic_error("winner team did not participate in match");
}
this->winner_team = team;
this->winner_team->num_rounds_cleared++;
if (this->winner_team == this->preceding_a->winner_team) {
this->preceding_b->winner_team->is_active = false;
} else {
this->preceding_a->winner_team->is_active = false;
}
}
void Tournament::Match::set_winner_team(shared_ptr<Team> team) {
this->set_winner_team_without_triggers(team);
this->on_winner_team_set();
}
shared_ptr<Tournament::Team> Tournament::Match::opponent_team_for_team(
shared_ptr<Team> team) const {
if (!this->preceding_a || !this->preceding_b) {
throw logic_error("zero-round matches do not have opponents");
}
if (team == this->preceding_a->winner_team) {
return this->preceding_b->winner_team;
} else if (team == this->preceding_b->winner_team) {
return this->preceding_a->winner_team;
} else {
throw logic_error("team is not registered for this match");
}
}
Tournament::Tournament(
shared_ptr<const DataIndex> data_index,
uint8_t number,
const string& name,
shared_ptr<const DataIndex::MapEntry> map,
const Rules& rules,
size_t num_teams,
bool is_2v2)
: log(string_printf("[Tournament/%02hhX] ", number)),
data_index(data_index),
number(number),
name(name),
map(map),
rules(rules),
num_teams(num_teams),
is_2v2(is_2v2),
current_state(State::REGISTRATION) {
if (this->num_teams < 4) {
throw invalid_argument("team count must be 4 or more");
}
if (this->num_teams > 32) {
throw invalid_argument("team count must be 32 or fewer");
}
if (this->num_teams & (this->num_teams - 1)) {
throw invalid_argument("team count must be a power of 2");
}
}
Tournament::Tournament(
std::shared_ptr<const DataIndex> data_index,
uint8_t number,
std::shared_ptr<const JSONObject> json)
: log(string_printf("[Tournament/%02hhX] ", number)),
data_index(data_index),
source_json(json),
number(number),
current_state(State::REGISTRATION) { }
void Tournament::init() {
vector<size_t> team_index_to_rounds_cleared;
bool is_registration_complete;
if (this->source_json) {
auto& dict = this->source_json->as_dict();
this->name = dict.at("name")->as_string();
this->map = this->data_index->definition_for_map_number(dict.at("map_number")->as_int());
this->rules = Rules(dict.at("rules"));
this->is_2v2 = dict.at("is_2v2")->as_bool();
is_registration_complete = dict.at("is_registration_complete")->as_bool();
for (const auto& team_json : dict.at("teams")->as_list()) {
auto& team_dict = team_json->as_dict();
auto& team = this->teams.emplace_back(new Team(
this->shared_from_this(),
this->teams.size(),
team_dict.at("max_players")->as_int()));
team->name = team_dict.at("name")->as_string();
team->password = team_dict.at("password")->as_string();
team_index_to_rounds_cleared.emplace_back(team_dict.at("num_rounds_cleared")->as_int());
for (const auto& player_json : team_dict.at("player_specs")->as_list()) {
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 {
team->players.emplace_back(this->data_index->com_deck(
player_json->as_string()));
}
}
}
this->num_teams = this->teams.size();
this->source_json.reset();
} else {
// 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->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(move(m));
}
current_round_matches = 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;
// Start with all first-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());
}
if (match_queue.count(nullptr)) {
throw logic_error("null match in match queue");
}
// For each match in the queue, either resolve it from the previous state or
// mark it as unresolvable (hence it should be pending when we're done)
while (!match_queue.empty()) {
auto match_it = match_queue.begin();
auto match = *match_it;
match_queue.erase(match_it);
if (!match->preceding_a->winner_team || !match->preceding_b->winner_team) {
throw logic_error("preceding matches are not resolved");
}
size_t& a_rounds_cleared = team_index_to_rounds_cleared[
match->preceding_a->winner_team->index];
size_t& b_rounds_cleared = team_index_to_rounds_cleared[
match->preceding_b->winner_team->index];
if (a_rounds_cleared && b_rounds_cleared) {
throw runtime_error("both teams won the same match");
}
if (!a_rounds_cleared && !b_rounds_cleared) {
this->pending_matches.emplace(match); // Neither team has won yet
} else {
if (a_rounds_cleared) {
a_rounds_cleared--;
match->set_winner_team_without_triggers(match->preceding_a->winner_team);
} else {
b_rounds_cleared--;
match->set_winner_team_without_triggers(match->preceding_b->winner_team);
}
// If both preceding matches of the following match are resolved, put
// the following match on the queue since it may be resolvable as well
auto following = match->following.lock();
if (following &&
following->preceding_a->winner_team &&
following->preceding_b->winner_team) {
match_queue.emplace(following);
}
}
}
if (!this->final_match->winner_team == this->pending_matches.empty()) {
throw logic_error("there must be pending matches if and only if the final match is not resolved");
}
// If all matches are resolved, then the tournament is complete
if (this->final_match->winner_team) {
this->current_state = State::COMPLETE;
}
} 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;
}
}
std::shared_ptr<JSONObject> Tournament::json() const {
unordered_map<string, shared_ptr<JSONObject>> dict;
dict.emplace("name", make_json_str(this->name));
dict.emplace("map_number", make_json_int(this->map->map.map_number));
dict.emplace("rules", this->rules.json());
dict.emplace("is_2v2", make_json_bool(this->is_2v2));
dict.emplace("is_registration_complete", make_json_bool(
this->current_state != State::REGISTRATION));
vector<shared_ptr<JSONObject>> teams_list;
for (auto team : this->teams) {
unordered_map<string, shared_ptr<JSONObject>> team_dict;
team_dict.emplace("max_players", make_json_int(team->max_players));
vector<shared_ptr<JSONObject>> player_jsons_list;
for (const auto& player : team->players) {
if (player.is_human()) {
player_jsons_list.emplace_back(make_json_int(player.serial_number));
} else {
player_jsons_list.emplace_back(make_json_str(player.com_deck->deck_name));
}
}
team_dict.emplace("player_specs", make_json_list(move(player_jsons_list)));
team_dict.emplace("name", make_json_str(team->name));
team_dict.emplace("password", make_json_str(team->password));
team_dict.emplace("num_rounds_cleared", make_json_int(team->num_rounds_cleared));
teams_list.emplace_back(new JSONObject(move(team_dict)));
}
dict.emplace("teams", make_json_list(move(teams_list)));
return shared_ptr<JSONObject>(new JSONObject(move(dict)));
}
std::shared_ptr<const DataIndex> Tournament::get_data_index() const {
return this->data_index;
}
uint8_t Tournament::get_number() const {
return this->number;
}
const string& Tournament::get_name() const {
return this->name;
}
shared_ptr<const DataIndex::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->winner_team) {
throw logic_error("tournament is complete but winner is not set");
}
return this->final_match->winner_team;
}
shared_ptr<Tournament::Match> Tournament::next_match_for_team(
shared_ptr<Team> team) const {
if (this->current_state == Tournament::State::REGISTRATION) {
return nullptr;
}
for (auto match : this->pending_matches) {
if (!match->preceding_a || !match->preceding_b) {
throw logic_error("zero-round match is pending after tournament registration phase");
}
if ((team == match->preceding_a->winner_team) ||
(team == match->preceding_b->winner_team)) {
return match;
}
}
return nullptr;
}
shared_ptr<Tournament::Match> Tournament::get_final_match() const {
return this->final_match;
}
shared_ptr<Tournament::Team> Tournament::team_for_serial_number(
uint32_t serial_number) const {
if (!this->all_player_serial_numbers.count(serial_number)) {
return nullptr;
}
for (auto team : this->teams) {
for (const auto& player : team->players) {
if (player.serial_number == serial_number) {
return team->is_active ? team : nullptr;
}
}
}
throw logic_error("serial number registered in tournament but not in any team");
}
const set<uint32_t>& Tournament::get_all_player_serial_numbers() const {
return this->all_player_serial_numbers;
}
void Tournament::start() {
if (this->current_state != State::REGISTRATION) {
throw runtime_error("tournament has already started");
}
this->current_state = State::IN_PROGRESS;
// Assign names to COM teams, and assign COM decks to all empty slots
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);
}
for (const auto& player : t->players) {
if (player.is_com()) {
throw logic_error("non-human player on team before tournament start");
}
}
if (this->data_index->num_com_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->data_index->random_com_deck());
}
}
// Resolve all possible CPU-only matches
for (auto m : this->zero_round_matches) {
m->on_winner_team_set();
}
}
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++) {
fputc(' ', stream);
fputc(' ', stream);
}
string match_str = m->str();
fprintf(stream, "%s%s\n", match_str.c_str(), this->pending_matches.count(m) ? " (PENDING)" : "");
if (m->preceding_a) {
print_match(m->preceding_a, indent_level + 1);
}
if (m->preceding_b) {
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());
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);
switch (this->current_state) {
case State::REGISTRATION:
fprintf(stream, " State: REGISTRATION\n");
break;
case State::IN_PROGRESS:
fprintf(stream, " State: IN_PROGRESS\n");
break;
case State::COMPLETE:
fprintf(stream, " State: COMPLETE\n");
break;
default:
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());
}
}
TournamentIndex::TournamentIndex(
shared_ptr<const DataIndex> data_index,
const string& state_filename,
bool skip_load_state)
: data_index(data_index), state_filename(state_filename) {
if (this->state_filename.empty() || skip_load_state) {
return;
}
auto json = JSONObject::parse(load_file(this->state_filename));
auto& list = json->as_list();
if (list.size() != 0x20) {
throw runtime_error("tournament JSON list length is incorrect");
}
for (size_t z = 0; z < 0x20; z++) {
if (!list.at(z)->is_null()) {
this->tournaments[z].reset(new Tournament(this->data_index, z, list[z]));
this->tournaments[z]->init();
}
}
}
void TournamentIndex::save() const {
if (this->state_filename.empty()) {
return;
}
vector<shared_ptr<JSONObject>> list;
for (size_t z = 0; z < 0x20; z++) {
if (this->tournaments[z]) {
list.emplace_back(this->tournaments[z]->json());
} else {
list.emplace_back(make_json_null());
}
}
auto json = make_json_list(move(list));
save_file(this->state_filename, json->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;
}
shared_ptr<Tournament> TournamentIndex::create_tournament(
const string& name,
shared_ptr<const DataIndex::MapEntry> 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");
}
auto t = make_shared<Tournament>(
this->data_index, number, name, map, rules, num_teams, is_2v2);
t->init();
this->tournaments[number] = t;
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];
}
}
return nullptr;
}
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);
if (team) {
return team;
}
}
return nullptr;
}
} // namespace Episode3
+195
View File
@@ -0,0 +1,195 @@
#pragma once
#include <stdint.h>
#include <event2/event.h>
#include <memory>
#include <vector>
#include <unordered_set>
#include <string>
#include <phosg/JSON.hh>
#include <phosg/Strings.hh>
#include "../Player.hh"
struct Lobby;
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 class State {
REGISTRATION = 0,
IN_PROGRESS,
COMPLETE,
};
struct PlayerEntry {
// Invariant: (serial_number == 0) != (com_deck == nullptr)
// (that is, exactly one of the following must be valid)
uint32_t serial_number;
std::shared_ptr<const COMDeckDefinition> com_deck;
explicit PlayerEntry(uint32_t serial_number);
explicit PlayerEntry(std::shared_ptr<const COMDeckDefinition> com_deck);
bool is_com() const;
bool is_human() const;
};
struct Team : public std::enable_shared_from_this<Team> {
std::weak_ptr<Tournament> tournament;
size_t index;
size_t max_players;
std::vector<PlayerEntry> players;
std::string name;
std::string password;
size_t num_rounds_cleared;
bool is_active;
Team(
std::shared_ptr<Tournament> tournament,
size_t index,
size_t max_players);
std::string str() const;
void register_player(
uint32_t serial_number,
const std::string& team_name,
const std::string& password);
bool unregister_player(uint32_t serial_number);
bool has_any_human_players() const;
size_t num_human_players() const;
size_t num_com_players() const;
};
struct Match : public std::enable_shared_from_this<Match> {
std::weak_ptr<Tournament> tournament;
std::shared_ptr<Match> preceding_a;
std::shared_ptr<Match> preceding_b;
std::weak_ptr<Match> following;
std::shared_ptr<Team> winner_team;
size_t round_num;
Match(
std::shared_ptr<Tournament> tournament,
std::shared_ptr<Match> preceding_a,
std::shared_ptr<Match> preceding_b);
Match(
std::shared_ptr<Tournament> tournament,
std::shared_ptr<Team> winner_team);
std::string str() const;
bool resolve_if_no_human_players();
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);
std::shared_ptr<Team> opponent_team_for_team(std::shared_ptr<Team> team) const;
};
Tournament(
std::shared_ptr<const DataIndex> data_index,
uint8_t number,
const std::string& name,
std::shared_ptr<const DataIndex::MapEntry> map,
const Rules& rules,
size_t num_teams,
bool is_2v2);
Tournament(
std::shared_ptr<const DataIndex> data_index,
uint8_t number,
std::shared_ptr<const JSONObject> json);
~Tournament() = default;
void init();
std::shared_ptr<JSONObject> json() const;
std::shared_ptr<const DataIndex> get_data_index() const;
uint8_t get_number() const;
const std::string& get_name() const;
std::shared_ptr<const DataIndex::MapEntry> get_map() const;
const Rules& get_rules() const;
bool get_is_2v2() const;
State get_state() const;
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;
std::shared_ptr<Team> team_for_serial_number(uint32_t serial_number) const;
const std::set<uint32_t>& get_all_player_serial_numbers() const;
void start();
void print_bracket(FILE* stream) const;
private:
PrefixedLogger log;
std::shared_ptr<const DataIndex> data_index;
std::shared_ptr<const JSONObject> source_json;
uint8_t number;
std::string name;
std::shared_ptr<const DataIndex::MapEntry> map;
Rules rules;
size_t num_teams;
bool is_2v2;
State current_state;
std::set<uint32_t> all_player_serial_numbers;
std::unordered_set<std::shared_ptr<Match>> pending_matches;
// This vector contains all teams in the original starting order of the
// tournament (that is, all teams in the first round). The order within this
// vector determines which team will play against which other team in the
// first round: [0] will play against [1], [2] will play against [3], etc.
std::vector<std::shared_ptr<Team>> teams;
// The tournament begins with a "zero round", in which each team automatically
// "wins" a match, putting them into the first round. This is just to make the
// data model easier to manage, so we don't have to have a type of match with
// no preceding round.
std::vector<std::shared_ptr<Match>> zero_round_matches;
std::shared_ptr<Match> final_match;
};
class TournamentIndex {
public:
explicit TournamentIndex(
std::shared_ptr<const DataIndex> data_index,
const std::string& state_filename,
bool skip_load_state = false);
~TournamentIndex() = default;
void save() const;
std::vector<std::shared_ptr<Tournament>> all_tournaments() const;
std::shared_ptr<Tournament> create_tournament(
const std::string& name,
std::shared_ptr<const DataIndex::MapEntry> 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;
std::shared_ptr<Tournament::Team> team_for_serial_number(
uint32_t serial_number) const;
private:
std::shared_ptr<const DataIndex> data_index;
std::string state_filename;
std::shared_ptr<Tournament> tournaments[0x20];
};
} // namespace Episode3
+1 -1
View File
@@ -15,7 +15,7 @@ FileContentsCache::File::File(
const string& name,
string&& data,
uint64_t load_time)
: name(name), data(move(data)), load_time(load_time) { }
: name(name), data(new string(move(data))), load_time(load_time) { }
shared_ptr<const FileContentsCache::File> FileContentsCache::replace(
const string& name, string&& data, uint64_t t) {
+8 -8
View File
@@ -14,7 +14,7 @@ class FileContentsCache {
public:
struct File {
std::string name;
std::string data;
shared_ptr<const std::string> data;
uint64_t load_time;
File() = delete;
@@ -68,29 +68,29 @@ public:
template <typename T, typename NameT>
GetObjResult<T> get_obj_or_load(NameT name) {
auto res = this->get_or_load(name);
if (res.file->data.size() != sizeof(T)) {
if (res.file->data->size() != sizeof(T)) {
throw runtime_error("cached string size is incorrect");
}
return {*reinterpret_cast<const T*>(res.file->data.data()), res.file, res.generate_called};
return {*reinterpret_cast<const T*>(res.file->data->data()), res.file, res.generate_called};
}
template <typename T, typename NameT>
GetObjResult<T> get_obj_or_throw(NameT name) {
auto res = this->get_or_throw(name);
if (res.file->data.size() != sizeof(T)) {
if (res.file->data->size() != sizeof(T)) {
throw runtime_error("cached string size is incorrect");
}
return {*reinterpret_cast<const T*>(res.file->data.data()), res.file, res.generate_called};
return {*reinterpret_cast<const T*>(res.file->data->data()), res.file, res.generate_called};
}
template <typename T, typename NameT>
GetObjResult<T> get_obj(NameT name, std::function<T(const std::string&)> generate) {
uint64_t t = now();
try {
auto& f = this->name_to_file.at(name);
if (f->data.size() != sizeof(T)) {
if (f->data->size() != sizeof(T)) {
throw 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};
return {*reinterpret_cast<const T*>(f->data->data()), f, false};
}
} catch (const out_of_range& e) { }
T value = generate(name);
@@ -101,7 +101,7 @@ public:
template <typename T, typename NameT>
GetObjResult<T> replace_obj(NameT name, const T& value) {
auto cached_value = this->replace(name, &value, sizeof(value));
return {*reinterpret_cast<const T*>(cached_value->data.data()), cached_value, false};
return {*reinterpret_cast<const T*>(cached_value->data->data()), cached_value, false};
}
private:
+7 -2
View File
@@ -118,6 +118,7 @@ shared_ptr<CompiledFunctionCode> compile_function_code(
ret->arch = arch;
ret->name = name;
ret->index = 0;
ret->hide_from_patches_menu = false;
if (arch == CompiledFunctionCode::Architecture::POWERPC) {
auto assembled = PPC32Emulator::assemble(text, {directory});
@@ -133,6 +134,8 @@ shared_ptr<CompiledFunctionCode> compile_function_code(
reloc_indexes.emplace(it.second / 4);
} else if (starts_with(it.first, "newserv_index_")) {
ret->index = stoul(it.first.substr(14), nullptr, 16);
} else if (it.first == "hide_from_patches_menu") {
ret->hide_from_patches_menu = true;
}
}
@@ -206,8 +209,10 @@ vector<MenuItem> FunctionCodeIndex::patch_menu() const {
ret.emplace_back(PatchesMenuItemID::GO_BACK, u"Go back", u"", 0);
for (const auto& it : this->name_to_patch_function) {
const auto& fn = it.second;
ret.emplace_back(fn->menu_item_id, decode_sjis(fn->name), u"",
MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL);
if (!fn->hide_from_patches_menu) {
ret.emplace_back(fn->menu_item_id, decode_sjis(fn->name), u"",
MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL);
}
}
return ret;
}
+1
View File
@@ -33,6 +33,7 @@ struct CompiledFunctionCode {
std::string name;
uint32_t index; // 0 = unused (not registered in index_to_function)
uint32_t menu_item_id;
bool hide_from_patches_menu;
bool is_big_endian() const;
+16 -7
View File
@@ -10,20 +10,20 @@ using namespace std;
// TODO: Support big-endian GSLs also (e.g. from PSO GC)
template <typename LongT>
struct GSLHeaderEntry {
ptext<char, 0x20> filename;
le_uint32_t offset; // In pages, so actual offset is this * 0x800
le_uint32_t size;
LongT offset; // In pages, so actual offset is this * 0x800
LongT size;
uint64_t unused;
};
} __attribute__((packed));
GSLArchive::GSLArchive(shared_ptr<const string> data) : data(data) {
template <typename LongT>
void GSLArchive::load_t() {
StringReader r(*this->data);
uint64_t min_data_offset = 0xFFFFFFFFFFFFFFFF;
while (r.where() < min_data_offset) {
const auto& entry = r.get<GSLHeaderEntry>();
const auto& entry = r.get<GSLHeaderEntry<LongT>>();
if (entry.filename.len() == 0) {
break;
}
@@ -35,6 +35,15 @@ GSLArchive::GSLArchive(shared_ptr<const string> data) : data(data) {
}
}
GSLArchive::GSLArchive(shared_ptr<const string> data, bool big_endian)
: data(data) {
if (big_endian) {
this->load_t<be_uint32_t>();
} else {
this->load_t<le_uint32_t>();
}
}
const unordered_map<string, GSLArchive::Entry> GSLArchive::all_entries() const {
return this->entries;
}
+4 -1
View File
@@ -12,7 +12,7 @@
class GSLArchive {
public:
GSLArchive(std::shared_ptr<const std::string> data);
GSLArchive(std::shared_ptr<const std::string> data, bool big_endian);
~GSLArchive() = default;
struct Entry {
@@ -26,6 +26,9 @@ public:
StringReader get_reader(const std::string& name) const;
private:
template <typename LongT>
void load_t();
std::shared_ptr<const std::string> data;
std::unordered_map<std::string, Entry> entries;
+54 -66
View File
@@ -147,7 +147,18 @@ static void flush_and_free_bufferevent(struct bufferevent* bev) {
IPStackSimulator::IPClient::TCPConnection::TCPConnection()
: server_bev(nullptr, flush_and_free_bufferevent),
pending_data(evbuffer_new(), evbuffer_free),
resend_push_event(nullptr, event_free) { }
resend_push_event(nullptr, event_free),
awaiting_first_ack(true),
server_addr(0),
server_port(0),
client_port(0),
next_client_seq(0),
acked_server_seq(0),
resend_push_usecs(DEFAULT_RESEND_PUSH_USECS),
next_push_max_frame_size(1024),
max_frame_size(1024),
bytes_received(0),
bytes_sent(0) { }
@@ -221,8 +232,7 @@ void IPStackSimulator::on_client_input(struct bufferevent* bev) {
try {
this->on_client_frame(c, frame);
} catch (const exception& e) {
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.warning("Failed to process frame: %s", e.what());
if (ip_stack_simulator_log.warning("Failed to process frame: %s", e.what())) {
print_data(stderr, frame);
}
}
@@ -250,15 +260,14 @@ void IPStackSimulator::on_client_error(struct bufferevent* bev,
void IPStackSimulator::on_client_frame(
shared_ptr<IPClient> c, const string& frame) {
if (this->state->ip_stack_debug) {
fputc('\n', stderr);
ip_stack_simulator_log.info("Virtual network sent frame");
if (ip_stack_simulator_log.info("Virtual network sent frame")) {
print_data(stderr, frame);
fputc('\n', stderr);
}
this->log_frame(frame);
FrameInfo fi(frame);
if (this->state->ip_stack_debug) {
if (ip_stack_simulator_log.should_log(LogLevel::INFO)) {
string fi_header = fi.header_str();
ip_stack_simulator_log.info("Frame header: %s", fi_header.c_str());
}
@@ -368,9 +377,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));
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Sending ARP response");
}
ip_stack_simulator_log.info("Sending ARP response");
if (this->pcap_text_log_file) {
StringWriter w;
@@ -429,7 +436,7 @@ void IPStackSimulator::on_client_udp_frame(
struct evbuffer* out_buf = bufferevent_get_output(c->bev.get());
if (this->state->ip_stack_debug) {
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);
@@ -478,10 +485,8 @@ uint64_t IPStackSimulator::tcp_conn_key_for_client_frame(const FrameInfo& fi) {
void IPStackSimulator::on_client_tcp_frame(
shared_ptr<IPClient> c, const FrameInfo& fi) {
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Virtual network sent TCP frame (seq=%08" PRIX32 ", ack=%08" PRIX32 ")",
fi.tcp->seq_num.load(), fi.tcp->ack_num.load());
}
ip_stack_simulator_log.info("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)) {
@@ -561,13 +566,8 @@ void IPStackSimulator::on_client_tcp_frame(
conn.bytes_sent = 0;
conn_str = this->str_for_tcp_connection(c, conn);
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Client opened TCP connection %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")",
conn_str.c_str(), conn.acked_server_seq, conn.next_client_seq);
} else {
ip_stack_simulator_log.info("Client opened TCP connection %s",
conn_str.c_str());
}
ip_stack_simulator_log.info("Client opened TCP connection %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")",
conn_str.c_str(), conn.acked_server_seq, conn.next_client_seq);
} else {
// Connection is NOT new; this is probably a resend of an earlier SYN
@@ -577,18 +577,14 @@ void IPStackSimulator::on_client_tcp_frame(
// TODO: We should check the syn/ack numbers here instead of just assuming
// they're correct
conn_str = this->str_for_tcp_connection(c, conn);
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Client resent SYN for TCP connection %s",
conn_str.c_str());
}
ip_stack_simulator_log.info("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);
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Sent SYN+ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")",
conn_str.c_str(), conn.acked_server_seq, conn.next_client_seq);
}
ip_stack_simulator_log.info("Sent SYN+ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")",
conn_str.c_str(), conn.acked_server_seq, conn.next_client_seq);
} else {
// This frame isn't a SYN, so a connection object should already exist
@@ -602,9 +598,7 @@ void IPStackSimulator::on_client_tcp_frame(
bool conn_valid = true;
if (fi.tcp->flags & TCPHeader::Flag::ACK) {
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Client sent ACK %08" PRIX32, fi.tcp->ack_num.load());
}
ip_stack_simulator_log.info("Client sent ACK %08" PRIX32, fi.tcp->ack_num.load());
if (conn->awaiting_first_ack) {
if (fi.tcp->ack_num != conn->acked_server_seq + 1) {
throw runtime_error("first ack_num was not acked_server_seq + 1");
@@ -614,9 +608,7 @@ void IPStackSimulator::on_client_tcp_frame(
} else {
if (seq_num_greater(fi.tcp->ack_num, conn->acked_server_seq)) {
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Advancing acked_server_seq from %08" PRIX32, conn->acked_server_seq);
}
ip_stack_simulator_log.info("Advancing acked_server_seq from %08" PRIX32, conn->acked_server_seq);
uint32_t ack_delta = fi.tcp->ack_num - conn->acked_server_seq;
size_t pending_bytes = evbuffer_get_length(conn->pending_data.get());
if (pending_bytes < ack_delta) {
@@ -628,10 +620,8 @@ void IPStackSimulator::on_client_tcp_frame(
conn->resend_push_usecs = DEFAULT_RESEND_PUSH_USECS;
conn->next_push_max_frame_size = conn->max_frame_size;
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Removed %08" PRIX32 " bytes from pending buffer and advanced acked_server_seq to %08" PRIX32,
ack_delta, conn->acked_server_seq);
}
ip_stack_simulator_log.info("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)) {
throw runtime_error("client sent lower ack num than previous frame");
@@ -670,7 +660,8 @@ void IPStackSimulator::on_client_tcp_frame(
// server immediately.
} else if (fi.payload_size != 0) {
string conn_str = this->state->ip_stack_debug ? this->str_for_tcp_connection(c, *conn) : "";
string conn_str = ip_stack_simulator_log.should_log(LogLevel::WARNING)
? this->str_for_tcp_connection(c, *conn) : "";
size_t payload_skip_bytes;
if (fi.tcp->seq_num == conn->next_client_seq) {
@@ -690,11 +681,9 @@ void IPStackSimulator::on_client_tcp_frame(
// Payload is in the future - we must have missed a data frame. We'll
// ignore it (but warn) and send an ACK later, and the client should
// retransmit the lost data
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.warning(
"Client sent out-of-order sequence number (expected %08" PRIX32 ", received %08" PRIX32 ", 0x%zX data bytes)",
conn->next_client_seq, fi.tcp->seq_num.load(), fi.payload_size);
}
ip_stack_simulator_log.warning(
"Client sent out-of-order sequence number (expected %08" PRIX32 ", received %08" PRIX32 ", 0x%zX data bytes)",
conn->next_client_seq, fi.tcp->seq_num.load(), fi.payload_size);
payload_skip_bytes = fi.payload_size;
}
@@ -706,14 +695,15 @@ void IPStackSimulator::on_client_tcp_frame(
const void* payload = reinterpret_cast<const uint8_t*>(fi.payload) + payload_skip_bytes;
size_t payload_size = fi.payload_size - payload_skip_bytes;
if (this->state->ip_stack_debug) {
if (payload_skip_bytes) {
ip_stack_simulator_log.info("Client sent data on TCP connection %s, overlapping existing ack'ed data (0x%zX bytes ignored)",
conn_str.c_str(), payload_skip_bytes);
} else {
ip_stack_simulator_log.info("Client sent data on TCP connection %s",
conn_str.c_str());
}
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)",
conn_str.c_str(), payload_skip_bytes);
} else {
was_logged = ip_stack_simulator_log.info("Client sent data on TCP connection %s",
conn_str.c_str());
}
if (was_logged) {
print_data(stderr, payload, payload_size);
}
@@ -725,14 +715,16 @@ void IPStackSimulator::on_client_tcp_frame(
// Update the sequence number and stats
conn->next_client_seq += payload_size;
conn->bytes_received += payload_size;
if (conn->next_client_seq < payload_size) {
ip_stack_simulator_log.warning("Client sequence number has wrapped (next=%08" PRIX32 ", bytes=%zX)",
fi.tcp->seq_num.load(), payload_size);
}
}
// Send an ACK
this->send_tcp_frame(c, *conn);
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Sent PSH ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ", bytes_received=0x%zX)",
conn_str.c_str(), conn->acked_server_seq, conn->next_client_seq, conn->bytes_received);
}
ip_stack_simulator_log.info("Sent PSH ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ", bytes_received=0x%zX)",
conn_str.c_str(), conn->acked_server_seq, conn->next_client_seq, conn->bytes_received);
}
if (conn_valid) {
@@ -801,10 +793,8 @@ void IPStackSimulator::send_pending_push_frame(
size_t bytes_to_send = min<size_t>(pending_bytes, conn.next_push_max_frame_size);
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Sending PSH frame with seq_num %08" PRIX32 ", 0x%zX/0x%zX data bytes",
conn.acked_server_seq, bytes_to_send, pending_bytes);
}
ip_stack_simulator_log.info("Sending PSH frame with seq_num %08" PRIX32 ", 0x%zX/0x%zX data bytes",
conn.acked_server_seq, bytes_to_send, pending_bytes);
this->send_tcp_frame(c, conn, TCPHeader::Flag::PSH, conn.pending_data.get(),
bytes_to_send);
@@ -915,10 +905,8 @@ void IPStackSimulator::dispatch_on_server_input(struct bufferevent*, void* ctx)
void IPStackSimulator::on_server_input(shared_ptr<IPClient> c, IPClient::TCPConnection& conn) {
struct evbuffer* buf = bufferevent_get_input(conn.server_bev.get());
if (this->state->ip_stack_debug) {
ip_stack_simulator_log.info("Server input event: 0x%zX bytes to read",
evbuffer_get_length(buf));
}
ip_stack_simulator_log.info("Server input event: 0x%zX bytes to read",
evbuffer_get_length(buf));
evbuffer_add_buffer(conn.pending_data.get(), buf);
this->send_pending_push_frame(c, conn);
+8 -4
View File
@@ -172,9 +172,11 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
}
}
// TODO: It appears that on DC, using an item should NOT delete it from the
// player's inventory - the client will send a followup 6x29 to do that.
bool should_delete_item = (c->version() != GameVersion::DC);
// 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.
bool should_delete_item = (c->version() != GameVersion::DC) &&
(c->version() != GameVersion::PC);
auto& item = c->game_data.player()->inventory.items[item_index];
if (item.data.data1w[0] == 0x0203) { // technique disk
@@ -229,7 +231,9 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
}
if (should_delete_item) {
c->game_data.player()->remove_item(item.data.id, 1);
// Allow overdrafting meseta if the client is not BB, since the server isn't
// informed when meseta is added or removed from the bank.
c->game_data.player()->remove_item(item.data.id, 1, c->version() != GameVersion::BB);
}
}
+10 -3
View File
@@ -163,11 +163,18 @@ void LicenseManager::ban_until(uint32_t serial_number, uint64_t end_time) {
}
}
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) {
uint32_t serial_number = l->serial_number;
this->serial_number_to_license.emplace(serial_number, l);
this->serial_number_to_license[l->serial_number] = l;
if (!l->username.empty()) {
this->bb_username_to_license.emplace(l->username, l);
this->bb_username_to_license[l->username] = l;
}
if (this->autosave) {
this->save();
+1
View File
@@ -82,6 +82,7 @@ public:
size_t count() 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;
+67 -17
View File
@@ -33,7 +33,6 @@ Lobby::Lobby(uint32_t id)
for (size_t x = 0; x < 12; x++) {
this->next_item_id[x] = 0x00010000 + 0x00200000 * x;
}
this->next_drop_item = PlayerInventoryItem();
}
void Lobby::reassign_leader_on_client_departure(size_t leaving_client_index) {
@@ -71,10 +70,18 @@ size_t Lobby::count_clients() const {
return ret;
}
void Lobby::add_client(shared_ptr<Client> c) {
void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
ssize_t index;
ssize_t min_client_id = (this->flags & Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0;
if (c->prefer_high_lobby_client_id) {
if (required_client_id >= 0) {
if (this->clients[required_client_id].get()) {
throw out_of_range("required slot is in use");
}
this->clients[required_client_id] = c;
index = required_client_id;
} else if (c->options.prefer_high_lobby_client_id) {
for (index = max_clients - 1; index >= min_client_id; index--) {
if (!this->clients[index].get()) {
this->clients[index] = c;
@@ -100,16 +107,15 @@ void Lobby::add_client(shared_ptr<Client> c) {
c->lobby_id = this->lobby_id;
// If there's no one else in the lobby, set the leader id as well
if (index == (max_clients - 1) * c->prefer_high_lobby_client_id) {
for (index = 0; index < max_clients; index++) {
if (this->clients[index].get() && this->clients[index] != c) {
break;
}
}
if (index >= max_clients) {
this->leader_id = c->lobby_client_id;
size_t leader_index;
for (leader_index = 0; leader_index < max_clients; leader_index++) {
if (this->clients[leader_index] && (this->clients[leader_index] != c)) {
break;
}
}
if (leader_index >= max_clients) {
this->leader_id = c->lobby_client_id;
}
// If the lobby is a game and item tracking is enabled, assign the inventory's
// item IDs
@@ -132,6 +138,18 @@ void Lobby::add_client(shared_ptr<Client> c) {
c->game_data.player()->inventory,
c->game_data.player()->disp.to_dcpcv3());
}
// Send spectator count notifications if needed
if (this->is_game() && (this->flags & Lobby::Flag::EPISODE_3_ONLY)) {
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);
}
} else {
send_ep3_update_spectator_count(this->shared_from_this());
}
}
}
void Lobby::remove_client(shared_ptr<Client> c) {
@@ -154,24 +172,46 @@ void Lobby::remove_client(shared_ptr<Client> c) {
this->reassign_leader_on_client_departure(c->lobby_client_id);
// If the lobby ios recording a battle record, add the player leave event
// If the lobby is recording a battle record, add the player leave event
if (this->battle_record) {
this->battle_record->delete_player(c->lobby_client_id);
}
// If the lobby is Episode 3, update the appropriate spectator counts
if (this->is_game() && (this->flags & Lobby::Flag::EPISODE_3_ONLY)) {
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);
}
} else {
send_ep3_update_spectator_count(this->shared_from_this());
}
}
}
void Lobby::move_client_to_lobby(shared_ptr<Lobby> dest_lobby,
shared_ptr<Client> c) {
void Lobby::move_client_to_lobby(
shared_ptr<Lobby> dest_lobby,
shared_ptr<Client> c,
ssize_t required_client_id) {
if (dest_lobby.get() == this) {
return;
}
if (dest_lobby->count_clients() >= dest_lobby->max_clients) {
throw out_of_range("no space left in lobby");
if (required_client_id >= 0) {
if (dest_lobby->clients[required_client_id]) {
throw out_of_range("required slot is in use");
}
} else {
ssize_t min_client_id = (this->flags & Lobby::Flag::IS_SPECTATOR_TEAM) ? 4 : 0;
size_t available_slots = dest_lobby->max_clients - min_client_id;
if (dest_lobby->count_clients() >= available_slots) {
throw out_of_range("no space left in lobby");
}
}
this->remove_client(c);
dest_lobby->add_client(c);
dest_lobby->add_client(c, required_client_id);
}
@@ -235,3 +275,13 @@ uint32_t Lobby::generate_item_id(uint8_t client_id) {
}
return this->next_game_item_id++;
}
unordered_map<uint32_t, shared_ptr<Client>> Lobby::clients_by_serial_number() const {
unordered_map<uint32_t, shared_ptr<Client>> ret;
for (auto c : this->clients) {
if (c) {
ret.emplace(c->license->serial_number, c);
}
}
return ret;
}
+28 -21
View File
@@ -20,28 +20,29 @@
#include "Episode3/BattleRecord.hh"
#include "Episode3/Server.hh"
struct Lobby {
struct Lobby : public std::enable_shared_from_this<Lobby> {
enum Flag {
GAME = 0x00000001,
EPISODE_3_ONLY = 0x00000002,
NON_V1_ONLY = 0x00000004, // DC NTE and DCv1 not allowed
PERSISTENT = 0x00000008,
GAME = 0x00000001,
EPISODE_3_ONLY = 0x00000002,
NON_V1_ONLY = 0x00000004, // DC NTE and DCv1 not allowed
PERSISTENT = 0x00000008,
// Flags used only for games
CHEATS_ENABLED = 0x00000100,
QUEST_IN_PROGRESS = 0x00000200,
BATTLE_IN_PROGRESS = 0x00000400,
JOINABLE_QUEST_IN_PROGRESS = 0x00000800,
ITEM_TRACKING_ENABLED = 0x00001000,
IS_SPECTATOR_TEAM = 0x00002000, // EPISODE_3_ONLY must also be set
SPECTATORS_FORBIDDEN = 0x00004000,
BATTLE_MODE = 0x00008000,
CHALLENGE_MODE = 0x00010000,
SOLO_MODE = 0x00020000,
CHEATS_ENABLED = 0x00000100,
QUEST_IN_PROGRESS = 0x00000200,
BATTLE_IN_PROGRESS = 0x00000400,
JOINABLE_QUEST_IN_PROGRESS = 0x00000800,
ITEM_TRACKING_ENABLED = 0x00001000,
IS_SPECTATOR_TEAM = 0x00002000, // EPISODE_3_ONLY must also be set
SPECTATORS_FORBIDDEN = 0x00004000,
BATTLE_MODE = 0x00008000,
CHALLENGE_MODE = 0x00010000,
SOLO_MODE = 0x00020000,
START_BATTLE_PLAYER_IMMEDIATELY = 0x00040000,
// Flags used only for lobbies
PUBLIC = 0x01000000,
DEFAULT = 0x02000000,
PUBLIC = 0x01000000,
DEFAULT = 0x02000000,
};
PrefixedLogger log;
@@ -61,7 +62,6 @@ struct Lobby {
std::vector<PSOEnemy> enemies;
std::array<uint32_t, 12> next_item_id;
uint32_t next_game_item_id;
PlayerInventoryItem next_drop_item;
std::unordered_map<uint32_t, FloorItem> item_id_to_floor_item;
parray<le_uint32_t, 0x20> variations;
@@ -94,6 +94,7 @@ struct Lobby {
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;
// Lobby stuff
uint8_t event;
@@ -104,6 +105,8 @@ struct Lobby {
uint32_t flags;
std::shared_ptr<const Quest> loading_quest;
std::array<std::shared_ptr<Client>, 12> clients;
// Keys in this map are client_id
std::unordered_map<size_t, std::weak_ptr<Client>> clients_to_add;
explicit Lobby(uint32_t id);
@@ -115,11 +118,13 @@ struct Lobby {
size_t count_clients() const;
bool any_client_loading() const;
void add_client(std::shared_ptr<Client> c);
void add_client(std::shared_ptr<Client> c, ssize_t required_client_id = -1);
void remove_client(std::shared_ptr<Client> c);
void move_client_to_lobby(std::shared_ptr<Lobby> dest_lobby,
std::shared_ptr<Client> c);
void move_client_to_lobby(
std::shared_ptr<Lobby> dest_lobby,
std::shared_ptr<Client> c,
ssize_t required_client_id = -1);
std::shared_ptr<Client> find_client(
const std::u16string* identifier = nullptr,
@@ -131,4 +136,6 @@ struct Lobby {
uint32_t generate_item_id(uint8_t client_id);
static uint8_t game_event_for_lobby_event(uint8_t lobby_event);
std::unordered_map<uint32_t, std::shared_ptr<Client>> clients_by_serial_number() const;
};
+245 -137
View File
@@ -154,6 +154,17 @@ void populate_state_from_config(shared_ptr<ServerState> s,
s->catch_handler_exceptions = true;
}
try {
s->proxy_allow_save_files = d.at("ProxyAllowSaveFiles")->as_bool();
} catch (const out_of_range&) {
s->proxy_allow_save_files = true;
}
try {
s->proxy_enable_login_options = d.at("ProxyEnableLoginOptions")->as_bool();
} catch (const out_of_range&) {
s->proxy_enable_login_options = false;
}
try {
s->ep3_behavior_flags = d.at("Episode3BehaviorFlags")->as_int();
} catch (const out_of_range&) {
@@ -260,92 +271,98 @@ void drop_privileges(const string& username) {
void print_usage() {
fputs("\
newserv - a Phantasy Star Online Swiss Army knife\n\
\n\
Usage:\n\
newserv [options] [input-filename [output-filename]]\n\
newserv [ACTION [OPTIONS...]]\n\
\n\
With no options, newserv runs in server mode. PSO clients can connect normally,\n\
join lobbies, play games, and use the proxy server. See README.md and\n\
system/config.json for more information.\n\
If ACTION is not specified, newserv runs in server mode. PSO clients can\n\
connect normally, join lobbies, play games, and use the proxy server. See\n\
README.md and system/config.json for more information.\n\
\n\
When options are given, newserv will do things other than running the server.\n\
When ACTION is given, newserv will do things other than running the server.\n\
\n\
Some modes accept input and/or output filenames; see the descriptions below for\n\
details. If input-filename is missing or is '-', newserv reads from stdin;\n\
similarly, if output-filename is missing or is '-', newserv writes to stdout.\n\
Some actions accept input and/or output filenames; see the descriptions below\n\
for details. If INPUT-FILENAME is missing or is '-', newserv reads from stdin.\n\
If OUTPUT-FILENAME is missing and the input is not from stdin, newserv writes\n\
the output to INPUT-FILENAME.dec; if OUTPUT-FILENAME is '-', newserv writes the\n\
output to stdout. If stdout is a terminal, data written there is formatted in a\n\
hex/ASCII view; otherwise, raw (binary) data is written there.\n\
\n\
The options are:\n\
--compress-prs\n\
--decompress-prs\n\
--compress-bc0 [input-filename [output-filename]]\n\
--decompress-bc0 [input-filename [output-filename]]\n\
Compress or decompress data using the PRS or BC0 algorithms. Both\n\
input-filename and output-filename may be specified.\n\
--encrypt-data\n\
--decrypt-data\n\
Encrypt or decrypt data using PSO's standard network protocol encryption.\n\
Both input-filename and output-filename may be specified. By default, PSO\n\
V3 (GameCube/XBOX) encryption is used, but this can be overridden with\n\
the --pc or --bb options. The --seed= option specifies the encryption\n\
seed (4 hex bytes for PC or GC, or 48 hex bytes for BB). For BB, the\n\
--key option is required as well, and refers to a .nsk file in\n\
system/blueburst/keys (without the directory or .nsk extension). For\n\
non-BB ciphers, the --big-endian option applies the cipher masks as\n\
big-endian instead of little-endian, which is necessary for some GameCube\n\
file formats.\n\
--find-decryption-seed\n\
Perform a brute-force search for a decryption seed of the given data.\n\
The ciphertext is specified with the --encrypted= option and the expected\n\
plaintext is specified with the --decrypted= option. The plaintext may\n\
include unmatched bytes (specified with the ? operator), but overall it\n\
must be the same length as the ciphertext. By default, this option uses\n\
PSO V3 encryption, but this can be overridden with --pc. (BB encryption\n\
seeds are too long to be searched for with this function.) By default,\n\
the number of worker threads is equal the the number of CPU cores in the\n\
system, but this can be overridden with the --threads= option.\n\
--decode-sjis\n\
Apply newserv\'s text decoding algorithm to the data on stdin, producing\n\
little-endian UTF-16 data on stdout. Both input-filename and\n\
output-filename may be specified.\n\
--decode-gci\n\
--decode-dlq\n\
--decode-qst\n\
Decode the given quest file into a compressed, unencrypted .bin or .dat\n\
file (or in the case of --decode-qst, both a .bin and a .dat file).\n\
input-filename must be specified, but output-filename msut not be; the\n\
output is written to <input-filename>.dec (or .bin, or .dat). DLQ and QST\n\
decoding is a relatively simple operation, but GCI decoding can be\n\
computationally expensive if the file is encrypted and doesn't contain an\n\
embedded seed. If you know the player\'s serial number who generated the\n\
GCI file, use the --seed= option and give the serial number (as a\n\
hex-encoded 32-bit integer). If you don\'t know the serial number, newserv\n\
will find it via a brute-force search, but this will take a long time.\n\
--cat-client=ADDR:PORT\n\
Connect to the given server and simulate a PSO client. newserv will then\n\
print all the received commands to stdout, and forward any commands typed\n\
into stdin to the remote server. It is assumed that the input and output\n\
are terminals, so all commands are hex-encoded. The --patch, --dc, --pc,\n\
--gc, and --bb options can be used to select the command format and\n\
encryption. If --bb is used, the --key option is also required (as in\n\
--decrypt-data above).\n\
--show-ep3-data\n\
Print the Episode 3 data files (maps and card definitions) from the\n\
system/ep3 directory in a human-readable format.\n\
--show-ep3-card=ID\n\
Describe the Episode 3 card with the given ID.\n\
--replay-log\n\
Replay a terminal log as if it were a client session. input-filename may\n\
be specified for this option. This is used for regression testing, to\n\
make sure client sessions are repeatable and code changes don\'t affect\n\
existing (working) functionality.\n\
--extract-gsl\n\
Extract all files from a GSL archive into the current directory.\n\
input-filename may be specified. If output-filename is specified, then it\n\
is treated as a prefix which is prepended to the filename of each file\n\
contained in the GSL archive. Importantly, if you want to put the files\n\
into a directory, you'll have to create the directory first, and include\n\
a trailing / on output-filename.\n\
The actions are:\n\
help\n\
You\'re reading it now.\n\
compress-prs [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
decompress-prs [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
compress-bc0 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
decompress-bc0 [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Compress or decompress data using the PRS or BC0 algorithms.\n\
prs-size [INPUT-FILENAME]\n\
Compute the decompressed size of the PRS-compressed input data, but don\'t\n\
write the decompressed data anywhere.\n\
encrypt-data [INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]]\n\
decrypt-data [INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS...]]\n\
Encrypt or decrypt data using PSO\'s standard network protocol encryption.\n\
By default, PSO V3 (GameCube/XBOX) encryption is used, but this can be\n\
overridden with the --pc or --bb options. The --seed=SEED option specifies\n\
the encryption seed (4 hex bytes for PC or GC, or 48 hex bytes for BB). For\n\
BB, the --key=KEY-NAME option is required as well, and refers to a .nsk\n\
file in system/blueburst/keys (without the directory or .nsk extension).\n\
For non-BB ciphers, the --big-endian option applies the cipher masks as\n\
big-endian instead of little-endian, which is necessary for some GameCube\n\
file formats.\n\
decrypt-trivial-data [--seed=SEED] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Decrypt (or encrypt; the algorithm is symmetric) data using the Episode 3\n\
trivial algorithm. If SEED is given, it should be specified as one hex\n\
byte. If SEED is not given, newserv will try all possible seeds and return\n\
the one that results in the greatest number of zero bytes in the output.\n\
find-decryption-seed <OPTIONS...>\n\
Perform a brute-force search for a decryption seed of the given data. The\n\
ciphertext is specified with the --encrypted=DATA option and the expected\n\
plaintext is specified with the --decrypted=DATA option. The plaintext may\n\
include unmatched bytes (specified with the Phosg parse_data_string ?\n\
operator), but overall it must be the same length as the ciphertext. By\n\
default, this option uses PSO V3 encryption, but this can be overridden\n\
with --pc. (BB encryption seeds are too long to be searched for with this\n\
function.) By default, the number of worker threads is equal the the number\n\
of CPU cores in the system, but this can be overridden with the\n\
--threads=NUM-THREADS option.\n\
decode-sjis [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
Apply newserv\'s text decoding algorithm to the input data, producing\n\
little-endian UTF-16 output data.\n\
decode-gci INPUT-FILENAME [OPTIONS...]\n\
decode-dlq INPUT-FILENAME\n\
decode-qst INPUT-FILENAME\n\
Decode the input quest file into a compressed, unencrypted .bin or .dat\n\
file (or in the case of decode-qst, both a .bin and a .dat file).\n\
INPUT-FILENAME must be specified, but there is no OUTPUT-FILENAME; the\n\
output is written to INPUT-FILENAME.dec (or .bin, or .dat). DLQ and QST\n\
decoding is a relatively simple operation, but GCI decoding can be\n\
computationally expensive if the file is encrypted and doesn\'t contain an\n\
embedded seed. If you know the player\'s serial number who generated the\n\
GCI file, use the --seed=SEED option and give the serial number (as a\n\
hex-encoded 32-bit integer). If you don\'t know the serial number, newserv\n\
will find it via a brute-force search, which will take a long time.\n\
cat-client ADDR:PORT\n\
Connect to the given server and simulate a PSO client. newserv will then\n\
print all the received commands to stdout, and forward any commands typed\n\
into stdin to the remote server. It is assumed that the input and output\n\
are terminals, so all commands are hex-encoded. The --patch, --dc, --pc,\n\
--gc, and --bb options can be used to select the command format and\n\
encryption. If --bb is used, the --key=KEY-NAME option is also required (as\n\
in decrypt-data above).\n\
show-ep3-data\n\
Print the Episode 3 maps and card definitions from the system/ep3 directory\n\
in a (sort of) human-readable format.\n\
replay-log [INPUT-FILENAME] [OPTIONS...]\n\
Replay a terminal log as if it were a client session. input-filename may be\n\
specified for this option. This is used for regression testing, to make\n\
sure client sessions are repeatable and code changes don\'t affect existing\n\
(working) functionality.\n\
extract-gsl [INPUT-FILENAME] [--big-endian]\n\
Extract all files from a GSL archive into the current directory.\n\
input-filename may be specified. If output-filename is specified, then it\n\
is treated as a prefix which is prepended to the filename of each file\n\
contained in the GSL archive. If --big-endian is given, the GSL header is\n\
read in GameCube format; otherwise it is read in PC/BB format.\n\
\n\
A few options apply to multiple modes described above:\n\
--parse-data\n\
@@ -362,6 +379,8 @@ enum class Behavior {
DECOMPRESS_PRS,
COMPRESS_BC0,
DECOMPRESS_BC0,
PRS_SIZE,
PRS_DISASSEMBLE,
ENCRYPT_DATA,
DECRYPT_DATA,
DECRYPT_TRIVIAL_DATA,
@@ -380,6 +399,8 @@ static bool behavior_takes_input_filename(Behavior b) {
(b == Behavior::DECOMPRESS_PRS) ||
(b == Behavior::COMPRESS_BC0) ||
(b == Behavior::DECOMPRESS_BC0) ||
(b == Behavior::PRS_SIZE) ||
(b == Behavior::PRS_DISASSEMBLE) ||
(b == Behavior::ENCRYPT_DATA) ||
(b == Behavior::DECRYPT_DATA) ||
(b == Behavior::DECRYPT_TRIVIAL_DATA) ||
@@ -403,6 +424,7 @@ static bool behavior_takes_output_filename(Behavior b) {
enum class QuestFileFormat {
GCI = 0,
VMS,
DLQ,
QST,
};
@@ -427,41 +449,10 @@ int main(int argc, char** argv) {
const char* replay_required_password = "";
uint32_t root_object_address = 0;
uint16_t ep3_card_id = 0xFFFF;
struct sockaddr_storage cat_client_remote;
for (int x = 1; x < argc; x++) {
if (!strcmp(argv[x], "--help")) {
print_usage();
return 0;
} else if (!strcmp(argv[x], "--compress-prs")) {
behavior = Behavior::COMPRESS_PRS;
} else if (!strcmp(argv[x], "--decompress-prs")) {
behavior = Behavior::DECOMPRESS_PRS;
} else if (!strcmp(argv[x], "--compress-bc0")) {
behavior = Behavior::COMPRESS_BC0;
} else if (!strcmp(argv[x], "--decompress-bc0")) {
behavior = Behavior::DECOMPRESS_BC0;
} else if (!strcmp(argv[x], "--encrypt-data")) {
behavior = Behavior::ENCRYPT_DATA;
} else if (!strcmp(argv[x], "--decrypt-data")) {
behavior = Behavior::DECRYPT_DATA;
} else if (!strcmp(argv[x], "--decrypt-trivial-data")) {
behavior = Behavior::DECRYPT_TRIVIAL_DATA;
} else if (!strcmp(argv[x], "--find-decryption-seed")) {
behavior = Behavior::FIND_DECRYPTION_SEED;
} else if (!strcmp(argv[x], "--decode-sjis")) {
behavior = Behavior::DECODE_SJIS;
} else if (!strcmp(argv[x], "--decode-gci")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::GCI;
} else if (!strcmp(argv[x], "--decode-dlq")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::DLQ;
} else if (!strcmp(argv[x], "--decode-qst")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::QST;
} else if (!strncmp(argv[x], "--cat-client=", 13)) {
behavior = Behavior::CAT_CLIENT;
cat_client_remote = make_sockaddr_storage(parse_netloc(&argv[x][13])).first;
} else if (!strncmp(argv[x], "--threads=", 10)) {
num_threads = strtoull(&argv[x][10], nullptr, 0);
} else if (!strcmp(argv[x], "--patch")) {
@@ -492,17 +483,6 @@ int main(int argc, char** argv) {
skip_little_endian = true;
} else if (!strcmp(argv[x], "--skip-big-endian")) {
skip_big_endian = true;
} else if (!strcmp(argv[x], "--show-ep3-data")) {
behavior = Behavior::SHOW_EP3_DATA;
} else if (!strncmp(argv[x], "--show-ep3-card=", 16)) {
behavior = Behavior::SHOW_EP3_DATA;
ep3_card_id = strtoul(&argv[x][16], nullptr, 16);
} else if (!strcmp(argv[x], "--parse-object-graph")) {
behavior = Behavior::PARSE_OBJECT_GRAPH;
} else if (!strcmp(argv[x], "--replay-log")) {
behavior = Behavior::REPLAY_LOG;
} else if (!strcmp(argv[x], "--extract-gsl")) {
behavior = Behavior::EXTRACT_GSL;
} else if (!strncmp(argv[x], "--require-password=", 19)) {
replay_required_password = &argv[x][19];
} else if (!strncmp(argv[x], "--require-access-key=", 21)) {
@@ -511,16 +491,69 @@ int main(int argc, char** argv) {
root_object_address = strtoul(&argv[x][12], nullptr, 16);
} else if (!strncmp(argv[x], "--config=", 9)) {
config_filename = &argv[x][9];
} else if (!strcmp(argv[x], "-") || argv[x][0] != '-') {
if (!input_filename && behavior_takes_input_filename(behavior)) {
if (behavior == Behavior::RUN_SERVER) {
if (!strcmp(argv[x], "help")) {
print_usage();
return 0;
} if (!strcmp(argv[x], "compress-prs")) {
behavior = Behavior::COMPRESS_PRS;
} else if (!strcmp(argv[x], "decompress-prs")) {
behavior = Behavior::DECOMPRESS_PRS;
} else if (!strcmp(argv[x], "compress-bc0")) {
behavior = Behavior::COMPRESS_BC0;
} else if (!strcmp(argv[x], "decompress-bc0")) {
behavior = Behavior::DECOMPRESS_BC0;
} else if (!strcmp(argv[x], "prs-size")) {
behavior = Behavior::PRS_SIZE;
} else if (!strcmp(argv[x], "disassemble-prs")) {
behavior = Behavior::PRS_DISASSEMBLE;
} else if (!strcmp(argv[x], "encrypt-data")) {
behavior = Behavior::ENCRYPT_DATA;
} else if (!strcmp(argv[x], "decrypt-data")) {
behavior = Behavior::DECRYPT_DATA;
} else if (!strcmp(argv[x], "decrypt-trivial-data")) {
behavior = Behavior::DECRYPT_TRIVIAL_DATA;
} else if (!strcmp(argv[x], "find-decryption-seed")) {
behavior = Behavior::FIND_DECRYPTION_SEED;
} else if (!strcmp(argv[x], "decode-sjis")) {
behavior = Behavior::DECODE_SJIS;
} else if (!strcmp(argv[x], "decode-gci")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::GCI;
} else if (!strcmp(argv[x], "decode-vms")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::VMS;
} else if (!strcmp(argv[x], "decode-dlq")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::DLQ;
} else if (!strcmp(argv[x], "decode-qst")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::QST;
} else if (!strcmp(argv[x], "cat-client")) {
behavior = Behavior::CAT_CLIENT;
} else if (!strcmp(argv[x], "show-ep3-data")) {
behavior = Behavior::SHOW_EP3_DATA;
} else if (!strcmp(argv[x], "parse-object-graph")) {
behavior = Behavior::PARSE_OBJECT_GRAPH;
} else if (!strcmp(argv[x], "replay-log")) {
behavior = Behavior::REPLAY_LOG;
} else if (!strcmp(argv[x], "extract-gsl")) {
behavior = Behavior::EXTRACT_GSL;
} else {
throw invalid_argument(string_printf("unknown command: %s (try --help)", argv[x]));
}
} else if (!input_filename && behavior_takes_input_filename(behavior)) {
input_filename = argv[x];
} else if (!output_filename && behavior_takes_output_filename(behavior)) {
output_filename = argv[x];
} else {
throw invalid_argument(string_printf("unknown option: %s", argv[x]));
throw invalid_argument(string_printf("unknown option: %s (try --help)", argv[x]));
}
} else {
throw invalid_argument(string_printf("unknown option: %s", argv[x]));
throw invalid_argument(string_printf("unknown option: %s (try --help)", argv[x]));
}
}
@@ -532,17 +565,26 @@ int main(int argc, char** argv) {
data = read_all(stdin);
}
if (parse_data) {
data = parse_data_string(data);
data = parse_data_string(data, nullptr, ParseDataFlags::ALLOW_FILES);
}
return data;
};
auto write_output_data = [&](const void* data, size_t size) {
// If the output is to a specified file, write it there
if (output_filename && strcmp(output_filename, "-")) {
save_file(output_filename, data, size);
// If no output filename is given and an input filename is given, write to
// <input-filename>.dec
} else if (!output_filename && input_filename && strcmp(input_filename, "-")) {
string filename = input_filename;
filename += ".dec";
save_file(filename, data, size);
// If stdout is a terminal, use print_data to write the result
} else if (isatty(fileno(stdout))) {
print_data(stdout, data, size);
fflush(stdout);
// If stdout is not a terminal, write the data as-is
} else {
fwritex(stdout, data, size);
fflush(stdout);
@@ -556,24 +598,52 @@ int main(int argc, char** argv) {
case Behavior::DECOMPRESS_BC0: {
string data = read_input_data();
size_t input_bytes = data.size();
auto progress_fn = [&](size_t input_progress, size_t output_progress) -> void {
float progress = static_cast<float>(input_progress * 100) / input_bytes;
float size_ratio = static_cast<float>(output_progress * 100) / input_progress;
fprintf(stderr, "... %zu/%zu (%g%%) => %zu (%g%%) \r",
input_progress, input_bytes, progress, output_progress, size_ratio);
};
uint64_t start = now();
if (behavior == Behavior::COMPRESS_PRS) {
data = prs_compress(data);
data = prs_compress(data, progress_fn);
} else if (behavior == Behavior::DECOMPRESS_PRS) {
data = prs_decompress(data);
} else if (behavior == Behavior::COMPRESS_BC0) {
data = bc0_compress(data);
data = bc0_compress(data, progress_fn);
} else if (behavior == Behavior::DECOMPRESS_BC0) {
data = bc0_decompress(data);
} else {
throw logic_error("invalid behavior");
}
log_info("%zu (0x%zX) bytes input => %zu (0x%zX) bytes output",
input_bytes, input_bytes, data.size(), data.size());
uint64_t end = now();
string time_str = format_duration(end - start);
float size_ratio = static_cast<float>(data.size() * 100) / input_bytes;
double bytes_per_sec = input_bytes / (static_cast<double>(end - start) / 1000000.0);
string bytes_per_sec_str = format_size(bytes_per_sec);
log_info("%zu (0x%zX) bytes input => %zu (0x%zX) bytes output (%g%%) in %s (%s / sec)",
input_bytes, input_bytes, data.size(), data.size(), size_ratio, time_str.c_str(), bytes_per_sec_str.c_str());
write_output_data(data.data(), data.size());
break;
}
case Behavior::PRS_SIZE: {
string data = read_input_data();
size_t input_bytes = data.size();
size_t output_bytes = prs_decompress_size(data);
log_info("%zu (0x%zX) bytes input => %zu (0x%zX) bytes output",
input_bytes, input_bytes, output_bytes, output_bytes);
break;
}
case Behavior::PRS_DISASSEMBLE: {
prs_disassemble(stdout, read_input_data());
break;
}
case Behavior::DECRYPT_DATA:
case Behavior::ENCRYPT_DATA: {
shared_ptr<PSOEncryption> crypt;
@@ -588,7 +658,7 @@ int main(int argc, char** argv) {
crypt.reset(new PSOV3Encryption(stoul(seed, nullptr, 16)));
break;
case GameVersion::BB: {
seed = parse_data_string(seed);
seed = parse_data_string(seed, nullptr, ParseDataFlags::ALLOW_FILES);
auto key = load_object_file<PSOBBEncryption::KeyFile>(
"system/blueburst/keys/" + key_file_name + ".nsk");
crypt.reset(new PSOBBEncryption(key, seed.data(), seed.size()));
@@ -633,8 +703,30 @@ int main(int argc, char** argv) {
}
case Behavior::DECRYPT_TRIVIAL_DATA: {
uint8_t basis = stoul(seed, nullptr, 16);
string data = read_input_data();
uint8_t basis;
if (seed.empty()) {
uint8_t best_seed = 0x00;
size_t best_seed_score = 0;
for (size_t z = 0; z < 0x100; z++) {
string decrypted = data;
decrypt_trivial_gci_data(decrypted.data(), decrypted.size(), z);
size_t score = 0;
for (size_t x = 0; x < decrypted.size(); x++) {
if (decrypted[x] == '\0') {
score++;
}
}
if (score > best_seed_score) {
best_seed = z;
best_seed_score = score;
}
}
fprintf(stderr, "Basis appears to be %02hhX\n", best_seed);
basis = best_seed;
} else {
basis = stoul(seed, nullptr, 16);
}
decrypt_trivial_gci_data(data.data(), data.size(), basis);
write_output_data(data.data(), data.size());
break;
@@ -652,14 +744,14 @@ int main(int argc, char** argv) {
vector<pair<string, string>> plaintexts;
for (const auto& plaintext_ascii : find_decryption_seed_plaintexts) {
string mask;
string data = parse_data_string(plaintext_ascii, &mask);
string data = parse_data_string(plaintext_ascii, &mask, ParseDataFlags::ALLOW_FILES);
if (data.size() != mask.size()) {
throw logic_error("plaintext and mask are not the same size");
}
max_plaintext_size = max<size_t>(max_plaintext_size, data.size());
plaintexts.emplace_back(move(data), move(mask));
}
string ciphertext = parse_data_string(find_decryption_seed_ciphertext);
string ciphertext = parse_data_string(find_decryption_seed_ciphertext, nullptr, ParseDataFlags::ALLOW_FILES);
auto mask_match = +[](const void* a, const void* b, const void* m, size_t size) -> bool {
const uint8_t* a8 = reinterpret_cast<const uint8_t*>(a);
@@ -722,6 +814,10 @@ int main(int argc, char** argv) {
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
save_file(output_filename_base + ".dec", Quest::decode_gci(
input_filename, num_threads, dec_seed));
} else if (quest_file_type == QuestFileFormat::VMS) {
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
save_file(output_filename_base + ".dec", Quest::decode_vms(
input_filename, num_threads, dec_seed));
} else if (quest_file_type == QuestFileFormat::DLQ) {
save_file(output_filename_base + ".dec", Quest::decode_dlq(
input_filename));
@@ -752,7 +848,7 @@ int main(int argc, char** argv) {
string data = read_input_data();
shared_ptr<string> data_shared(new string(move(data)));
GSLArchive gsl(data_shared);
GSLArchive gsl(data_shared, big_endian);
for (const auto& entry_it : gsl.all_entries()) {
auto e = gsl.get(entry_it.first);
save_file(output_filename + entry_it.first, e.first, e.second);
@@ -771,6 +867,7 @@ int main(int argc, char** argv) {
load_object_file<PSOBBEncryption::KeyFile>("system/blueburst/keys/" + key_file_name + ".nsk")));
}
shared_ptr<struct event_base> base(event_base_new(), event_base_free);
auto cat_client_remote = make_sockaddr_storage(parse_netloc(input_filename)).first;
CatSession session(base, cat_client_remote, cli_version, key);
event_base_dispatch(base.get());
break;
@@ -857,7 +954,7 @@ int main(int argc, char** argv) {
state->bb_patch_file_index.reset(new PatchFileIndex("system/patch-bb"));
try {
auto gsl_file = state->bb_patch_file_index->get("./data/data.gsl");
state->bb_data_gsl.reset(new GSLArchive(gsl_file->load_data()));
state->bb_data_gsl.reset(new GSLArchive(gsl_file->load_data(), false));
config_log.info("data.gsl found in BB patch files");
} catch (const out_of_range&) {
config_log.info("data.gsl is not present in BB patch files");
@@ -887,6 +984,17 @@ int main(int argc, char** argv) {
state->ep3_data_index.reset(new Episode3::DataIndex(
"system/ep3", state->ep3_behavior_flags));
const string& tournament_state_filename = "system/ep3/tournament-state.json";
try {
state->ep3_tournament_index.reset(new Episode3::TournamentIndex(
state->ep3_data_index, tournament_state_filename));
config_log.info("Loaded Episode 3 tournament state");
} catch (const exception& e) {
config_log.warning("Cannot load Episode 3 tournament state: %s", e.what());
state->ep3_tournament_index.reset(new Episode3::TournamentIndex(
state->ep3_data_index, tournament_state_filename, true));
}
config_log.info("Collecting quest metadata");
state->quest_index.reset(new QuestIndex("system/quests"));
+27 -19
View File
@@ -13,16 +13,19 @@
// casting values all over the place, so we can't use enum classes either.
namespace MenuID {
constexpr uint32_t MAIN = 0x11000011;
constexpr uint32_t INFORMATION = 0x22000022;
constexpr uint32_t LOBBY = 0x33000033;
constexpr uint32_t GAME = 0x44000044;
constexpr uint32_t QUEST = 0x55000055;
constexpr uint32_t QUEST_FILTER = 0x66000066;
constexpr uint32_t PROXY_DESTINATIONS = 0x77000077;
constexpr uint32_t PROGRAMS = 0x88000088;
constexpr uint32_t PATCHES = 0x99000099;
constexpr uint32_t PROXY_OPTIONS = 0xAA0000AA;
constexpr uint32_t MAIN = 0x11000011;
constexpr uint32_t INFORMATION = 0x22000022;
constexpr uint32_t LOBBY = 0x33000033;
constexpr uint32_t GAME = 0x44000044;
constexpr uint32_t QUEST = 0x55000055;
constexpr uint32_t QUEST_FILTER = 0x66000066;
constexpr uint32_t PROXY_DESTINATIONS = 0x77000077;
constexpr uint32_t PROGRAMS = 0x88000088;
constexpr uint32_t PATCHES = 0x99000099;
constexpr uint32_t PROXY_OPTIONS = 0xAA0000AA;
constexpr uint32_t TOURNAMENTS = 0xBB0000BB;
constexpr uint32_t TOURNAMENTS_FOR_SPEC = 0xBB1111BB;
constexpr uint32_t TOURNAMENT_ENTRIES = 0xCC0000CC;
}
namespace MainMenuItemID {
@@ -54,15 +57,20 @@ namespace PatchesMenuItemID {
}
namespace ProxyOptionsMenuItemID {
constexpr uint32_t GO_BACK = 0xAAFFFFAA;
constexpr uint32_t INFINITE_HP = 0xAA1111AA;
constexpr uint32_t INFINITE_TP = 0xAA2222AA;
constexpr uint32_t SWITCH_ASSIST = 0xAA3333AA;
constexpr uint32_t BLOCK_EVENTS = 0xAA4444AA;
constexpr uint32_t BLOCK_PATCHES = 0xAA5555AA;
constexpr uint32_t SAVE_FILES = 0xAA6666AA;
constexpr uint32_t SUPPRESS_LOGIN = 0xAA7777AA;
constexpr uint32_t SKIP_CARD = 0xAA8888AA;
constexpr uint32_t GO_BACK = 0xAAFFFFAA;
constexpr uint32_t CHAT_COMMANDS = 0xAA0000AA;
constexpr uint32_t CHAT_FILTER = 0xAA1111AA;
constexpr uint32_t INFINITE_HP = 0xAA2222AA;
constexpr uint32_t INFINITE_TP = 0xAA3333AA;
constexpr uint32_t SWITCH_ASSIST = 0xAA4444AA;
constexpr uint32_t BLOCK_EVENTS = 0xAA5555AA;
constexpr uint32_t BLOCK_PATCHES = 0xAA6666AA;
constexpr uint32_t SAVE_FILES = 0xAA7777AA;
constexpr uint32_t RED_NAME = 0xAA8888AA;
constexpr uint32_t BLANK_NAME = 0xAA9999AA;
constexpr uint32_t SUPPRESS_LOGIN = 0xAAAAAAAA;
constexpr uint32_t SKIP_CARD = 0xAABBBBAA;
constexpr uint32_t EP3_INFINITE_MESETA = 0xAACCCCAA;
}
+37 -8
View File
@@ -23,11 +23,11 @@ void PSOEncryption::decrypt(void* data, size_t size, bool advance) {
PSORC4Encryption::PSORC4Encryption(
PSOLFGEncryption::PSOLFGEncryption(
uint32_t seed, size_t stream_length, size_t end_offset)
: stream(stream_length, 0), offset(0), end_offset(end_offset), seed(seed) { }
uint32_t PSORC4Encryption::next(bool advance) {
uint32_t PSOLFGEncryption::next(bool advance) {
if (this->offset == this->end_offset) {
this->update_stream();
}
@@ -39,7 +39,7 @@ uint32_t PSORC4Encryption::next(bool advance) {
}
template <typename LongT>
void PSORC4Encryption::encrypt_t(void* vdata, size_t size, bool advance) {
void PSOLFGEncryption::encrypt_t(void* vdata, size_t size, bool advance) {
if (size & 3) {
throw invalid_argument("size must be a multiple of 4");
}
@@ -54,15 +54,15 @@ void PSORC4Encryption::encrypt_t(void* vdata, size_t size, bool advance) {
}
}
void PSORC4Encryption::encrypt(void* vdata, size_t size, bool advance) {
void PSOLFGEncryption::encrypt(void* vdata, size_t size, bool advance) {
this->encrypt_t<le_uint32_t>(vdata, size, advance);
}
void PSORC4Encryption::encrypt_big_endian(void* vdata, size_t size, bool advance) {
void PSOLFGEncryption::encrypt_big_endian(void* vdata, size_t size, bool advance) {
this->encrypt_t<be_uint32_t>(vdata, size, advance);
}
void PSORC4Encryption::encrypt_both_endian(
void PSOLFGEncryption::encrypt_both_endian(
void* le_vdata, void* be_vdata, size_t size, bool advance) {
if (size & 3) {
throw invalid_argument("size must be a multiple of 4");
@@ -84,7 +84,7 @@ void PSORC4Encryption::encrypt_both_endian(
PSOV2Encryption::PSOV2Encryption(uint32_t seed)
: PSORC4Encryption(seed, this->STREAM_LENGTH + 1, this->STREAM_LENGTH) {
: PSOLFGEncryption(seed, this->STREAM_LENGTH + 1, this->STREAM_LENGTH) {
uint32_t esi, ebx, edi, eax, edx, var1;
esi = 1;
ebx = this->seed;
@@ -138,7 +138,7 @@ PSOEncryption::Type PSOV2Encryption::type() const {
PSOV3Encryption::PSOV3Encryption(uint32_t seed)
: PSORC4Encryption(seed, this->STREAM_LENGTH, this->STREAM_LENGTH) {
: PSOLFGEncryption(seed, this->STREAM_LENGTH, this->STREAM_LENGTH) {
uint32_t x, y, basekey, source1, source2, source3;
basekey = 0;
@@ -837,6 +837,35 @@ shared_ptr<PSOBBEncryption> PSOBBMultiKeyImitatorEncryption::ensure_crypt() {
JSD0Encryption::JSD0Encryption(const void* seed, size_t seed_size) : key(0) {
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(seed);
for (size_t z = 0; z < seed_size; z++) {
this->key ^= bytes[z];
}
}
void JSD0Encryption::decrypt(void* data, size_t size, bool) {
uint8_t* bytes = reinterpret_cast<uint8_t*>(data);
for (size_t z = 0; z < size; z++) {
bytes[z] ^= this->key;
bytes[z] -= this->key;
}
}
void JSD0Encryption::encrypt(void* data, size_t size, bool) {
uint8_t* bytes = reinterpret_cast<uint8_t*>(data);
for (size_t z = 0; z < size; z++) {
bytes[z] += this->key;
bytes[z] ^= this->key;
}
}
PSOEncryption::Type JSD0Encryption::type() const {
return Type::JSD0;
}
void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis) {
uint8_t* bytes = reinterpret_cast<uint8_t*>(data);
uint8_t key = basis + 0x80;
+20 -4
View File
@@ -18,6 +18,7 @@ public:
V2 = 0,
V3,
BB,
JSD0,
};
virtual ~PSOEncryption() = default;
@@ -40,7 +41,7 @@ protected:
class PSORC4Encryption : public PSOEncryption {
class PSOLFGEncryption : public PSOEncryption {
public:
virtual void encrypt(void* data, size_t size, bool advance = true);
void encrypt_big_endian(void* data, size_t size, bool advance = true);
@@ -49,7 +50,7 @@ public:
uint32_t next(bool advance = true);
protected:
explicit PSORC4Encryption(uint32_t seed, size_t stream_length, size_t end_offset);
explicit PSOLFGEncryption(uint32_t seed, size_t stream_length, size_t end_offset);
template <typename LongT>
void encrypt_t(void* data, size_t size, bool advance);
@@ -62,7 +63,7 @@ protected:
uint32_t seed;
};
class PSOV2Encryption : public PSORC4Encryption {
class PSOV2Encryption : public PSOLFGEncryption {
public:
explicit PSOV2Encryption(uint32_t seed);
@@ -74,7 +75,7 @@ protected:
static constexpr size_t STREAM_LENGTH = 56;
};
class PSOV3Encryption : public PSORC4Encryption {
class PSOV3Encryption : public PSOLFGEncryption {
public:
explicit PSOV3Encryption(uint32_t key);
@@ -229,4 +230,19 @@ protected:
class JSD0Encryption : public PSOEncryption {
public:
JSD0Encryption(const void* seed, size_t seed_size);
virtual void encrypt(void* data, size_t size, bool advance = true);
virtual void decrypt(void* data, size_t size, bool advance = true);
virtual Type type() const = 0;
private:
uint8_t key;
};
void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis);
+6 -3
View File
@@ -793,18 +793,21 @@ void PlayerBank::add_item(const PlayerBankItem& item) {
// TODO: Eliminate code duplication between this function and the parallel
// function in PlayerBank
PlayerInventoryItem SavedPlayerDataBB::remove_item(
uint32_t item_id, uint32_t amount) {
uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft) {
PlayerInventoryItem ret;
// If we're removing meseta (signaled by an invalid item ID), then create a
// meseta item.
if (item_id == 0xFFFFFFFF) {
if (amount > this->disp.meseta) {
if (amount <= this->disp.meseta) {
this->disp.meseta -= amount;
} else if (allow_meseta_overdraft) {
this->disp.meseta = 0;
} else {
throw out_of_range("player does not have enough meseta");
}
ret.data.data1[0] = 0x04;
ret.data.data2d = amount;
this->disp.meseta -= amount;
return ret;
}
+21 -5
View File
@@ -370,6 +370,8 @@ struct PlayerChallengeDataV3 {
parray<UnknownPair, 3> unknown_a6; // 0x18 bytes
parray<uint8_t, 0x28> unknown_a7;
} __attribute__((packed)) unknown_a1; // 0x100 bytes
// On Episode 3, unknown_a2[0] is win count, [1] is loss count, and [4] is
// disconnect count
parray<le_uint16_t, 8> unknown_a2;
parray<le_uint32_t, 2> unknown_a3;
} __attribute__((packed)); // 0x11C bytes
@@ -381,6 +383,19 @@ struct PlayerChallengeDataBB {
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));
struct PSOPlayerDataDCPC { // For command 61
PlayerInventory inventory;
PlayerDispDataDCPCV3 disp;
@@ -390,7 +405,7 @@ struct PSOPlayerDataV3 { // For command 61
PlayerInventory inventory;
PlayerDispDataDCPCV3 disp;
PlayerChallengeDataV3 challenge_data;
parray<uint8_t, 0x18> unknown;
ChoiceSearchConfig<le_uint16_t> choice_search_config;
ptext<char, 0xAC> info_board;
parray<le_uint32_t, 0x1E> blocked_senders;
le_uint32_t auto_reply_enabled;
@@ -404,11 +419,11 @@ struct PSOPlayerDataGCEp3 { // For command 61
PlayerInventory inventory;
PlayerDispDataDCPCV3 disp;
PlayerChallengeDataV3 challenge_data;
parray<uint8_t, 0x18> unknown;
ChoiceSearchConfig<le_uint16_t> choice_search_config;
ptext<char, 0xAC> info_board;
parray<le_uint32_t, 0x1E> blocked_senders;
le_uint32_t auto_reply_enabled;
char auto_reply[0xAC];
ptext<char, 0xAC> auto_reply;
Episode3::PlayerConfig ep3_config;
} __attribute__((packed));
@@ -416,7 +431,7 @@ struct PSOPlayerDataBB { // For command 61
PlayerInventory inventory;
PlayerDispDataBB disp;
PlayerChallengeDataBB challenge_data;
parray<uint8_t, 0x18> unknown;
ChoiceSearchConfig<le_uint16_t> choice_search_config;
ptext<char16_t, 0xAC> info_board;
parray<le_uint32_t, 0x1E> blocked_senders;
le_uint32_t auto_reply_enabled;
@@ -468,7 +483,8 @@ struct SavedPlayerDataBB { // .nsc file format
parray<uint8_t, 0x0028> tech_menu_config;
void add_item(const PlayerInventoryItem& item);
PlayerInventoryItem remove_item(uint32_t item_id, uint32_t amount);
PlayerInventoryItem remove_item(
uint32_t item_id, uint32_t amount, bool allow_meseta_overdraft);
void print_inventory(FILE* stream) const;
} __attribute__((packed));
+601 -327
View File
File diff suppressed because it is too large Load Diff
+47 -22
View File
@@ -44,6 +44,7 @@ ProxyServer::ProxyServer(
shared_ptr<struct event_base> base,
shared_ptr<ServerState> state)
: base(base),
destroy_sessions_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &ProxyServer::dispatch_destroy_sessions, this), event_free),
state(state),
next_unlicensed_session_id(0xFF00000000000001) { }
@@ -196,8 +197,6 @@ void ProxyServer::on_client_connect(
auto cmd = prepare_server_init_contents_console(
server_key, client_key, 0);
session->channel.send(0x02, 0x00, &cmd, sizeof(cmd));
// TODO: Is this actually needed?
// bufferevent_flush(session->channel.bev.get(), EV_READ | EV_WRITE, BEV_FLUSH);
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));
@@ -356,6 +355,9 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
should_close_unlinked_session = true;
}
// Note that ch.bev will be moved from when the linked session is resumed, so
// we need to retain a copy of it in order to close the unlinked session
// afterward.
struct bufferevent* session_key = ch.bev.get();
// If license is non-null, then the client has a password and can be connected
@@ -425,9 +427,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3
}
if (should_close_unlinked_session) {
session->log.info("Closing session");
session->server->bev_to_unlinked_session.erase(session_key);
// At this point, (*this) is destroyed! We must be careful not to touch it.
session->server->delete_session(session_key);
}
}
@@ -440,8 +440,8 @@ void ProxyServer::UnlinkedSession::on_error(Channel& ch, short events) {
evutil_socket_error_to_string(err));
}
if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) {
session->log.info("Unlinked client has disconnected");
session->server->bev_to_unlinked_session.erase(session->channel.bev.get());
session->log.info("Client has disconnected");
session->server->delete_session(session->channel.bev.get());
}
}
@@ -482,20 +482,13 @@ ProxyServer::LinkedSession::LinkedSession(
sub_version(0), // This is set during resume()
language(1), // Default = English. This is also set during resume()
remote_guild_card_number(-1),
enable_chat_filter(true),
switch_assist(false),
infinite_hp(false),
infinite_tp(false),
save_files(false),
suppress_remote_login(false),
function_call_return_value(-1),
next_item_id(0x0F000000),
override_section_id(-1),
override_lobby_event(-1),
override_lobby_number(-1),
lobby_players(12),
lobby_client_id(0),
leader_client_id(0),
area(0),
x(0.0),
z(0.0),
is_in_game(false) {
this->last_switch_enabled_command.header.subcommand = 0;
memset(this->prev_server_command_bytes, 0, sizeof(this->prev_server_command_bytes));
@@ -655,13 +648,13 @@ void ProxyServer::LinkedSession::on_error(Channel& ch, short events) {
if (events & BEV_EVENT_CONNECTED) {
session->log.info("%s channel connected", is_server_stream ? "Server" : "Client");
if (is_server_stream && (session->override_lobby_event >= 0) &&
if (is_server_stream && (session->options.override_lobby_event >= 0) &&
(
((session->version == GameVersion::GC) && !(session->newserv_client_config.cfg.flags & Client::Flag::IS_TRIAL_EDITION)) ||
(session->version == GameVersion::XB) ||
(session->version == GameVersion::BB)
)) {
session->client_channel.send(0xDA, session->override_lobby_event);
session->client_channel.send(0xDA, session->options.override_lobby_event);
}
}
if (events & BEV_EVENT_ERROR) {
@@ -722,9 +715,7 @@ void ProxyServer::LinkedSession::send_to_game_server(const char* error_message)
update_client_config_cmd.cfg = this->newserv_client_config.cfg;
this->client_channel.send(0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd));
static const vector<string> version_to_port_name({
"bb-patch", "console-login", "pc-login", "console-login", "console-login", "bb-init"});
const auto& port_name = version_to_port_name.at(static_cast<size_t>(
const auto& port_name = version_to_login_port_name.at(static_cast<size_t>(
this->version));
S_Reconnect_19 reconnect_cmd = {{
@@ -750,6 +741,7 @@ void ProxyServer::LinkedSession::send_to_game_server(const char* error_message)
}
this->client_channel.send(0x19, 0x00, &reconnect_cmd, sizeof(reconnect_cmd));
this->close_on_disconnect = true;
}
}
@@ -804,6 +796,18 @@ shared_ptr<ProxyServer::LinkedSession> ProxyServer::get_session() {
return this->id_to_session.begin()->second;
}
shared_ptr<ProxyServer::LinkedSession> ProxyServer::get_session_by_name(
const std::string& name) {
try {
uint64_t session_id = stoull(name, nullptr, 16);
return this->id_to_session.at(session_id);
} catch (const invalid_argument&) {
throw runtime_error("invalid session name");
} catch (const out_of_range&) {
throw runtime_error("no such session");
}
}
shared_ptr<ProxyServer::LinkedSession> ProxyServer::create_licensed_session(
shared_ptr<const License> l, uint16_t local_port, GameVersion version,
const ClientConfigBB& newserv_client_config) {
@@ -823,6 +827,27 @@ void ProxyServer::delete_session(uint64_t id) {
}
}
void ProxyServer::delete_session(struct bufferevent* bev) {
auto it = this->bev_to_unlinked_session.find(bev);
if (it == this->bev_to_unlinked_session.end()) {
throw logic_error("unlinked session exists but is not registered");
}
it->second->log.info("Closing session");
this->unlinked_sessions_to_destroy.emplace(move(it->second));
this->bev_to_unlinked_session.erase(it);
auto tv = usecs_to_timeval(0);
event_add(this->destroy_sessions_ev.get(), &tv);
}
void ProxyServer::dispatch_destroy_sessions(evutil_socket_t, short, void* ctx) {
reinterpret_cast<ProxyServer*>(ctx)->destroy_sessions();
}
void ProxyServer::destroy_sessions() {
this->unlinked_sessions_to_destroy.clear();
}
size_t ProxyServer::delete_disconnected_sessions() {
size_t count = 0;
for (auto it = this->id_to_session.begin(); it != this->id_to_session.end();) {
+10 -11
View File
@@ -59,24 +59,14 @@ public:
std::string hardware_id; // Only used for DC sessions
std::string login_command_bb;
ClientOptions options;
int64_t remote_guild_card_number;
parray<uint8_t, 0x20> remote_client_config_data;
ClientConfigBB newserv_client_config;
bool enable_chat_filter;
bool switch_assist;
bool infinite_hp;
bool infinite_tp;
bool save_files;
bool suppress_remote_login;
std::deque<bool> should_forward_function_call_return_queue;
int64_t function_call_return_value; // -1 = don't block function calls
G_SwitchStateChanged_6x05 last_switch_enabled_command;
PlayerInventoryItem next_drop_item;
uint32_t next_item_id;
int16_t override_section_id;
int16_t override_lobby_event;
int16_t override_lobby_number;
int64_t override_random_seed;
struct LobbyPlayer {
uint32_t guild_card_number;
@@ -89,6 +79,8 @@ public:
size_t lobby_client_id;
size_t leader_client_id;
uint16_t area;
float x;
float z;
bool is_in_game;
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt;
@@ -162,12 +154,14 @@ 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,
uint16_t local_port,
GameVersion version,
const ClientConfigBB& newserv_client_config);
void delete_session(uint64_t id);
void delete_session(struct bufferevent* bev);
size_t delete_disconnected_sessions();
@@ -215,12 +209,17 @@ private:
};
std::shared_ptr<struct event_base> base;
std::shared_ptr<struct event> destroy_sessions_ev;
std::shared_ptr<ServerState> state;
std::map<int, std::shared_ptr<ListeningSocket>> listeners;
std::unordered_map<struct bufferevent*, std::shared_ptr<UnlinkedSession>> bev_to_unlinked_session;
std::unordered_set<std::shared_ptr<UnlinkedSession>> unlinked_sessions_to_destroy;
std::unordered_map<uint64_t, std::shared_ptr<LinkedSession>> id_to_session;
uint64_t next_unlicensed_session_id;
static void dispatch_destroy_sessions(evutil_socket_t, short, void* ctx);
void destroy_sessions();
void on_client_connect(
struct bufferevent* bev,
uint16_t listen_port,
+188 -39
View File
@@ -96,7 +96,7 @@ struct PSOGCIFileHeader {
}
} __attribute__((packed));
struct PSOGCIFileEncryptedHeader {
struct PSOGCIOrVMSFileEncryptedHeader {
be_uint32_t round2_seed;
// To compute checksum, set checksum to zero, then compute the CRC32 of the
// entire data section, including this header struct (but not the unencrypted
@@ -107,49 +107,69 @@ struct PSOGCIFileEncryptedHeader {
// Data follows here.
} __attribute__((packed));
string decrypt_gci_data_section(
const void* data_section, size_t size, uint32_t seed) {
template <bool IsBigEndian>
string decrypt_gci_or_vms_v2_data_section(
const void* data_section,
size_t size,
uint32_t seed,
bool use_reverse_table) {
string decrypted(size, '\0');
{
PSOV2Encryption shuf_crypt(seed);
ShuffleTables shuf(shuf_crypt);
shuf.shuffle(decrypted.data(), data_section, size, true);
shuf.shuffle(decrypted.data(), data_section, size, use_reverse_table);
}
auto* be_dwords = reinterpret_cast<be_uint32_t*>(decrypted.data());
PSOV2Encryption crypt(seed);
for (size_t z = 0; z < decrypted.size() / sizeof(be_uint32_t); z++) {
be_dwords[z] = crypt.next() - be_dwords[z];
if (IsBigEndian) {
auto* be_dwords = reinterpret_cast<be_uint32_t*>(decrypted.data());
for (size_t z = 0; z < decrypted.size() / sizeof(be_uint32_t); z++) {
be_dwords[z] = crypt.next() - be_dwords[z];
}
} else {
auto* le_dwords = reinterpret_cast<le_uint32_t*>(decrypted.data());
for (size_t z = 0; z < decrypted.size() / sizeof(le_uint32_t); z++) {
le_dwords[z] = crypt.next() - le_dwords[z];
}
}
auto* header = reinterpret_cast<PSOGCIFileEncryptedHeader*>(
auto* header = reinterpret_cast<PSOGCIOrVMSFileEncryptedHeader*>(
decrypted.data());
PSOV2Encryption(header->round2_seed).encrypt_big_endian(
decrypted.data() + 4, (decrypted.size() - 4) & (~3));
PSOV2Encryption round2_crypt(header->round2_seed);
if (IsBigEndian) {
round2_crypt.encrypt_big_endian(
decrypted.data() + 4, (decrypted.size() - 4) & (~3));
} else {
round2_crypt.decrypt(
decrypted.data() + 4, (decrypted.size() - 4) & (~3));
}
uint32_t expected_crc = header->checksum;
header->checksum = 0;
uint32_t actual_crc = crc32(decrypted.data(), decrypted.size());
header->checksum = expected_crc;
if (expected_crc != actual_crc) {
throw runtime_error("incorrect decrypted data section checksum");
}
if (header->decompressed_size & 0xFFF00000) {
throw runtime_error("decompressed_size too large");
}
size_t orig_size = decrypted.size();
decrypted.resize((orig_size + 3) & (~3));
PSOV2Encryption(header->round3_seed).decrypt(
decrypted.data() + sizeof(PSOGCIFileEncryptedHeader),
decrypted.size() - sizeof(PSOGCIFileEncryptedHeader));
decrypted.data() + sizeof(PSOGCIOrVMSFileEncryptedHeader),
decrypted.size() - sizeof(PSOGCIOrVMSFileEncryptedHeader));
decrypted.resize(orig_size);
string ret = decrypted.substr(sizeof(PSOGCIFileEncryptedHeader));
// Some GCI files have decompressed_size fields that are 8 bytes smaller than
// the actual decompressed size of the data. They seem to work fine, so we
// accept both cases as correct.
size_t decompressed_size = prs_decompress_size(ret);
size_t decompressed_size = prs_decompress_size(
decrypted.data() + sizeof(PSOGCIOrVMSFileEncryptedHeader),
decrypted.size() - sizeof(PSOGCIOrVMSFileEncryptedHeader));
if ((decompressed_size != header->decompressed_size) &&
(decompressed_size != header->decompressed_size - 8)) {
throw runtime_error(string_printf(
@@ -157,16 +177,40 @@ string decrypt_gci_data_section(
decompressed_size, header->decompressed_size.load()));
}
return ret;
return decrypted.substr(sizeof(PSOGCIOrVMSFileEncryptedHeader));
}
string find_seed_and_decrypt_gci_data_section(
string decrypt_vms_v1_data_section(const void* data_section, size_t size) {
StringReader r(data_section, size);
uint32_t expected_decompressed_size = r.get_u32l();
uint32_t seed = r.get_u32l();
string data = r.read(r.remaining());
size_t orig_size = data.size();
data.resize((orig_size + 3) & (~3));
PSOV2Encryption(seed).decrypt(data.data(), data.size());
data.resize(orig_size);
size_t actual_decompressed_size = prs_decompress_size(data);
if (actual_decompressed_size != expected_decompressed_size) {
throw runtime_error(string_printf(
"decompressed size (%zu) does not match size in header (%" PRId32 ")",
actual_decompressed_size, expected_decompressed_size));
}
return data;
}
template <bool IsBigEndian>
string find_seed_and_decrypt_gci_or_vms_v2_data_section(
const void* data_section, size_t size, size_t num_threads) {
mutex result_lock;
string result;
uint64_t result_seed = parallel_range<uint64_t>([&](uint64_t seed, size_t) {
try {
string ret = decrypt_gci_data_section(data_section, size, seed);
string ret = decrypt_gci_or_vms_v2_data_section<IsBigEndian>(
data_section, size, seed, true);
lock_guard<mutex> g(result_lock);
result = move(ret);
return true;
@@ -186,6 +230,47 @@ string find_seed_and_decrypt_gci_data_section(
struct PSOVMSFileHeader {
ptext<char, 0x10> short_desc; // "PSO/DOWNLOAD " or "PSOV2/DOWNLOAD "
ptext<char, 0x20> long_desc; // Usually quest name
ptext<char, 0x10> creator_id;
le_uint16_t num_icons;
le_uint16_t animation_speed;
le_uint16_t eyecatch_type;
le_uint16_t crc;
le_uint32_t data_size; // Not including header and icons
parray<uint8_t, 0x14> unused;
parray<le_uint16_t, 0x10> icon_palette;
// Variable-length field follows here:
// parray<parray<uint8_t, 0x200>, num_icons> icon;
bool 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);
}
} __attribute__((packed));
struct PSODownloadQuestHeader {
le_uint32_t size;
le_uint32_t encryption_seed;
@@ -325,6 +410,9 @@ Quest::Quest(const string& bin_filename)
this->file_format = FileFormat::BIN_DAT_GCI;
this->has_mnm_extension = ends_with(bin_filename, ".mnm.gci");
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
} else if (ends_with(bin_filename, ".bin.vms")) {
this->file_format = FileFormat::BIN_DAT_VMS;
this->file_basename = bin_filename.substr(0, bin_filename.size() - 8);
} else if (ends_with(bin_filename, ".bin.dlq") || ends_with(bin_filename, ".mnm.dlq")) {
this->file_format = FileFormat::BIN_DAT_DLQ;
this->has_mnm_extension = ends_with(bin_filename, ".mnm.dlq");
@@ -546,7 +634,11 @@ shared_ptr<const string> Quest::bin_contents() const {
break;
case FileFormat::BIN_DAT_GCI:
this->bin_contents_ptr.reset(new string(this->decode_gci(
this->file_basename + (this->has_mnm_extension ? ".mnm.gci" : ".bin.gci"), false)));
this->file_basename + (this->has_mnm_extension ? ".mnm.gci" : ".bin.gci"))));
break;
case FileFormat::BIN_DAT_VMS:
this->bin_contents_ptr.reset(new string(this->decode_vms(
this->file_basename + (this->has_mnm_extension ? ".mnm.vms" : ".bin.vms"))));
break;
case FileFormat::BIN_DAT_DLQ:
this->bin_contents_ptr.reset(new string(this->decode_dlq(
@@ -578,7 +670,10 @@ shared_ptr<const string> Quest::dat_contents() const {
this->dat_contents_ptr.reset(new string(prs_compress(load_file(this->file_basename + ".datd"))));
break;
case FileFormat::BIN_DAT_GCI:
this->dat_contents_ptr.reset(new string(this->decode_gci(this->file_basename + ".dat.gci", false)));
this->dat_contents_ptr.reset(new string(this->decode_gci(this->file_basename + ".dat.gci")));
break;
case FileFormat::BIN_DAT_VMS:
this->dat_contents_ptr.reset(new string(this->decode_vms(this->file_basename + ".dat.vms")));
break;
case FileFormat::BIN_DAT_DLQ:
this->dat_contents_ptr.reset(new string(this->decode_dlq(this->file_basename + ".dat.dlq")));
@@ -617,18 +712,18 @@ string Quest::decode_gci(
}
if (header.game_id[2] == 'O') { // Episodes 1&2 (GPO*)
const auto& encrypted_header = r.get<PSOGCIFileEncryptedHeader>(false);
const auto& encrypted_header = r.get<PSOGCIOrVMSFileEncryptedHeader>(false);
// Unencrypted GCI files appear to always have zeroes in these fields.
// Encrypted GCI files are highly unlikely to have zeroes in ALL of these
// fields, so assume it's encrypted if any of them are nonzero.
if (encrypted_header.round2_seed || encrypted_header.checksum || encrypted_header.round3_seed) {
if (known_seed >= 0) {
return decrypt_gci_data_section(
r.getv(header.data_size), header.data_size, known_seed);
return decrypt_gci_or_vms_v2_data_section<true>(
r.getv(header.data_size), header.data_size, known_seed, true);
} else if (header.embedded_seed != 0) {
return decrypt_gci_data_section(
r.getv(header.data_size), header.data_size, header.embedded_seed);
return decrypt_gci_or_vms_v2_data_section<true>(
r.getv(header.data_size), header.data_size, header.embedded_seed, true);
} else {
if (find_seed_num_threads < 0) {
@@ -637,13 +732,13 @@ string Quest::decode_gci(
if (find_seed_num_threads == 0) {
find_seed_num_threads = thread::hardware_concurrency();
}
return find_seed_and_decrypt_gci_data_section(
return find_seed_and_decrypt_gci_or_vms_v2_data_section<true>(
r.getv(header.data_size), header.data_size, find_seed_num_threads);
}
} else { // Unencrypted GCI format
r.skip(sizeof(PSOGCIFileEncryptedHeader));
string compressed_data = r.readx(header.data_size - sizeof(PSOGCIFileEncryptedHeader));
r.skip(sizeof(PSOGCIOrVMSFileEncryptedHeader));
string compressed_data = r.readx(header.data_size - sizeof(PSOGCIOrVMSFileEncryptedHeader));
size_t decompressed_bytes = prs_decompress_size(compressed_data);
size_t expected_decompressed_bytes = encrypted_header.decompressed_size - 8;
@@ -692,6 +787,38 @@ string Quest::decode_gci(
}
}
string Quest::decode_vms(
const string& filename, ssize_t find_seed_num_threads, int64_t known_seed) {
string data = load_file(filename);
StringReader r(data);
const auto& header = r.get<PSOVMSFileHeader>();
if (!header.checksum_correct()) {
throw runtime_error("VMS file unencrypted header checksum is incorrect");
}
r.skip(header.num_icons * 0x200);
const void* data_section = r.getv(header.data_size);
try {
return decrypt_vms_v1_data_section(data_section, header.data_size);
} catch (const exception& e) { }
if (known_seed >= 0) {
return decrypt_gci_or_vms_v2_data_section<false>(
data_section, header.data_size, known_seed, true);
} else {
if (find_seed_num_threads < 0) {
throw runtime_error("GCI file appears to be encrypted");
}
if (find_seed_num_threads == 0) {
find_seed_num_threads = thread::hardware_concurrency();
}
return find_seed_and_decrypt_gci_or_vms_v2_data_section<false>(
data_section, header.data_size, find_seed_num_threads);
}
}
string Quest::decode_dlq(const string& filename) {
uint32_t decompressed_size;
uint32_t key;
@@ -731,6 +858,7 @@ static pair<string, string> decode_qst_t(FILE* f) {
string internal_dat_filename;
uint32_t bin_file_size = 0;
uint32_t dat_file_size = 0;
Quest::FileFormat subformat = Quest::FileFormat::QST; // Stand-in for unknown
while (!r.eof()) {
// Handle BB's implicit 8-byte command alignment
static constexpr size_t alignment = sizeof(HeaderT);
@@ -741,7 +869,22 @@ static pair<string, string> decode_qst_t(FILE* f) {
}
const auto& header = r.get<HeaderT>();
if (header.command == 0x44) {
if (header.command == 0x44 || header.command == 0x13) {
if (subformat == Quest::FileFormat::QST) {
subformat = Quest::FileFormat::BIN_DAT;
} else if (subformat != Quest::FileFormat::BIN_DAT) {
throw runtime_error("QST file contains mixed download and non-download commands");
}
} else if (header.command == 0xA6 || header.command == 0xA7) {
if (subformat == Quest::FileFormat::QST) {
subformat = Quest::FileFormat::BIN_DAT_DLQ;
} else if (subformat != Quest::FileFormat::BIN_DAT_DLQ) {
throw runtime_error("QST file contains mixed download and non-download commands");
}
}
if (header.command == 0x44 || header.command == 0xA6) {
if (header.size != sizeof(HeaderT) + sizeof(OpenFileT)) {
throw runtime_error("qst open file command has incorrect size");
}
@@ -768,7 +911,7 @@ static pair<string, string> decode_qst_t(FILE* f) {
throw runtime_error("qst contains non-bin, non-dat file");
}
} else if (header.command == 0x13) {
} else if (header.command == 0x13 || header.command == 0xA7) {
if (header.size != sizeof(HeaderT) + sizeof(S_WriteFile_13_A7)) {
throw runtime_error("qst write file command has incorrect size");
}
@@ -807,25 +950,30 @@ static pair<string, string> decode_qst_t(FILE* f) {
throw runtime_error("dat file does not match expected size");
}
if (subformat == Quest::FileFormat::BIN_DAT_DLQ) {
bin_contents = Quest::decode_dlq(bin_contents);
dat_contents = Quest::decode_dlq(dat_contents);
}
return make_pair(bin_contents, dat_contents);
}
pair<string, string> Quest::decode_qst(const string& filename) {
auto f = fopen_unique(filename, "rb");
// qst files start with an open file command, but the format differs depending
// QST files start with an open file command, but the format differs depending
// on the PSO version that the qst file is for. We can detect the format from
// the first 4 bytes in the file:
// - BB: 58 00 44 00
// - PC: 3C ?? 44 00
// - DC/V3: 44 ?? 3C 00
// - BB: 58 00 44 00 or 58 00 A6 00
// - PC: 3C 00 44 ?? or 3C 00 A6 ??
// - DC/V3: 44 ?? 3C 00 or A6 ?? 3C 00
uint32_t signature = freadx<be_uint32_t>(f.get());
fseek(f.get(), 0, SEEK_SET);
if (signature == 0x58004400) {
if (signature == 0x58004400 || signature == 0x5800A600) {
return decode_qst_t<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(f.get());
} else if ((signature & 0xFF00FFFF) == 0x3C004400) {
} else if ((signature & 0xFFFFFF00) == 0x3C004400 || (signature & 0xFFFFFF00) == 0x3C00A600) {
return decode_qst_t<PSOCommandHeaderPC, S_OpenFile_PC_V3_44_A6>(f.get());
} else if ((signature & 0xFF00FFFF) == 0x44003C00) {
} else if ((signature & 0xFF00FFFF) == 0x44003C00 || (signature & 0xFF00FFFF) == 0xA6003C00) {
return decode_qst_t<PSOCommandHeaderDCV3, S_OpenFile_PC_V3_44_A6>(f.get());
} else {
throw runtime_error("invalid qst file format");
@@ -851,6 +999,7 @@ QuestIndex::QuestIndex(const string& directory) : directory(directory) {
if (ends_with(filename, ".bin") ||
ends_with(filename, ".bind") ||
ends_with(filename, ".bin.gci") ||
ends_with(filename, ".bin.vms") ||
ends_with(filename, ".bin.dlq") ||
ends_with(filename, ".mnm") ||
ends_with(filename, ".mnmd") ||
+5
View File
@@ -40,6 +40,7 @@ public:
BIN_DAT = 0,
BIN_DAT_UNCOMPRESSED,
BIN_DAT_GCI,
BIN_DAT_VMS,
BIN_DAT_DLQ,
QST,
};
@@ -75,6 +76,10 @@ public:
const std::string& filename,
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1);
static std::string decode_vms(
const std::string& filename,
ssize_t find_seed_num_threads = -1,
int64_t known_seed = -1);
static std::string decode_dlq(const std::string& filename);
static std::pair<std::string, std::string> decode_qst(const std::string& filename);
+1
View File
@@ -8,6 +8,7 @@ using namespace std;
RareItemSet::RareItemSet(shared_ptr<const string> data) : data(data) {
// TODO: Actually parse the GSL here instead of treating it as a blob
if (this->data->size() != sizeof(Table) * 10 * 4 * 3) {
throw runtime_error("data file size is incorrect");
}
+1285 -647
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -6,8 +6,21 @@
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,
uint8_t episode,
uint8_t difficulty,
uint32_t flags,
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);
+253 -227
View File
@@ -63,6 +63,12 @@ const CmdT& check_size_sc(
static const unordered_set<uint8_t> watcher_subcommands({
0x07, // Symbol chat
0x74, // Word select
0xBD, // Word select during battle (with private_flags)
});
static void forward_subcommand(shared_ptr<Lobby> l, shared_ptr<Client> c,
uint8_t command, uint8_t flag, const void* data, size_t size) {
@@ -98,8 +104,15 @@ static void forward_subcommand(shared_ptr<Lobby> l, shared_ptr<Client> c,
send_command_excluding_client(l, c, command, flag, data, size);
}
for (const auto& watcher_lobby : l->watcher_lobbies) {
forward_subcommand(watcher_lobby, c, command, flag, data, size);
// Before battle, forward only chat commands to watcher lobbies; during
// battle, forward everything to watcher lobbies.
if (size &&
(watcher_subcommands.count(*reinterpret_cast<const uint8_t*>(data) ||
(l->ep3_server_base &&
l->ep3_server_base->server->setup_phase != Episode3::SetupPhase::REGISTRATION)))) {
for (const auto& watcher_lobby : l->watcher_lobbies) {
forward_subcommand(watcher_lobby, c, command, flag, data, size);
}
}
if (l->battle_record && l->battle_record->battle_in_progress()) {
@@ -118,7 +131,7 @@ static void forward_subcommand(shared_ptr<Lobby> l, shared_ptr<Client> c,
static void on_subcommand_invalid(shared_ptr<ServerState>,
static void on_invalid(shared_ptr<ServerState>,
shared_ptr<Lobby>, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_UnusedHeader>(
@@ -131,7 +144,7 @@ static void on_subcommand_invalid(shared_ptr<ServerState>,
}
}
static void on_subcommand_unimplemented(shared_ptr<ServerState>,
static void on_unimplemented(shared_ptr<ServerState>,
shared_ptr<Lobby>, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_UnusedHeader>(
@@ -146,14 +159,14 @@ static void on_subcommand_unimplemented(shared_ptr<ServerState>,
static void on_subcommand_forward_check_size(shared_ptr<ServerState>,
static void on_forward_check_size(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
check_size_sc<G_UnusedHeader>(data, sizeof(G_UnusedHeader), 0xFFFF);
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_forward_check_game(shared_ptr<ServerState>,
static void on_forward_check_game(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (!l->is_game()) {
@@ -162,7 +175,7 @@ static void on_subcommand_forward_check_game(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_forward_check_game_loading(shared_ptr<ServerState>,
static void on_forward_check_game_loading(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (!l->is_game() || !l->any_client_loading()) {
@@ -171,7 +184,7 @@ static void on_subcommand_forward_check_game_loading(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_forward_check_size_client(shared_ptr<ServerState>,
static void on_forward_check_size_client(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_ClientIDHeader>(
@@ -182,7 +195,7 @@ static void on_subcommand_forward_check_size_client(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_forward_check_size_game(shared_ptr<ServerState>,
static void on_forward_check_size_game(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
check_size_sc<G_UnusedHeader>(data, sizeof(G_UnusedHeader), 0xFFFF);
@@ -192,7 +205,7 @@ static void on_subcommand_forward_check_size_game(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_forward_check_size_ep3_lobby(shared_ptr<ServerState>,
static void on_forward_check_size_ep3_lobby(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
check_size_sc<G_UnusedHeader>(data, sizeof(G_UnusedHeader), 0xFFFF);
@@ -202,7 +215,7 @@ static void on_subcommand_forward_check_size_ep3_lobby(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_forward_check_size_ep3_game(shared_ptr<ServerState>,
static void on_forward_check_size_ep3_game(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
check_size_sc<G_UnusedHeader>(data, sizeof(G_UnusedHeader), 0xFFFF);
@@ -217,10 +230,10 @@ static void on_subcommand_forward_check_size_ep3_game(shared_ptr<ServerState>,
////////////////////////////////////////////////////////////////////////////////
// Ep3 subcommands
static void on_subcommand_ep3_battle_subs(shared_ptr<ServerState>,
static void on_ep3_battle_subs(shared_ptr<ServerState> s,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& orig_data) {
check_size_sc<G_CardBattleCommandHeader>(
const auto& header = check_size_sc<G_CardBattleCommandHeader>(
orig_data, sizeof(G_CardBattleCommandHeader), 0xFFFF);
if (!l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) {
return;
@@ -229,11 +242,25 @@ static void on_subcommand_ep3_battle_subs(shared_ptr<ServerState>,
string data = orig_data;
set_mask_for_ep3_game_command(data.data(), data.size(), 0);
uint8_t mask_key = 0;
while (!mask_key) {
mask_key = random_object<uint8_t>();
if (header.subcommand == 0xB5) {
if (header.subsubcommand == 0x1A) {
return;
} else if (header.subsubcommand == 0x36) {
const auto& cmd = check_size_t<G_Unknown_GC_Ep3_6xB5x36>(data);
if (l->is_game() && (cmd.unknown_a1 >= 4)) {
return;
}
}
}
set_mask_for_ep3_game_command(data.data(), data.size(), mask_key);
if (!(s->ep3_data_index->behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
uint8_t mask_key = 0;
while (!mask_key) {
mask_key = random_object<uint8_t>();
}
set_mask_for_ep3_game_command(data.data(), data.size(), mask_key);
}
forward_subcommand(l, c, command, flag, data);
}
@@ -242,7 +269,7 @@ static void on_subcommand_ep3_battle_subs(shared_ptr<ServerState>,
////////////////////////////////////////////////////////////////////////////////
// Chat commands and the like
static void on_subcommand_send_guild_card(shared_ptr<ServerState>,
static void on_send_guild_card(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (!command_is_private(command) || !l || (flag >= l->max_clients) ||
@@ -279,7 +306,7 @@ static void on_subcommand_send_guild_card(shared_ptr<ServerState>,
}
// client sends a symbol chat
static void on_subcommand_symbol_chat(shared_ptr<ServerState>,
static void on_symbol_chat(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_SymbolChat_6x07>(data);
@@ -291,7 +318,7 @@ static void on_subcommand_symbol_chat(shared_ptr<ServerState>,
}
// client sends a word select chat
static void on_subcommand_word_select(shared_ptr<ServerState>,
static void on_word_select(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_WordSelect_6x74>(data);
@@ -304,7 +331,7 @@ static void on_subcommand_word_select(shared_ptr<ServerState>,
}
// client is done loading into a lobby (we use this to trigger arrow updates)
static void on_subcommand_set_player_visibility(shared_ptr<ServerState>,
static void on_set_player_visibility(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_SetPlayerVisibility_6x22_6x23>(data);
@@ -323,7 +350,7 @@ static void on_subcommand_set_player_visibility(shared_ptr<ServerState>,
////////////////////////////////////////////////////////////////////////////////
// Game commands used by cheat mechanisms
static void on_subcommand_change_area(shared_ptr<ServerState>,
static void on_change_area(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_InterLevelWarp_6x21>(data);
@@ -335,7 +362,7 @@ static void on_subcommand_change_area(shared_ptr<ServerState>,
}
// when a player is hit by an enemy, heal them if infinite HP is enabled
static void on_subcommand_hit_by_enemy(shared_ptr<ServerState>,
static void on_hit_by_enemy(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_ClientIDHeader>(data, sizeof(G_ClientIDHeader), 0xFFFF);
@@ -343,13 +370,13 @@ static void on_subcommand_hit_by_enemy(shared_ptr<ServerState>,
return;
}
forward_subcommand(l, c, command, flag, data);
if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->infinite_hp) {
if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->options.infinite_hp) {
send_player_stats_change(l, c, PlayerStatsChange::ADD_HP, 2550);
}
}
// when a player casts a tech, restore TP if infinite TP is enabled
static void on_subcommand_cast_technique_finished(shared_ptr<ServerState>,
static void on_cast_technique_finished(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_CastTechniqueComplete_6x48>(data);
@@ -357,12 +384,12 @@ static void on_subcommand_cast_technique_finished(shared_ptr<ServerState>,
return;
}
forward_subcommand(l, c, command, flag, data);
if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->infinite_tp) {
if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->options.infinite_tp) {
send_player_stats_change(l, c, PlayerStatsChange::ADD_TP, 255);
}
}
static void on_subcommand_attack_finished(shared_ptr<ServerState> s,
static void on_attack_finished(shared_ptr<ServerState> s,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_AttackFinished_6x46>(data,
@@ -371,10 +398,10 @@ static void on_subcommand_attack_finished(shared_ptr<ServerState> s,
if (cmd.count > allowed_count) {
throw runtime_error("invalid attack finished command");
}
on_subcommand_forward_check_size_client(s, l, c, command, flag, data);
on_forward_check_size_client(s, l, c, command, flag, data);
}
static void on_subcommand_cast_technique(shared_ptr<ServerState> s,
static void on_cast_technique(shared_ptr<ServerState> s,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_CastTechnique_6x47>(data,
@@ -383,10 +410,10 @@ static void on_subcommand_cast_technique(shared_ptr<ServerState> s,
if (cmd.target_count > allowed_count) {
throw runtime_error("invalid cast technique command");
}
on_subcommand_forward_check_size_client(s, l, c, command, flag, data);
on_forward_check_size_client(s, l, c, command, flag, data);
}
static void on_subcommand_subtract_pb_energy(shared_ptr<ServerState> s,
static void on_subtract_pb_energy(shared_ptr<ServerState> s,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_SubtractPBEnergy_6x49>(data,
@@ -395,10 +422,10 @@ static void on_subcommand_subtract_pb_energy(shared_ptr<ServerState> s,
if (cmd.entry_count > allowed_count) {
throw runtime_error("invalid subtract PB energy command");
}
on_subcommand_forward_check_size_client(s, l, c, command, flag, data);
on_forward_check_size_client(s, l, c, command, flag, data);
}
static void on_subcommand_switch_state_changed(shared_ptr<ServerState>,
static void on_switch_state_changed(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
auto& cmd = check_size_t<G_SwitchStateChanged_6x05>(data);
@@ -407,7 +434,7 @@ static void on_subcommand_switch_state_changed(shared_ptr<ServerState>,
}
forward_subcommand(l, c, command, flag, data);
if (cmd.flags && cmd.header.object_id != 0xFFFF) {
if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->switch_assist &&
if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->options.switch_assist &&
(c->last_switch_enabled_command.header.subcommand == 0x05)) {
c->log.info("[Switch assist] Replaying previous enable command");
forward_subcommand(l, c, command, flag, &c->last_switch_enabled_command,
@@ -421,7 +448,7 @@ static void on_subcommand_switch_state_changed(shared_ptr<ServerState>,
////////////////////////////////////////////////////////////////////////////////
template <typename CmdT>
void on_subcommand_movement(shared_ptr<ServerState>,
void on_movement(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<CmdT>(data);
@@ -439,7 +466,7 @@ void on_subcommand_movement(shared_ptr<ServerState>,
////////////////////////////////////////////////////////////////////////////////
// Item commands
static void on_subcommand_player_drop_item(shared_ptr<ServerState>,
static void on_player_drop_item(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_DropItem_6x2A>(data);
@@ -449,8 +476,8 @@ static void on_subcommand_player_drop_item(shared_ptr<ServerState>,
}
if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) {
l->add_item(c->game_data.player()->remove_item(cmd.item_id, 0), cmd.area,
cmd.x, cmd.z);
auto item = c->game_data.player()->remove_item(cmd.item_id, 0, c->version() != GameVersion::BB);
l->add_item(item, cmd.area, cmd.x, cmd.z);
l->log.info("Player %hu dropped item %08" PRIX32 " at %hu:(%g, %g)",
cmd.header.client_id.load(), cmd.item_id.load(), cmd.area.load(),
@@ -461,7 +488,7 @@ static void on_subcommand_player_drop_item(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_create_inventory_item(shared_ptr<ServerState>,
static void on_create_inventory_item(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_CreateInventoryItem_DC_6x2B>(data,
@@ -491,7 +518,7 @@ static void on_subcommand_create_inventory_item(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_drop_partial_stack(shared_ptr<ServerState>,
static void on_drop_partial_stack(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_DropStackedItem_DC_6x5D>(data,
@@ -522,7 +549,7 @@ static void on_subcommand_drop_partial_stack(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_drop_partial_stack_bb(shared_ptr<ServerState>,
static void on_drop_partial_stack_bb(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (l->version == GameVersion::BB) {
@@ -536,7 +563,8 @@ static void on_subcommand_drop_partial_stack_bb(shared_ptr<ServerState>,
throw logic_error("item tracking not enabled in BB game");
}
auto item = c->game_data.player()->remove_item(cmd.item_id, cmd.amount);
auto item = c->game_data.player()->remove_item(
cmd.item_id, cmd.amount, c->version() != GameVersion::BB);
// if a stack was split, the original item still exists, so the dropped item
// needs a new ID. remove_item signals this by returning an item with id=-1
@@ -563,7 +591,7 @@ static void on_subcommand_drop_partial_stack_bb(shared_ptr<ServerState>,
}
}
static void on_subcommand_buy_shop_item(shared_ptr<ServerState>,
static void on_buy_shop_item(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_BuyShopItem_6x5E>(data);
@@ -590,7 +618,7 @@ static void on_subcommand_buy_shop_item(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_box_or_enemy_item_drop(shared_ptr<ServerState>,
static void on_box_or_enemy_item_drop(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_DropItem_DC_6x5F>(data,
@@ -603,20 +631,21 @@ static void on_subcommand_box_or_enemy_item_drop(shared_ptr<ServerState>,
return;
}
PlayerInventoryItem item;
item.present = 1;
item.flags = 0;
item.data = cmd.data;
l->add_item(item, cmd.area, cmd.x, cmd.z);
l->log.info("Leader created ground item %08" PRIX32 " at %hhu:(%g, %g)",
item.data.id.load(), cmd.area, cmd.x.load(), cmd.z.load());
if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) {
PlayerInventoryItem item;
item.present = 1;
item.flags = 0;
item.data = cmd.data;
l->add_item(item, cmd.area, cmd.x, cmd.z);
l->log.info("Leader created ground item %08" PRIX32 " at %hhu:(%g, %g)",
item.data.id.load(), cmd.area, cmd.x.load(), cmd.z.load());
}
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_pick_up_item(shared_ptr<ServerState>,
static void on_pick_up_item(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
auto& cmd = check_size_sc<G_PickUpItem_6x59>(data);
@@ -644,7 +673,7 @@ static void on_subcommand_pick_up_item(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_pick_up_item_request(shared_ptr<ServerState>,
static void on_pick_up_item_request(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
// This is handled by the server on BB, and by the leader on other versions
@@ -672,7 +701,7 @@ static void on_subcommand_pick_up_item_request(shared_ptr<ServerState>,
}
}
static void on_subcommand_equip_unequip_item(shared_ptr<ServerState>,
static void on_equip_unequip_item(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_EquipOrUnequipItem_6x25_6x26>(data);
@@ -697,7 +726,7 @@ static void on_subcommand_equip_unequip_item(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_use_item(shared_ptr<ServerState>,
static void on_use_item(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_UseItem_6x27>(data);
@@ -718,11 +747,11 @@ static void on_subcommand_use_item(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_open_shop_bb_or_ep3_battle_subs(shared_ptr<ServerState> s,
static void on_open_shop_bb_or_ep3_battle_subs(shared_ptr<ServerState> s,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (l->flags & Lobby::Flag::EPISODE_3_ONLY) {
on_subcommand_ep3_battle_subs(s, l, c, command, flag, data);
on_ep3_battle_subs(s, l, c, command, flag, data);
} else if (!l->common_item_creator.get()) {
throw runtime_error("received shop subcommand without item creator present");
@@ -753,7 +782,7 @@ static void on_subcommand_open_shop_bb_or_ep3_battle_subs(shared_ptr<ServerState
}
}
static void on_subcommand_open_bank_bb_or_card_trade_counter_ep3(shared_ptr<ServerState>,
static void on_open_bank_bb_or_card_trade_counter_ep3(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag, const string& data) {
if ((l->version == GameVersion::BB) && l->is_game()) {
send_bank(c);
@@ -762,7 +791,7 @@ static void on_subcommand_open_bank_bb_or_card_trade_counter_ep3(shared_ptr<Serv
}
}
static void on_subcommand_bank_action_bb(shared_ptr<ServerState>,
static void on_bank_action_bb(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t, uint8_t, const string& data) {
if (l->version == GameVersion::BB) {
const auto& cmd = check_size_sc<G_BankAction_BB_6xBD>(data);
@@ -786,7 +815,8 @@ static void on_subcommand_bank_action_bb(shared_ptr<ServerState>,
c->game_data.player()->bank.meseta += cmd.meseta_amount;
c->game_data.player()->disp.meseta -= cmd.meseta_amount;
} else { // item
auto item = c->game_data.player()->remove_item(cmd.item_id, cmd.item_amount);
auto item = c->game_data.player()->remove_item(
cmd.item_id, cmd.item_amount, c->version() != GameVersion::BB);
c->game_data.player()->bank.add_item(item);
send_destroy_item(l, c, cmd.item_id, cmd.item_amount);
}
@@ -811,7 +841,7 @@ static void on_subcommand_bank_action_bb(shared_ptr<ServerState>,
}
}
static void on_subcommand_sort_inventory_bb(shared_ptr<ServerState>,
static void on_sort_inventory_bb(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t, uint8_t,
const string& data) {
if (l->version == GameVersion::BB) {
@@ -854,13 +884,8 @@ static bool drop_item(
PlayerInventoryItem item;
// If there's an override item set (via the $item command), use that item code
if (l->next_drop_item.data.data1d[0]) {
item = l->next_drop_item;
l->next_drop_item.clear();
// If the game is BB, run the rare + common drop logic
} else if (l->version == GameVersion::BB) {
if (l->version == GameVersion::BB) {
if (!l->common_item_creator.get()) {
throw runtime_error("received box drop subcommand without item creator present");
}
@@ -905,8 +930,8 @@ static bool drop_item(
}
}
// If the game is not BB and there's no override item, forward the request to
// the leader instead of generating the item drop command
// If the game is not BB, forward the request to the leader instead of
// generating the item drop command
} else {
return false;
}
@@ -920,7 +945,7 @@ static bool drop_item(
return true;
}
static void on_subcommand_enemy_drop_item_request(shared_ptr<ServerState> s,
static void on_enemy_drop_item_request(shared_ptr<ServerState> s,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (!l->is_game()) {
@@ -935,7 +960,7 @@ static void on_subcommand_enemy_drop_item_request(shared_ptr<ServerState> s,
}
}
static void on_subcommand_box_drop_item_request(shared_ptr<ServerState> s,
static void on_box_drop_item_request(shared_ptr<ServerState> s,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (!l->is_game()) {
@@ -948,7 +973,7 @@ static void on_subcommand_box_drop_item_request(shared_ptr<ServerState> s,
}
}
static void on_subcommand_phase_setup(shared_ptr<ServerState>,
static void on_phase_setup(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (c->version() == GameVersion::DC || c->version() == GameVersion::PC) {
@@ -1003,7 +1028,7 @@ static void on_subcommand_phase_setup(shared_ptr<ServerState>,
}
// enemy hit by player
static void on_subcommand_enemy_hit(shared_ptr<ServerState>,
static void on_enemy_hit(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (l->version == GameVersion::BB) {
@@ -1026,7 +1051,7 @@ static void on_subcommand_enemy_hit(shared_ptr<ServerState>,
forward_subcommand(l, c, command, flag, data);
}
static void on_subcommand_enemy_killed(shared_ptr<ServerState> s,
static void on_enemy_killed(shared_ptr<ServerState> s,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
forward_subcommand(l, c, command, flag, data);
@@ -1096,7 +1121,7 @@ static void on_subcommand_enemy_killed(shared_ptr<ServerState> s,
}
}
static void on_subcommand_destroy_inventory_item(shared_ptr<ServerState>,
static void on_destroy_inventory_item(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_DeleteInventoryItem_6x29>(data);
@@ -1107,7 +1132,8 @@ static void on_subcommand_destroy_inventory_item(shared_ptr<ServerState>,
return;
}
if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) {
c->game_data.player()->remove_item(cmd.item_id, cmd.amount);
c->game_data.player()->remove_item(
cmd.item_id, cmd.amount, c->version() != GameVersion::BB);
l->log.info("Inventory item %hu:%08" PRIX32 " destroyed (%" PRIX32 " of them)",
cmd.header.client_id.load(), cmd.item_id.load(), cmd.amount.load());
c->game_data.player()->print_inventory(stderr);
@@ -1115,7 +1141,7 @@ static void on_subcommand_destroy_inventory_item(shared_ptr<ServerState>,
}
}
static void on_subcommand_destroy_ground_item(shared_ptr<ServerState>,
static void on_destroy_ground_item(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
const auto& cmd = check_size_sc<G_DestroyGroundItem_6x63>(data);
@@ -1129,7 +1155,7 @@ static void on_subcommand_destroy_ground_item(shared_ptr<ServerState>,
}
}
static void on_subcommand_identify_item_bb(shared_ptr<ServerState>,
static void on_identify_item_bb(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
if (l->version == GameVersion::BB) {
@@ -1163,7 +1189,7 @@ static void on_subcommand_identify_item_bb(shared_ptr<ServerState>,
}
}
static void on_subcommand_accept_identify_item_bb(shared_ptr<ServerState>,
static void on_accept_identify_item_bb(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t command, uint8_t flag,
const string& data) {
@@ -1189,7 +1215,7 @@ static void on_subcommand_accept_identify_item_bb(shared_ptr<ServerState>,
}
}
static void on_subcommand_sell_item_at_shop_bb(shared_ptr<ServerState>,
static void on_sell_item_at_shop_bb(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client>, uint8_t, uint8_t, const string&) {
if (l->version == GameVersion::BB) {
@@ -1206,7 +1232,7 @@ static void on_subcommand_sell_item_at_shop_bb(shared_ptr<ServerState>,
}
}
static void on_subcommand_buy_shop_item_bb(shared_ptr<ServerState>,
static void on_buy_shop_item_bb(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client>, uint8_t, uint8_t, const string&) {
if (l->version == GameVersion::BB) {
@@ -1223,7 +1249,7 @@ static void on_subcommand_buy_shop_item_bb(shared_ptr<ServerState>,
}
}
static void on_subcommand_medical_center_bb(shared_ptr<ServerState>,
static void on_medical_center_bb(shared_ptr<ServerState>,
shared_ptr<Lobby> l, shared_ptr<Client> c, uint8_t, uint8_t, const string&) {
if (l->version == GameVersion::BB) {
@@ -1244,214 +1270,214 @@ typedef void (*subcommand_handler_t)(shared_ptr<ServerState> s,
const string& data);
subcommand_handler_t subcommand_handlers[0x100] = {
/* 00 */ on_subcommand_invalid,
/* 00 */ on_invalid,
/* 01 */ nullptr,
/* 02 */ nullptr,
/* 03 */ nullptr,
/* 04 */ nullptr,
/* 05 */ on_subcommand_switch_state_changed,
/* 06 */ on_subcommand_send_guild_card,
/* 07 */ on_subcommand_symbol_chat,
/* 05 */ on_switch_state_changed,
/* 06 */ on_send_guild_card,
/* 07 */ on_symbol_chat,
/* 08 */ nullptr,
/* 09 */ nullptr,
/* 0A */ on_subcommand_enemy_hit,
/* 0B */ on_subcommand_forward_check_size_game,
/* 0C */ on_subcommand_forward_check_size_game, // Add condition (poison/slow/etc.)
/* 0D */ on_subcommand_forward_check_size_game, // Remove condition (poison/slow/etc.)
/* 0A */ on_enemy_hit,
/* 0B */ on_forward_check_size_game,
/* 0C */ on_forward_check_size_game, // Add condition (poison/slow/etc.)
/* 0D */ on_forward_check_size_game, // Remove condition (poison/slow/etc.)
/* 0E */ nullptr,
/* 0F */ nullptr,
/* 10 */ nullptr,
/* 11 */ nullptr,
/* 12 */ on_subcommand_forward_check_size_game, // Dragon actions
/* 13 */ on_subcommand_forward_check_size_game, // De Rol Le actions
/* 14 */ on_subcommand_forward_check_size_game,
/* 15 */ on_subcommand_forward_check_size_game, // Vol Opt actions
/* 16 */ on_subcommand_forward_check_size_game, // Vol Opt actions
/* 17 */ on_subcommand_forward_check_size_game,
/* 18 */ on_subcommand_forward_check_size_game,
/* 19 */ on_subcommand_forward_check_size_game, // Dark Falz actions
/* 12 */ on_forward_check_size_game, // Dragon actions
/* 13 */ on_forward_check_size_game, // De Rol Le actions
/* 14 */ on_forward_check_size_game,
/* 15 */ on_forward_check_size_game, // Vol Opt actions
/* 16 */ on_forward_check_size_game, // Vol Opt actions
/* 17 */ on_forward_check_size_game,
/* 18 */ on_forward_check_size_game,
/* 19 */ on_forward_check_size_game, // Dark Falz actions
/* 1A */ nullptr,
/* 1B */ nullptr,
/* 1C */ on_subcommand_forward_check_size_game,
/* 1C */ on_forward_check_size_game,
/* 1D */ nullptr,
/* 1E */ nullptr,
/* 1F */ on_subcommand_forward_check_size,
/* 20 */ on_subcommand_forward_check_size,
/* 21 */ on_subcommand_change_area, // Inter-level warp
/* 22 */ on_subcommand_forward_check_size_client, // Set player visibility
/* 23 */ on_subcommand_set_player_visibility, // Set player visibility
/* 24 */ on_subcommand_forward_check_size_game,
/* 25 */ on_subcommand_equip_unequip_item, // Equip item
/* 26 */ on_subcommand_equip_unequip_item, // Unequip item
/* 27 */ on_subcommand_use_item,
/* 28 */ on_subcommand_forward_check_size_game, // Feed MAG
/* 29 */ on_subcommand_destroy_inventory_item, // Delete item (via bank deposit / sale / feeding MAG)
/* 2A */ on_subcommand_player_drop_item,
/* 2B */ on_subcommand_create_inventory_item, // Create inventory item (e.g. from tekker or bank withdrawal)
/* 2C */ on_subcommand_forward_check_size, // Talk to NPC
/* 2D */ on_subcommand_forward_check_size, // Done talking to NPC
/* 1F */ on_forward_check_size,
/* 20 */ on_forward_check_size,
/* 21 */ on_change_area, // Inter-level warp
/* 22 */ on_forward_check_size_client, // Set player visibility
/* 23 */ on_set_player_visibility, // Set player visibility
/* 24 */ on_forward_check_size_game,
/* 25 */ on_equip_unequip_item, // Equip item
/* 26 */ on_equip_unequip_item, // Unequip item
/* 27 */ on_use_item,
/* 28 */ on_forward_check_size_game, // Feed MAG
/* 29 */ on_destroy_inventory_item, // Delete item (via bank deposit / sale / feeding MAG)
/* 2A */ on_player_drop_item,
/* 2B */ on_create_inventory_item, // Create inventory item (e.g. from tekker or bank withdrawal)
/* 2C */ on_forward_check_size, // Talk to NPC
/* 2D */ on_forward_check_size, // Done talking to NPC
/* 2E */ nullptr,
/* 2F */ on_subcommand_hit_by_enemy,
/* 30 */ on_subcommand_forward_check_size_game, // Level up
/* 31 */ on_subcommand_forward_check_size_game, // Medical center
/* 32 */ on_subcommand_forward_check_size_game, // Medical center
/* 33 */ on_subcommand_forward_check_size_game, // Moon atomizer/Reverser
/* 2F */ on_hit_by_enemy,
/* 30 */ on_forward_check_size_game, // Level up
/* 31 */ on_forward_check_size_game, // Medical center
/* 32 */ on_forward_check_size_game, // Medical center
/* 33 */ on_forward_check_size_game, // Moon atomizer/Reverser
/* 34 */ nullptr,
/* 35 */ nullptr,
/* 36 */ on_subcommand_forward_check_game,
/* 37 */ on_subcommand_forward_check_size_game, // Photon blast
/* 36 */ on_forward_check_game,
/* 37 */ on_forward_check_size_game, // Photon blast
/* 38 */ nullptr,
/* 39 */ on_subcommand_forward_check_size_game, // Photon blast ready
/* 3A */ on_subcommand_forward_check_size_game,
/* 3B */ on_subcommand_forward_check_size,
/* 39 */ on_forward_check_size_game, // Photon blast ready
/* 3A */ on_forward_check_size_game,
/* 3B */ on_forward_check_size,
/* 3C */ nullptr,
/* 3D */ nullptr,
/* 3E */ on_subcommand_movement<G_StopAtPosition_6x3E>, // Stop moving
/* 3F */ on_subcommand_movement<G_SetPosition_6x3F>, // Set position (e.g. when materializing after warp)
/* 40 */ on_subcommand_movement<G_WalkToPosition_6x40>, // Walk
/* 3E */ on_movement<G_StopAtPosition_6x3E>, // Stop moving
/* 3F */ on_movement<G_SetPosition_6x3F>, // Set position (e.g. when materializing after warp)
/* 40 */ on_movement<G_WalkToPosition_6x40>, // Walk
/* 41 */ nullptr,
/* 42 */ on_subcommand_movement<G_RunToPosition_6x42>, // Run
/* 43 */ on_subcommand_forward_check_size_client,
/* 44 */ on_subcommand_forward_check_size_client,
/* 45 */ on_subcommand_forward_check_size_client,
/* 46 */ on_subcommand_attack_finished,
/* 47 */ on_subcommand_cast_technique,
/* 48 */ on_subcommand_cast_technique_finished,
/* 49 */ on_subcommand_subtract_pb_energy,
/* 4A */ on_subcommand_forward_check_size_client,
/* 4B */ on_subcommand_hit_by_enemy,
/* 4C */ on_subcommand_hit_by_enemy,
/* 4D */ on_subcommand_forward_check_size_client,
/* 4E */ on_subcommand_forward_check_size_client,
/* 4F */ on_subcommand_forward_check_size_client,
/* 50 */ on_subcommand_forward_check_size_client,
/* 42 */ on_movement<G_RunToPosition_6x42>, // Run
/* 43 */ on_forward_check_size_client,
/* 44 */ on_forward_check_size_client,
/* 45 */ on_forward_check_size_client,
/* 46 */ on_attack_finished,
/* 47 */ on_cast_technique,
/* 48 */ on_cast_technique_finished,
/* 49 */ on_subtract_pb_energy,
/* 4A */ on_forward_check_size_client,
/* 4B */ on_hit_by_enemy,
/* 4C */ on_hit_by_enemy,
/* 4D */ on_forward_check_size_client,
/* 4E */ on_forward_check_size_client,
/* 4F */ on_forward_check_size_client,
/* 50 */ on_forward_check_size_client,
/* 51 */ nullptr,
/* 52 */ on_subcommand_forward_check_size, // Toggle shop/bank interaction
/* 53 */ on_subcommand_forward_check_size_game,
/* 52 */ on_forward_check_size, // Toggle shop/bank interaction
/* 53 */ on_forward_check_size_game,
/* 54 */ nullptr,
/* 55 */ on_subcommand_forward_check_size_client, // Intra-map warp
/* 56 */ on_subcommand_forward_check_size_client,
/* 57 */ on_subcommand_forward_check_size_client,
/* 58 */ on_subcommand_forward_check_size_game,
/* 59 */ on_subcommand_pick_up_item, // Item picked up
/* 5A */ on_subcommand_pick_up_item_request, // Request to pick up item
/* 55 */ on_forward_check_size_client, // Intra-map warp
/* 56 */ on_forward_check_size_client,
/* 57 */ on_forward_check_size_client,
/* 58 */ on_forward_check_size_client, // Begin playing emote
/* 59 */ on_pick_up_item, // Item picked up
/* 5A */ on_pick_up_item_request, // Request to pick up item
/* 5B */ nullptr,
/* 5C */ nullptr,
/* 5D */ on_subcommand_drop_partial_stack, // Drop meseta or stacked item
/* 5E */ on_subcommand_buy_shop_item, // Buy item at shop
/* 5F */ on_subcommand_box_or_enemy_item_drop, // Drop item from box/enemy
/* 60 */ on_subcommand_enemy_drop_item_request, // Request for item drop (handled by the server on BB)
/* 61 */ on_subcommand_forward_check_size_game, // Feed mag
/* 5D */ on_drop_partial_stack, // Drop meseta or stacked item
/* 5E */ on_buy_shop_item, // Buy item at shop
/* 5F */ on_box_or_enemy_item_drop, // Drop item from box/enemy
/* 60 */ on_enemy_drop_item_request, // Request for item drop (handled by the server on BB)
/* 61 */ on_forward_check_size_game, // Feed mag
/* 62 */ nullptr,
/* 63 */ on_subcommand_destroy_ground_item, // Destroy an item on the ground (used when too many items have been dropped)
/* 63 */ on_destroy_ground_item, // Destroy an item on the ground (used when too many items have been dropped)
/* 64 */ nullptr,
/* 65 */ nullptr,
/* 66 */ on_subcommand_forward_check_size_game, // Use star atomizer
/* 67 */ on_subcommand_forward_check_size_game, // Create enemy set
/* 68 */ on_subcommand_forward_check_size_game, // Telepipe/Ryuker
/* 69 */ on_subcommand_forward_check_size_game,
/* 6A */ on_subcommand_forward_check_size_game,
/* 6B */ on_subcommand_forward_check_game_loading,
/* 6C */ on_subcommand_forward_check_game_loading,
/* 6D */ on_subcommand_forward_check_game_loading,
/* 6E */ on_subcommand_forward_check_game_loading,
/* 6F */ on_subcommand_forward_check_game_loading,
/* 70 */ on_subcommand_forward_check_game_loading,
/* 71 */ on_subcommand_forward_check_game_loading,
/* 72 */ on_subcommand_forward_check_game_loading,
/* 73 */ on_subcommand_invalid,
/* 74 */ on_subcommand_word_select,
/* 75 */ on_subcommand_phase_setup,
/* 76 */ on_subcommand_forward_check_size_game, // Enemy killed
/* 77 */ on_subcommand_forward_check_size_game, // Sync quest data
/* 66 */ on_forward_check_size_game, // Use star atomizer
/* 67 */ on_forward_check_size_game, // Create enemy set
/* 68 */ on_forward_check_size_game, // Telepipe/Ryuker
/* 69 */ on_forward_check_size_game,
/* 6A */ on_forward_check_size_game,
/* 6B */ on_forward_check_game_loading,
/* 6C */ on_forward_check_game_loading,
/* 6D */ on_forward_check_game_loading,
/* 6E */ on_forward_check_game_loading,
/* 6F */ on_forward_check_game_loading,
/* 70 */ on_forward_check_game_loading,
/* 71 */ on_forward_check_game_loading,
/* 72 */ on_forward_check_game_loading,
/* 73 */ on_invalid,
/* 74 */ on_word_select,
/* 75 */ on_phase_setup,
/* 76 */ on_forward_check_size_game, // Enemy killed
/* 77 */ on_forward_check_size_game, // Sync quest data
/* 78 */ nullptr,
/* 79 */ on_subcommand_forward_check_size, // Lobby 14/15 soccer game
/* 79 */ on_forward_check_size, // Lobby 14/15 soccer game
/* 7A */ nullptr,
/* 7B */ nullptr,
/* 7C */ on_subcommand_forward_check_size_game,
/* 7D */ on_subcommand_forward_check_size_game,
/* 7C */ on_forward_check_size_game,
/* 7D */ on_forward_check_size_game,
/* 7E */ nullptr,
/* 7F */ nullptr,
/* 80 */ on_subcommand_forward_check_size_game, // Trigger trap
/* 80 */ on_forward_check_size_game, // Trigger trap
/* 81 */ nullptr,
/* 82 */ nullptr,
/* 83 */ on_subcommand_forward_check_size_game, // Place trap
/* 84 */ on_subcommand_forward_check_size_game,
/* 85 */ on_subcommand_forward_check_size_game,
/* 86 */ on_subcommand_forward_check_size_game, // Hit destructible wall
/* 87 */ nullptr,
/* 88 */ on_subcommand_forward_check_size_game,
/* 89 */ on_subcommand_forward_check_size_game,
/* 8A */ nullptr,
/* 83 */ on_forward_check_size_game, // Place trap
/* 84 */ on_forward_check_size_game,
/* 85 */ on_forward_check_size_game,
/* 86 */ on_forward_check_size_game, // Hit destructible wall
/* 87 */ on_forward_check_size_game, // Shrink character
/* 88 */ on_forward_check_size_game,
/* 89 */ on_forward_check_size_game,
/* 8A */ on_forward_check_size_game,
/* 8B */ nullptr,
/* 8C */ nullptr,
/* 8D */ on_subcommand_forward_check_size_client,
/* 8D */ on_forward_check_size_client,
/* 8E */ nullptr,
/* 8F */ nullptr,
/* 90 */ nullptr,
/* 91 */ on_subcommand_forward_check_size_game,
/* 91 */ on_forward_check_size_game,
/* 92 */ nullptr,
/* 93 */ on_subcommand_forward_check_size_game, // Timed switch activated
/* 94 */ on_subcommand_forward_check_size_game, // Warp (the $warp chat command is implemented using this)
/* 93 */ on_forward_check_size_game, // Timed switch activated
/* 94 */ on_forward_check_size_game, // Warp (the $warp chat command is implemented using this)
/* 95 */ nullptr,
/* 96 */ nullptr,
/* 97 */ nullptr,
/* 98 */ nullptr,
/* 99 */ nullptr,
/* 9A */ on_subcommand_forward_check_size_game, // Update player stat ($infhp/$inftp are implemented using this command)
/* 9A */ on_forward_check_size_game, // Update player stat ($infhp/$inftp are implemented using this command)
/* 9B */ nullptr,
/* 9C */ on_subcommand_forward_check_size_game,
/* 9C */ on_forward_check_size_game,
/* 9D */ nullptr,
/* 9E */ nullptr,
/* 9F */ on_subcommand_forward_check_size_game, // Gal Gryphon actions
/* A0 */ on_subcommand_forward_check_size_game, // Gal Gryphon actions
/* A1 */ on_subcommand_forward_check_size_game, // Part of revive process. Occurs right after revive command, function unclear.
/* A2 */ on_subcommand_box_drop_item_request, // Request for item drop from box (handled by server on BB)
/* A3 */ on_subcommand_forward_check_size_game, // Episode 2 boss actions
/* A4 */ on_subcommand_forward_check_size_game, // Olga Flow phase 1 actions
/* A5 */ on_subcommand_forward_check_size_game, // Olga Flow phase 2 actions
/* A6 */ on_subcommand_forward_check_size, // Trade proposal
/* 9F */ on_forward_check_size_game, // Gal Gryphon actions
/* A0 */ on_forward_check_size_game, // Gal Gryphon actions
/* A1 */ on_forward_check_size_game, // Part of revive process. Occurs right after revive command, function unclear.
/* A2 */ on_box_drop_item_request, // Request for item drop from box (handled by server on BB)
/* A3 */ on_forward_check_size_game, // Episode 2 boss actions
/* A4 */ on_forward_check_size_game, // Olga Flow phase 1 actions
/* A5 */ on_forward_check_size_game, // Olga Flow phase 2 actions
/* A6 */ on_forward_check_size, // Trade proposal
/* A7 */ nullptr,
/* A8 */ on_subcommand_forward_check_size_game, // Gol Dragon actions
/* A9 */ on_subcommand_forward_check_size_game, // Barba Ray actions
/* AA */ on_subcommand_forward_check_size_game, // Episode 2 boss actions
/* AB */ on_subcommand_forward_check_size_client, // Create lobby chair
/* A8 */ on_forward_check_size_game, // Gol Dragon actions
/* A9 */ on_forward_check_size_game, // Barba Ray actions
/* AA */ on_forward_check_size_game, // Episode 2 boss actions
/* AB */ on_forward_check_size_client, // Create lobby chair
/* AC */ nullptr,
/* AD */ on_subcommand_forward_check_size_game, // Olga Flow phase 2 subordinate boss actions
/* AE */ on_subcommand_forward_check_size_client,
/* AF */ on_subcommand_forward_check_size_client, // Turn in lobby chair
/* B0 */ on_subcommand_forward_check_size_client, // Move in lobby chair
/* AD */ on_forward_check_size_game, // Olga Flow phase 2 subordinate boss actions
/* AE */ on_forward_check_size_client,
/* AF */ on_forward_check_size_client, // Turn in lobby chair
/* B0 */ on_forward_check_size_client, // Move in lobby chair
/* B1 */ nullptr,
/* B2 */ nullptr,
/* B3 */ on_subcommand_ep3_battle_subs,
/* B4 */ on_subcommand_ep3_battle_subs,
/* B5 */ on_subcommand_open_shop_bb_or_ep3_battle_subs, // BB shop request
/* B3 */ on_ep3_battle_subs,
/* B4 */ on_ep3_battle_subs,
/* B5 */ on_open_shop_bb_or_ep3_battle_subs, // BB shop request
/* B6 */ nullptr, // BB shop contents (server->client only)
/* B7 */ on_subcommand_buy_shop_item_bb,
/* B8 */ on_subcommand_identify_item_bb,
/* B7 */ on_buy_shop_item_bb,
/* B8 */ on_identify_item_bb,
/* B9 */ nullptr,
/* BA */ on_subcommand_accept_identify_item_bb,
/* BB */ on_subcommand_open_bank_bb_or_card_trade_counter_ep3,
/* BC */ on_subcommand_forward_check_size_ep3_game, // BB bank contents (server->client only), Ep3 card trade sequence
/* BD */ on_subcommand_bank_action_bb,
/* BE */ on_subcommand_forward_check_size, // BB create inventory item (server->client only), Ep3 sound chat
/* BF */ on_subcommand_forward_check_size_ep3_lobby, // Ep3 change music, also BB give EXP (BB usage is server->client only)
/* C0 */ on_subcommand_sell_item_at_shop_bb,
/* BA */ on_accept_identify_item_bb,
/* BB */ on_open_bank_bb_or_card_trade_counter_ep3,
/* BC */ on_forward_check_size_ep3_game, // BB bank contents (server->client only), Ep3 card trade sequence
/* BD */ on_bank_action_bb,
/* BE */ on_forward_check_size, // BB create inventory item (server->client only), Ep3 sound chat
/* BF */ on_forward_check_size_ep3_lobby, // Ep3 change music, also BB give EXP (BB usage is server->client only)
/* C0 */ on_sell_item_at_shop_bb,
/* C1 */ nullptr,
/* C2 */ nullptr,
/* C3 */ on_subcommand_drop_partial_stack_bb, // Split stacked item - not sent if entire stack is dropped
/* C4 */ on_subcommand_sort_inventory_bb,
/* C5 */ on_subcommand_medical_center_bb,
/* C3 */ on_drop_partial_stack_bb, // Split stacked item - not sent if entire stack is dropped
/* C4 */ on_sort_inventory_bb,
/* C5 */ on_medical_center_bb,
/* C6 */ nullptr,
/* C7 */ nullptr,
/* C8 */ on_subcommand_enemy_killed,
/* C8 */ on_enemy_killed,
/* C9 */ nullptr,
/* CA */ nullptr,
/* CB */ nullptr,
/* CC */ nullptr,
/* CD */ nullptr,
/* CE */ nullptr,
/* CF */ on_subcommand_forward_check_size_game,
/* CF */ on_forward_check_size_game,
/* D0 */ nullptr,
/* D1 */ nullptr,
/* D2 */ nullptr,
@@ -1512,7 +1538,7 @@ void on_subcommand(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
if (fn) {
fn(s, l, c, command, flag, data);
} else {
on_subcommand_unimplemented(s, l, c, command, flag, data);
on_unimplemented(s, l, c, command, flag, data);
}
}
+80 -40
View File
@@ -13,7 +13,11 @@ using namespace std;
ReplaySession::Event::Event(Type type, uint64_t client_id, size_t line_num)
: type(type), client_id(client_id), complete(false), line_num(line_num) { }
: type(type),
client_id(client_id),
allow_size_disparity(false),
complete(false),
line_num(line_num) { }
string ReplaySession::Event::str() const {
string ret;
@@ -26,6 +30,9 @@ string ReplaySession::Event::str() const {
} else if (this->type == Type::RECEIVE) {
ret = string_printf("Event[%" PRIu64 ", RECEIVE %04zX", this->client_id, this->data.size());
}
if (this->allow_size_disparity) {
ret += ", size disparity allowed";
}
if (this->complete) {
ret += ", done";
}
@@ -212,15 +219,17 @@ void ReplaySession::check_for_password(shared_ptr<const Event> ev) const {
void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
auto version = this->clients.at(ev->client_id)->version;
void* cmd_data = ev->mask.data() + ((version == GameVersion::BB) ? 8 : 4);
size_t cmd_size = ev->mask.size() - ((version == GameVersion::BB) ? 8 : 4);
void* cmd_data = ev->data.data() + ((version == GameVersion::BB) ? 8 : 4);
size_t cmd_size = ev->data.size() - ((version == GameVersion::BB) ? 8 : 4);
void* mask_data = ev->mask.data() + ((version == GameVersion::BB) ? 8 : 4);
size_t mask_size = ev->mask.size() - ((version == GameVersion::BB) ? 8 : 4);
switch (version) {
case GameVersion::PATCH: {
const auto& header = check_size_t<PSOCommandHeaderPC>(
ev->data, sizeof(PSOCommandHeaderPC), 0xFFFF);
if (header.command == 0x02) {
auto& cmd_mask = check_size_t<S_ServerInit_Patch_02>(cmd_data, cmd_size);
auto& cmd_mask = check_size_t<S_ServerInit_Patch_02>(mask_data, mask_size);
cmd_mask.server_key = 0;
cmd_mask.client_key = 0;
}
@@ -243,55 +252,84 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
case 0x17:
case 0x91:
case 0x9B: {
auto& cmd_mask = check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>(
cmd_data, cmd_size, sizeof(S_ServerInitDefault_DC_PC_V3_02_17_91_9B), 0xFFFF);
cmd_mask.server_key = 0;
cmd_mask.client_key = 0;
auto& mask = check_size_t<S_ServerInitDefault_DC_PC_V3_02_17_91_9B>(
mask_data, mask_size, sizeof(S_ServerInitDefault_DC_PC_V3_02_17_91_9B), 0xFFFF);
mask.server_key = 0;
mask.client_key = 0;
break;
}
case 0x19: {
if (cmd_size == sizeof(S_ReconnectSplit_19)) {
auto& cmd_mask = check_size_t<S_ReconnectSplit_19>(cmd_data, cmd_size);
cmd_mask.pc_address = 0;
cmd_mask.gc_address = 0;
if (mask_size == sizeof(S_ReconnectSplit_19)) {
auto& mask = check_size_t<S_ReconnectSplit_19>(mask_data, mask_size);
mask.pc_address = 0;
mask.gc_address = 0;
} else {
auto& cmd_mask = check_size_t<S_Reconnect_19>(cmd_data, cmd_size);
cmd_mask.address = 0;
auto& mask = check_size_t<S_Reconnect_19>(mask_data, mask_size);
mask.address = 0;
}
break;
}
case 0x41: {
if (version == GameVersion::PC) {
auto& cmd_mask = check_size_t<S_GuildCardSearchResult_PC_41>(cmd_data, cmd_size);
cmd_mask.reconnect_command.address = 0;
auto& mask = check_size_t<S_GuildCardSearchResult_PC_41>(mask_data, mask_size);
mask.reconnect_command.address = 0;
} else if (version == GameVersion::BB) {
auto& cmd_mask = check_size_t<S_GuildCardSearchResult_BB_41>(cmd_data, cmd_size);
cmd_mask.reconnect_command.address = 0;
auto& mask = check_size_t<S_GuildCardSearchResult_BB_41>(mask_data, mask_size);
mask.reconnect_command.address = 0;
} else { // V3
auto& cmd_mask = check_size_t<S_GuildCardSearchResult_DC_V3_41>(cmd_data, cmd_size);
cmd_mask.reconnect_command.address = 0;
auto& mask = check_size_t<S_GuildCardSearchResult_DC_V3_41>(mask_data, mask_size);
mask.reconnect_command.address = 0;
}
break;
}
case 0x64: {
if (version == GameVersion::PC) {
auto& cmd_mask = check_size_t<S_JoinGame_PC_64>(cmd_data, cmd_size);
cmd_mask.variations.clear(0);
cmd_mask.rare_seed = 0;
auto& mask = check_size_t<S_JoinGame_PC_64>(mask_data, mask_size);
mask.variations.clear(0);
mask.rare_seed = 0;
} else { // V3
auto& cmd_mask = check_size_t<S_JoinGame_DC_GC_64>(cmd_data, cmd_size,
auto& mask = check_size_t<S_JoinGame_DC_GC_64>(mask_data, mask_size,
sizeof(S_JoinGame_DC_GC_64), sizeof(S_JoinGame_GC_Ep3_64));
cmd_mask.variations.clear(0);
cmd_mask.rare_seed = 0;
mask.variations.clear(0);
mask.rare_seed = 0;
}
break;
}
case 0xB1: {
for (size_t x = 8; x < ev->mask.size(); x++) {
for (size_t x = 4; x < ev->mask.size(); x++) {
ev->mask[x] = 0;
}
break;
}
case 0xC9: {
if (mask_size == 0xCC) {
auto& mask = check_size_t<G_ServerVersionStrings_GC_Ep3_6xB4x46>(
mask_data, mask_size);
mask.version_signature.clear(0);
mask.date_str1.clear(0);
mask.date_str2.clear(0);
}
break;
}
case 0x6C: {
if (version == GameVersion::GC && mask_size >= 0x14) {
const auto& cmd = check_size_t<G_MapList_GC_Ep3_6xB6x40>(
cmd_data, cmd_size, sizeof(G_MapList_GC_Ep3_6xB6x40), 0xFFFF);
if ((cmd.header.header.basic_header.subcommand == 0xB6) &&
(cmd.header.subsubcommand == 0x40)) {
check_size_t<PSOCommandHeaderDCV3>(ev->mask, sizeof(PSOCommandHeaderDCV3), 0xFFFF).size = 0;
auto& mask = check_size_t<G_MapList_GC_Ep3_6xB6x40>(
mask_data, mask_size, sizeof(G_MapList_GC_Ep3_6xB6x40), 0xFFFF);
mask.header.header.size = 0;
mask.compressed_data_size = 0;
ev->allow_size_disparity = true;
for (size_t z = sizeof(PSOCommandHeaderDCV3) + sizeof(G_MapList_GC_Ep3_6xB6x40); z < ev->mask.size(); z++) {
ev->mask[z] = 0;
}
}
}
break;
}
}
break;
}
@@ -300,21 +338,21 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
ev->data, sizeof(PSOCommandHeaderBB), 0xFFFF).command;
switch (command) {
case 0x0003: {
auto& cmd_mask = check_size_t<S_ServerInitDefault_BB_03_9B>(
cmd_data, cmd_size, sizeof(S_ServerInitDefault_BB_03_9B), 0xFFFF);
cmd_mask.server_key.clear(0);
cmd_mask.client_key.clear(0);
auto& mask = check_size_t<S_ServerInitDefault_BB_03_9B>(
mask_data, mask_size, sizeof(S_ServerInitDefault_BB_03_9B), 0xFFFF);
mask.server_key.clear(0);
mask.client_key.clear(0);
break;
}
case 0x0019: {
auto& cmd_mask = check_size_t<S_Reconnect_19>(cmd_data, cmd_size);
cmd_mask.address = 0;
auto& mask = check_size_t<S_Reconnect_19>(mask_data, mask_size);
mask.address = 0;
break;
}
case 0x0064: {
auto& cmd_mask = check_size_t<S_JoinGame_BB_64>(cmd_data, cmd_size);
cmd_mask.variations.clear(0);
cmd_mask.rare_seed = 0;
auto& mask = check_size_t<S_JoinGame_BB_64>(mask_data, mask_size);
mask.variations.clear(0);
mask.rare_seed = 0;
break;
}
case 0x00B1: {
@@ -324,8 +362,8 @@ void ReplaySession::apply_default_mask(shared_ptr<Event> ev) {
break;
}
case 0x00E6: {
auto& cmd_mask = check_size_t<S_ClientInit_BB_00E6>(cmd_data, cmd_size);
cmd_mask.team_id = 0;
auto& mask = check_size_t<S_ClientInit_BB_00E6>(mask_data, mask_size);
mask.team_id = 0;
break;
}
}
@@ -593,6 +631,8 @@ void ReplaySession::dispatch_on_error(Channel& ch, short events) {
void ReplaySession::on_command_received(
shared_ptr<Client> c, uint16_t command, uint32_t flag, string& data) {
// TODO: Use the iovec form of print_data here instead of
// prepend_command_header (which copies the string)
string full_command = prepend_command_header(
c->version, c->channel.crypt_in.get(), command, flag, data);
this->commands_received++;
@@ -604,14 +644,14 @@ void ReplaySession::on_command_received(
}
auto& ev = c->receive_events.front();
if (full_command.size() != ev->data.size()) {
if ((full_command.size() != ev->data.size()) && !ev->allow_size_disparity) {
replay_log.error("Expected command:");
print_data(stderr, ev->data, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::OFFSET_16_BITS);
replay_log.error("Received command:");
print_data(stderr, full_command, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::OFFSET_16_BITS);
throw runtime_error(string_printf("(ev-line %zu) received command sizes do not match", ev->line_num));
}
for (size_t x = 0; x < full_command.size(); x++) {
for (size_t x = 0; x < min<size_t>(full_command.size(), ev->data.size()); x++) {
if ((full_command[x] & ev->mask[x]) != (ev->data[x] & ev->mask[x])) {
replay_log.error("Expected command:");
print_data(stderr, ev->data, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::OFFSET_16_BITS);
+1
View File
@@ -42,6 +42,7 @@ private:
uint64_t client_id;
std::string data; // Only used for SEND and RECEIVE
std::string mask; // Only used for RECEIVE
bool allow_size_disparity;
bool complete;
size_t line_num;
+610 -69
View File
@@ -16,11 +16,15 @@
#include "Compression.hh"
#include "FileContentsCache.hh"
#include "Text.hh"
#include "StaticGameData.hh"
using namespace std;
extern const char* QUEST_BARRIER_DISCONNECT_HOOK_NAME;
extern const char* CARD_AUCTION_DISCONNECT_HOOK_NAME;
const unordered_set<uint32_t> v2_crypt_initial_client_commands({
0x00260088, // (17) DCNTE license check
0x00B0008B, // (02) DCNTE login
@@ -28,11 +32,11 @@ const unordered_set<uint32_t> v2_crypt_initial_client_commands({
0x00280090, // (17) DCv1 license check
0x00B00093, // (02) DCv1 login
0x01140093, // (02) DCv1 extended login
0x00E0009A, // (17) DCv2 license check
0x00CC009D, // (02) DCv2 login
0x00CC019D, // (02) DCv2 login (UDP off)
0x0130009D, // (02) DCv2 extended login
0x0130019D, // (02) DCv2 extended login (UDP off)
0x00E0009A, // (17) DCv2/GCNTE license check
0x00CC009D, // (02) DCv2/GCNTE login
0x00CC019D, // (02) DCv2/GCNTE login (UDP off)
0x0130009D, // (02) DCv2/GCNTE extended login
0x0130019D, // (02) DCv2/GCNTE extended login (UDP off)
// Note: PSO PC initial commands are not listed here because we don't use a
// detector encryption for PSO PC (instead, we use the split reconnect command
// to send PC to a different port).
@@ -72,6 +76,16 @@ void send_command_excluding_client(shared_ptr<Lobby> l, shared_ptr<Client> c,
}
}
void send_command_if_not_loading(shared_ptr<Lobby> l,
uint16_t command, uint32_t flag, const void* data, size_t size) {
for (auto& client : l->clients) {
if (!client || (client->flags & Client::Flag::LOADING)) {
continue;
}
send_command(client, command, flag, data, size);
}
}
void send_command(shared_ptr<Lobby> l, uint16_t command, uint32_t flag,
const void* data, size_t size) {
send_command_excluding_client(l, nullptr, command, flag, data, size);
@@ -284,11 +298,6 @@ void send_quest_open_file_t(
void send_quest_buffer_overflow(
shared_ptr<ServerState> s, shared_ptr<Client> c) {
// TODO: Figure out a way to share this state across sessions. Maybe we could
// e.g. modify send_1D to send a nonzero flag value, which we could use to
// know that the client already has this patch? Or just add another command in
// the login sequence?
// PSO Episode 3 USA doesn't natively support the B2 command, but we can add
// it back to the game with some tricky commands. For details on how this
// works, see system/ppc/Episode3USAQuestBufferOverflow.s.
@@ -314,8 +323,8 @@ void send_quest_buffer_overflow(
void send_function_call(
shared_ptr<Client> c,
shared_ptr<CompiledFunctionCode> code,
const std::unordered_map<std::string, uint32_t>& label_writes,
const std::string& suffix,
const unordered_map<string, uint32_t>& label_writes,
const string& suffix,
uint32_t checksum_addr,
uint32_t checksum_size) {
return send_function_call(
@@ -332,8 +341,8 @@ void send_function_call(
Channel& ch,
uint64_t client_flags,
shared_ptr<CompiledFunctionCode> code,
const std::unordered_map<std::string, uint32_t>& label_writes,
const std::string& suffix,
const unordered_map<string, uint32_t>& label_writes,
const string& suffix,
uint32_t checksum_addr,
uint32_t checksum_size) {
if (client_flags & Client::Flag::NO_SEND_FUNCTION_CALL) {
@@ -373,6 +382,9 @@ void send_function_call(
} else {
crypt.encrypt(data.data(), data.size());
}
w.write(data);
data = move(w.str());
}
}
@@ -491,16 +503,16 @@ void send_stream_file_index_bb(shared_ptr<Client> c) {
string key = "system/blueburst/" + filename;
auto cache_res = bb_stream_files_cache.get_or_load(key);
auto& e = entries.emplace_back();
e.size = cache_res.file->data.size();
e.size = cache_res.file->data->size();
// Computing the checksum can be slow, so we cache it along with the file
// data. If the cache result was just populated, then it may be different,
// so we always recompute the checksum in that case.
if (cache_res.generate_called) {
e.checksum = crc32(cache_res.file->data.data(), e.size);
e.checksum = crc32(cache_res.file->data->data(), e.size);
bb_stream_files_cache.replace_obj<uint32_t>(key + ".crc32", e.checksum);
} else {
auto compute_checksum = [&](const string&) -> uint32_t {
return crc32(cache_res.file->data.data(), e.size);
return crc32(cache_res.file->data->data(), e.size);
};
e.checksum = bb_stream_files_cache.get_obj<uint32_t>(key + ".crc32", compute_checksum).obj;
}
@@ -515,13 +527,13 @@ void send_stream_file_chunk_bb(shared_ptr<Client> c, uint32_t chunk_index) {
auto cache_result = bb_stream_files_cache.get("<BB stream file>", +[](const string&) -> string {
size_t bytes = 0;
for (const auto& name : stream_file_entries) {
bytes += bb_stream_files_cache.get_or_load("system/blueburst/" + name).file->data.size();
bytes += bb_stream_files_cache.get_or_load("system/blueburst/" + name).file->data->size();
}
string ret;
ret.reserve(bytes);
for (const auto& name : stream_file_entries) {
ret += bb_stream_files_cache.get_or_load("system/blueburst/" + name).file->data;
ret += *bb_stream_files_cache.get_or_load("system/blueburst/" + name).file->data;
}
return ret;
});
@@ -530,11 +542,11 @@ void send_stream_file_chunk_bb(shared_ptr<Client> c, uint32_t chunk_index) {
S_StreamFileChunk_BB_02EB chunk_cmd;
chunk_cmd.chunk_index = chunk_index;
size_t offset = sizeof(chunk_cmd.data) * chunk_index;
if (offset > contents.size()) {
if (offset > contents->size()) {
throw runtime_error("client requested chunk beyond end of stream file");
}
size_t bytes = min<size_t>(contents.size() - offset, sizeof(chunk_cmd.data));
memcpy(chunk_cmd.data, contents.data() + offset, bytes);
size_t bytes = min<size_t>(contents->size() - offset, sizeof(chunk_cmd.data));
memcpy(chunk_cmd.data, contents->data() + offset, bytes);
size_t cmd_size = offsetof(S_StreamFileChunk_BB_02EB, data) + bytes;
cmd_size = (cmd_size + 3) & ~3;
@@ -649,6 +661,17 @@ void send_message_box(shared_ptr<Client> c, const u16string& text) {
send_text(c->channel, command, text, true);
}
void send_ep3_timed_message_box(Channel& ch, uint32_t frames, const string& message) {
StringWriter w;
w.put<S_TimedMessageBoxHeader_GC_Ep3_EA>({frames});
add_color(w, message.data(), message.size());
w.put_u8(0);
while (w.size() & 3) {
w.put_u8(0);
}
ch.send(0xEA, 0x00, w.str());
}
void send_lobby_name(shared_ptr<Client> c, const u16string& text) {
send_text(c->channel, 0x8A, text, false);
}
@@ -670,7 +693,7 @@ void send_ship_info(Channel& ch, const u16string& text) {
send_header_text(ch, 0x11, 0, text, true);
}
void send_text_message(Channel& ch, const std::u16string& text) {
void send_text_message(Channel& ch, const u16string& text) {
send_header_text(ch, 0xB0, 0, text, true);
}
@@ -694,6 +717,22 @@ void send_text_message(shared_ptr<ServerState> s, const u16string& text) {
}
}
__attribute__((format(printf, 2, 3))) void send_ep3_text_message_printf(
shared_ptr<ServerState> s, const char* format, ...) {
va_list va;
va_start(va, format);
string buf = string_vprintf(format, va);
va_end(va);
u16string decoded = decode_sjis(buf);
for (auto& it : s->id_to_lobby) {
for (auto& c : it.second->clients) {
if (c && (c->flags & Client::Flag::IS_EPISODE_3)) {
send_text_message(c, decoded);
}
}
}
}
u16string prepare_chat_message(
GameVersion version,
const u16string& from_name,
@@ -713,8 +752,18 @@ u16string prepare_chat_message(
return data;
}
void send_chat_message(Channel& ch, const u16string& text) {
send_header_text(ch, 0x06, 0, text, false);
void send_chat_message(Channel& ch, const u16string& text, char private_flags) {
if (private_flags != 0) {
if (ch.version != GameVersion::GC) {
throw runtime_error("nonzero private_flags in non-GC chat message");
}
u16string effective_text;
effective_text.push_back(static_cast<char16_t>(private_flags));
effective_text += text;
send_header_text(ch, 0x06, 0, effective_text, false);
} else {
send_header_text(ch, 0x06, 0, text, false);
}
}
void send_chat_message(shared_ptr<Client> c, uint32_t from_guild_card_number,
@@ -826,9 +875,7 @@ void send_card_search_result_t(
shared_ptr<Client> c,
shared_ptr<Client> result,
shared_ptr<Lobby> result_lobby) {
static const vector<string> version_to_port_name({
"bb-lobby", "console-lobby", "pc-lobby", "console-lobby", "console-lobby", "bb-lobby"});
const auto& port_name = version_to_port_name.at(static_cast<size_t>(c->version()));
const auto& port_name = version_to_lobby_port_name.at(static_cast<size_t>(c->version()));
S_GuildCardSearchResult<CommandHeaderT, CharT> cmd;
cmd.player_tag = 0x00010000;
@@ -855,8 +902,8 @@ void send_card_search_result_t(
result_lobby->lobby_id, encoded_server_name.c_str());
}
cmd.location_string = location_string;
cmd.extension.menu_id = MenuID::LOBBY;
cmd.extension.lobby_id = result->lobby_id;
cmd.extension.lobby_refs[0].menu_id = MenuID::LOBBY;
cmd.extension.lobby_refs[0].item_id = result->lobby_id;
cmd.extension.player_name = result->game_data.player()->disp.name;
send_command_t(c, 0x41, 0x00, cmd);
@@ -1058,7 +1105,11 @@ void send_menu(shared_ptr<Client> c, const u16string& menu_name,
template <typename CharT>
void send_game_menu_t(shared_ptr<Client> c, shared_ptr<ServerState> s) {
void send_game_menu_t(
shared_ptr<Client> c,
shared_ptr<ServerState> s,
bool is_spectator_team_list,
bool show_tournaments_only) {
vector<S_GameMenuEntry<CharT>> entries;
{
auto& e = entries.emplace_back();
@@ -1074,6 +1125,7 @@ void send_game_menu_t(shared_ptr<Client> c, shared_ptr<ServerState> s) {
if (!l->is_game() || (l->version != c->version())) {
continue;
}
bool l_is_ep3 = !!(l->flags & Lobby::Flag::EPISODE_3_ONLY);
bool c_is_ep3 = !!(c->flags & Client::Flag::IS_EPISODE_3);
if (l_is_ep3 != c_is_ep3) {
@@ -1083,6 +1135,14 @@ void send_game_menu_t(shared_ptr<Client> c, shared_ptr<ServerState> s) {
continue;
}
bool l_is_spectator_team = !!(l->flags & Lobby::Flag::IS_SPECTATOR_TEAM);
if (l_is_spectator_team != is_spectator_team_list) {
continue;
}
if (show_tournaments_only && !l->tournament_match) {
continue;
}
auto& e = entries.emplace_back();
e.menu_id = MenuID::GAME;
e.game_id = l->lobby_id;
@@ -1110,16 +1170,20 @@ void send_game_menu_t(shared_ptr<Client> c, shared_ptr<ServerState> s) {
e.name = l->name;
}
send_command_vt(c, 0x08, entries.size() - 1, entries);
send_command_vt(c, is_spectator_team_list ? 0xE6 : 0x08, entries.size() - 1, entries);
}
void send_game_menu(shared_ptr<Client> c, shared_ptr<ServerState> s) {
void send_game_menu(
shared_ptr<Client> c,
shared_ptr<ServerState> s,
bool is_spectator_team_list,
bool show_tournaments_only) {
if ((c->version() == GameVersion::DC) ||
(c->version() == GameVersion::GC) ||
(c->version() == GameVersion::XB)) {
send_game_menu_t<char>(c, s);
send_game_menu_t<char>(c, s, is_spectator_team_list, show_tournaments_only);
} else {
send_game_menu_t<char16_t>(c, s);
send_game_menu_t<char16_t>(c, s, is_spectator_team_list, show_tournaments_only);
}
}
@@ -1565,8 +1629,8 @@ void send_get_player_info(shared_ptr<Client> c) {
////////////////////////////////////////////////////////////////////////////////
// Trade window
void send_execute_item_trade(std::shared_ptr<Client> c,
const std::vector<ItemData>& items) {
void send_execute_item_trade(shared_ptr<Client> c,
const vector<ItemData>& items) {
SC_TradeItems_D0_D3 cmd;
if (items.size() > sizeof(cmd.items) / sizeof(cmd.items[0])) {
throw logic_error("too many items in execute trade command");
@@ -1579,8 +1643,8 @@ void send_execute_item_trade(std::shared_ptr<Client> c,
send_command_t(c, 0xD3, 0x00, cmd);
}
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_card_trade(shared_ptr<Client> c,
const vector<pair<uint32_t, uint32_t>>& card_to_count) {
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
throw logic_error("cannot send trade cards command to non-Ep3 client");
}
@@ -1679,9 +1743,9 @@ void send_warp(shared_ptr<Client> c, uint32_t area) {
c->area = area;
}
void send_ep3_change_music(shared_ptr<Client> c, uint32_t song) {
void send_ep3_change_music(Channel& ch, uint32_t song) {
G_ChangeLobbyMusic_GC_Ep3_6xBF cmd = {{0xBF, 0x02, 0}, song};
send_command_t(c, 0x60, 0x00, cmd);
ch.send(0x60, 0x00, cmd);
}
void send_set_player_visibility(shared_ptr<Lobby> l, shared_ptr<Client> c,
@@ -1711,12 +1775,15 @@ void send_drop_item(shared_ptr<Lobby> l, const ItemData& item,
send_command_t(l, 0x60, 0x00, cmd);
}
// Notifies other players that a stack was split and part of it dropped (a new
// item was created)
void send_drop_stacked_item(Channel& ch, const ItemData& item,
uint8_t area, float x, float z) {
G_DropStackedItem_PC_V3_BB_6x5D cmd = {
{{0x5D, 0x0A, 0x0000}, area, 0, x, z, item}, 0};
ch.send(0x60, 0x00, &cmd, sizeof(cmd));
}
void send_drop_stacked_item(shared_ptr<Lobby> l, const ItemData& item,
uint8_t area, float x, float z) {
// TODO: Is this order correct? The original code sent {item, 0}, but it seems
// GC sends {0, item} (the last two fields in the struct are switched).
G_DropStackedItem_PC_V3_BB_6x5D cmd = {
{{0x5D, 0x0A, 0x0000}, area, 0, x, z, item}, 0};
send_command_t(l, 0x60, 0x00, cmd);
@@ -1841,10 +1908,10 @@ void send_ep3_card_list_update(shared_ptr<ServerState> s, shared_ptr<Client> c)
}
void send_ep3_media_update(
std::shared_ptr<Client> c,
shared_ptr<Client> c,
uint32_t type,
uint32_t which,
const std::string& compressed_data) {
const string& compressed_data) {
StringWriter w;
w.put<S_UpdateMediaHeader_GC_Ep3_B9>({type, which, compressed_data.size(), 0});
w.write(compressed_data);
@@ -1854,17 +1921,16 @@ void send_ep3_media_update(
send_command(c, 0xB9, 0x00, w.str());
}
// sends the client a generic rank
void send_ep3_rank_update(shared_ptr<Client> c) {
S_RankUpdate_GC_Ep3_B7 cmd = {
0, "\0\0\0\0\0\0\0\0\0\0\0", 0x00FFFFFF, 0x00FFFFFF, 0xFFFFFFFF};
0, "\0\0\0\0\0\0\0\0\0\0\0", 1000000, 1000000, 0xFFFFFFFF};
send_command_t(c, 0xB7, 0x00, cmd);
}
void send_ep3_card_battle_table_state(shared_ptr<Lobby> l, uint16_t table_number) {
S_CardBattleTableState_GC_Ep3_E4 cmd;
for (size_t z = 0; z < 4; z++) {
cmd.entries[z].present = 0;
cmd.entries[z].state = 0;
cmd.entries[z].unknown_a1 = 0;
cmd.entries[z].guild_card_number = 0;
}
@@ -1879,12 +1945,11 @@ void send_ep3_card_battle_table_state(shared_ptr<Lobby> l, uint16_t table_number
throw runtime_error("invalid battle table seat number");
}
auto& e = cmd.entries[c->card_battle_table_seat_number];
if (e.present) {
throw runtime_error("multiple clients in the same battle table seat");
if (e.state == 0) {
e.state = c->card_battle_table_seat_state;
e.guild_card_number = c->license->serial_number;
clients.emplace(c);
}
e.present = 1;
e.guild_card_number = c->license->serial_number;
clients.emplace(c);
}
}
@@ -1893,6 +1958,382 @@ void send_ep3_card_battle_table_state(shared_ptr<Lobby> l, uint16_t table_number
}
}
void send_ep3_set_context_token(shared_ptr<Client> c, uint32_t context_token) {
G_SetContextToken_GC_Ep3_6xB4x1F cmd;
cmd.context_token = context_token;
send_command_t(c, 0xC9, 0x00, cmd);
}
void send_ep3_confirm_tournament_entry(
shared_ptr<ServerState> s,
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn) {
S_ConfirmTournamentEntry_GC_Ep3_CC cmd;
if (tourn) {
cmd.tournament_name = tourn->get_name();
cmd.server_name = encode_sjis(s->name);
// TODO: Fill this in appropriately when we support scheduled start times
cmd.start_time = "Unknown";
auto& teams = tourn->all_teams();
cmd.num_teams = min<size_t>(teams.size(), 0x20);
for (size_t z = 0; z < min<size_t>(teams.size(), 0x20); z++) {
cmd.team_entries[z].win_count = teams[z]->num_rounds_cleared;
cmd.team_entries[z].is_active = teams[z]->is_active;
cmd.team_entries[z].name = teams[z]->name;
}
}
send_command_t(c, 0xCC, tourn ? 0x01 : 0x00, cmd);
}
void send_ep3_tournament_list(
shared_ptr<ServerState> s,
shared_ptr<Client> c,
bool is_for_spectator_team_create) {
S_TournamentList_GC_Ep3_E0 cmd;
size_t z = 0;
for (const auto& tourn : s->ep3_tournament_index->all_tournaments()) {
if (z >= 0x20) {
throw logic_error("more than 32 tournaments exist");
}
auto& entry = cmd.entries[z];
entry.menu_id = is_for_spectator_team_create
? MenuID::TOURNAMENTS_FOR_SPEC : MenuID::TOURNAMENTS;
entry.item_id = tourn->get_number();
// TODO: What does it mean for a tournament to be locked? Should we support
// that?
// TODO: Write appropriate round text (1st, 2nd, 3rd) here. This is
// nontrivial because unlike Sega's implementation, newserv does not require
// a round to completely finish before starting matches in the next round,
// as long as the winners of the preceding matches have been determined.
entry.state =
(tourn->get_state() == Episode3::Tournament::State::REGISTRATION)
? 0x00 : 0x05;
// TODO: Fill in cmd.start_time here when we implement scheduled starts.
entry.name = tourn->get_name();
const auto& teams = tourn->all_teams();
for (auto team : teams) {
if (!team->name.empty()) {
entry.num_teams++;
}
}
entry.max_teams = teams.size();
entry.unknown_a3 = 0xFFFF;
entry.unknown_a4 = 0xFFFF;
z++;
}
send_command_t(c, 0xE0, z, cmd);
}
void send_ep3_tournament_entry_list(
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn,
bool is_for_spectator_team_create) {
S_TournamentEntryList_GC_Ep3_E2 cmd;
cmd.players_per_team = tourn->get_is_2v2() ? 2 : 1;
size_t z = 0;
for (const auto& team : tourn->all_teams()) {
if (z >= 0x20) {
throw logic_error("more than 32 teams in tournament");
}
auto& entry = cmd.entries[z];
entry.menu_id = MenuID::TOURNAMENT_ENTRIES;
entry.item_id = (tourn->get_number() << 16) | z;
entry.unknown_a2 = team->num_rounds_cleared;
entry.locked = team->password.empty() ? 0 : 1;
if (tourn->get_state() != Episode3::Tournament::State::REGISTRATION) {
entry.state = 2;
} else if (team->name.empty()) {
entry.state = 0;
} else if (team->players.size() < team->max_players) {
entry.state = 1;
} else {
entry.state = 2;
}
entry.name = team->name;
z++;
}
send_command_t(c, is_for_spectator_team_create ? 0xE7 : 0xE2, z, cmd);
}
void send_ep3_tournament_details(
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn) {
S_TournamentGameDetails_GC_Ep3_E3 cmd;
cmd.name = tourn->get_name();
cmd.map_name = tourn->get_map()->map.name;
cmd.rules = tourn->get_rules();
const auto& teams = tourn->all_teams();
for (size_t z = 0; z < min<size_t>(teams.size(), 0x20); z++) {
cmd.bracket_entries[z].win_count = teams[z]->num_rounds_cleared;
cmd.bracket_entries[z].is_active = teams[z]->is_active ? 1 : 0;
cmd.bracket_entries[z].team_name = teams[z]->name;
}
cmd.num_bracket_entries = teams.size();
cmd.players_per_team = tourn->get_is_2v2() ? 2 : 1;
send_command_t(c, 0xE3, 0x02, cmd);
}
string ep3_description_for_client(shared_ptr<Client> c) {
if (!(c->flags & Client::Flag::IS_EPISODE_3)) {
throw runtime_error("client is not Episode 3");
}
auto player = c->game_data.player();
return string_printf(
"%s CLv%" PRIu32 " %c",
name_for_char_class(player->disp.char_class),
player->disp.level + 1,
char_for_language_code(player->inventory.language));
}
void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
shared_ptr<Lobby> primary_lobby;
if (l->flags & Lobby::Flag::IS_SPECTATOR_TEAM) {
primary_lobby = l->watched_lobby.lock();
} else {
primary_lobby = l;
}
auto tourn_match = primary_lobby ? primary_lobby->tournament_match : nullptr;
auto tourn = tourn_match ? tourn_match->tournament.lock() : nullptr;
if (tourn) {
S_TournamentGameDetails_GC_Ep3_E3 cmd;
cmd.name = encode_sjis(l->name);
cmd.map_name = tourn->get_map()->map.name;
cmd.rules = tourn->get_rules();
const auto& teams = tourn->all_teams();
for (size_t z = 0; z < min<size_t>(teams.size(), 0x20); z++) {
auto& entry = cmd.bracket_entries[z];
entry.win_count = teams[z]->num_rounds_cleared;
entry.is_active = teams[z]->is_active ? 1 : 0;
entry.team_name = teams[z]->name;
}
cmd.num_bracket_entries = teams.size();
cmd.players_per_team = tourn->get_is_2v2() ? 2 : 1;
if (primary_lobby) {
auto serial_number_to_client = primary_lobby->clients_by_serial_number();
auto describe_team = [&](S_TournamentGameDetails_GC_Ep3_E3::TeamEntry& team_entry, shared_ptr<const Episode3::Tournament::Team> team) -> void {
team_entry.team_name = team->name;
for (size_t z = 0; z < team->players.size(); z++) {
auto& entry = team_entry.players[z];
const auto& player = team->players[z];
if (player.is_human()) {
auto c = serial_number_to_client.at(player.serial_number);
entry.name = c->game_data.player()->disp.name;
entry.description = ep3_description_for_client(c);
} else {
entry.name = player.com_deck->player_name;
entry.description = "Deck: " + player.com_deck->deck_name;
}
}
};
describe_team(cmd.team_entries[0], tourn_match->preceding_a->winner_team);
describe_team(cmd.team_entries[1], tourn_match->preceding_b->winner_team);
}
uint8_t flag;
if (l != primary_lobby) {
for (auto spec_c : l->clients) {
if (spec_c) {
auto& entry = cmd.spectator_entries[cmd.num_spectators++];
entry.name = encode_sjis(spec_c->game_data.player()->disp.name);
entry.description = ep3_description_for_client(spec_c);
}
}
flag = 0x05;
} else {
flag = 0x03;
}
send_command_t(c, 0xE3, flag, cmd);
} else {
S_GameInformation_GC_Ep3_E1 cmd;
cmd.game_name = encode_sjis(l->name);
if (primary_lobby) {
size_t num_players = 0;
for (const auto& opp_c : primary_lobby->clients) {
if (opp_c) {
cmd.player_entries[num_players].name = opp_c->game_data.player()->disp.name;
cmd.player_entries[num_players].description = ep3_description_for_client(opp_c);
num_players++;
}
}
}
uint8_t flag;
if (l != primary_lobby) {
// TODO: This doesn't work (nothing shows up), but it appears to be a
// client bug? There doesn't appear to be a count field in the command
// anywhere...?
size_t num_spectators = 0;
for (auto spec_c : l->clients) {
if (spec_c) {
auto& entry = cmd.spectator_entries[num_spectators++];
entry.name = encode_sjis(spec_c->game_data.player()->disp.name);
entry.description = ep3_description_for_client(spec_c);
}
}
flag = 0x04;
} else if (primary_lobby &&
primary_lobby->ep3_server_base &&
primary_lobby->ep3_server_base->server->get_setup_phase() != Episode3::SetupPhase::REGISTRATION) {
cmd.rules = primary_lobby->ep3_server_base->map_and_rules1->rules;
flag = 0x01;
} else {
flag = 0x00;
}
send_command_t(c, 0xE1, flag, cmd);
}
}
void send_ep3_set_tournament_player_decks(
shared_ptr<Lobby> l,
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament::Match> match) {
auto tourn = match->tournament.lock();
if (!tourn) {
throw runtime_error("tournament is deleted");
}
G_SetTournamentPlayerDecks_GC_Ep3_6xB4x3D cmd;
cmd.rules = tourn->get_rules();
cmd.map_number = tourn->get_map()->map.map_number.load();
cmd.player_slot = 0xFF;
for (size_t z = 0; z < 4; z++) {
auto& entry = cmd.entries[z];
entry.player_name.clear(0);
entry.deck_name.clear(0);
entry.unknown_a1.clear(0);
entry.card_ids.clear(0);
entry.client_id = z;
}
auto serial_number_to_client = l->clients_by_serial_number();
auto add_entries_for_team = [&](shared_ptr<const Episode3::Tournament::Team> team, size_t base_index) -> void {
for (size_t z = 0; z < team->players.size(); z++) {
auto& entry = cmd.entries[base_index + z];
const auto& player = team->players[z];
if (player.is_human()) {
entry.type = 1; // Human
entry.player_name = serial_number_to_client.at(player.serial_number)->game_data.player()->disp.name;
if (player.serial_number == c->license->serial_number) {
cmd.player_slot = base_index + z;
}
} else {
entry.type = 2; // COM
entry.player_name = player.com_deck->player_name;
entry.deck_name = player.com_deck->deck_name;
entry.card_ids = player.com_deck->card_ids;
}
entry.unknown_a2 = 6;
}
};
add_entries_for_team(match->preceding_a->winner_team, 0);
add_entries_for_team(match->preceding_b->winner_team, 2);
if (!(tourn->get_data_index()->behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
uint8_t mask_key = (random_object<uint32_t>() % 0xFF) + 1;
set_mask_for_ep3_game_command(&cmd, sizeof(cmd), mask_key);
}
send_command_t(c, 0xC9, 0x00, cmd);
// TODO: Handle disconnection during the match (the other team should win)
}
void send_ep3_tournament_match_result(
shared_ptr<Lobby> l, shared_ptr<const Episode3::Tournament::Match> match) {
auto tourn = match->tournament.lock();
if (!tourn) {
return;
}
if ((match->winner_team != match->preceding_a->winner_team) &&
(match->winner_team != match->preceding_b->winner_team)) {
throw logic_error("cannot send tournament result without valid winner team");
}
auto serial_number_to_client = l->clients_by_serial_number();
auto write_player_names = [&](G_TournamentMatchResult_GC_Ep3_6xB4x51::NamesEntry& entry, shared_ptr<const Episode3::Tournament::Team> team) -> void {
for (size_t z = 0; z < team->players.size(); z++) {
const auto& player = team->players[z];
if (player.is_human()) {
entry.player_names[z] = serial_number_to_client.at(player.serial_number)->game_data.player()->disp.name;
} else {
entry.player_names[z] = player.com_deck->player_name;
}
}
};
G_TournamentMatchResult_GC_Ep3_6xB4x51 cmd;
cmd.match_description = (match == tourn->get_final_match())
? string_printf("(%s) Final match", tourn->get_name().c_str())
: string_printf("(%s) Round %zu", tourn->get_name().c_str(), match->round_num);
cmd.names_entries[0].team_name = match->preceding_a->winner_team->name;
write_player_names(cmd.names_entries[0], match->preceding_a->winner_team);
cmd.names_entries[1].team_name = match->preceding_b->winner_team->name;
write_player_names(cmd.names_entries[1], match->preceding_b->winner_team);
// The value 6 here causes the client to show the "Congratulations" text
// instead of "On to the next round"
cmd.round_num = (match == tourn->get_final_match()) ? 6 : match->round_num;
cmd.num_players_per_team = match->preceding_a->winner_team->max_players;
cmd.winner_team_id = (match->preceding_b->winner_team == match->winner_team);
// TODO: This amount should vary depending on the match level / round number,
// but newserv doesn't currently implement meseta at all - we just always give
// the player 1000000 and never charge for anything.
cmd.meseta_amount = 100;
cmd.meseta_reward_text = "You got %s meseta!";
if (!(tourn->get_data_index()->behavior_flags & Episode3::BehaviorFlag::DISABLE_MASKING)) {
uint8_t mask_key = (random_object<uint32_t>() % 0xFF) + 1;
set_mask_for_ep3_game_command(&cmd, sizeof(cmd), mask_key);
}
send_command_t(l, 0xC9, 0x00, cmd);
}
void send_ep3_update_spectator_count(shared_ptr<Lobby> l) {
G_SetGameMetadata_GC_Ep3_6xB4x52 cmd;
for (auto watcher_l : l->watcher_lobbies) {
for (auto c : watcher_l->clients) {
cmd.total_spectators += (c.get() != nullptr);
}
}
// TODO: Make s available here so we can apply masking if needed (perhaps by
// adding a weak_ptr in Lobby... it'd be dumb to require s to be passed in to
// this function just to check a behavior flag)
// Note: We can't use send_command_t(l, ...) here because that would send the
// same command to l and to all wather lobbies. The commands should have
// different values depending on who's in each watcher lobby, so we have to
// manually send to each client here.
for (auto c : l->clients) {
if (c) {
send_command_t(c, 0xC9, 0x00, cmd);
}
}
for (auto watcher_l : l->watcher_lobbies) {
cmd.local_spectators = 0;
for (auto c : watcher_l->clients) {
cmd.local_spectators += (c.get() != nullptr);
}
for (auto c : watcher_l->clients) {
if (c) {
send_command_t(c, 0xC9, 0x00, cmd);
}
}
}
}
void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) {
if (size < 8) {
throw logic_error("Episode 3 game command is too short for masking");
@@ -1939,9 +2380,9 @@ void send_quest_file_chunk(
size_t chunk_index,
const void* data,
size_t size,
QuestFileType type) {
bool is_download_quest) {
if (size > 0x400) {
throw invalid_argument("quest file chunks must be 1KB or smaller");
throw logic_error("quest file chunks must be 1KB or smaller");
}
S_WriteFile_13_A7 cmd;
@@ -1952,41 +2393,141 @@ void send_quest_file_chunk(
}
cmd.data_size = size;
send_command_t(c, (type == QuestFileType::ONLINE) ? 0x13 : 0xA7, chunk_index, cmd);
send_command_t(c, is_download_quest ? 0xA7 : 0x13, chunk_index, cmd);
}
void send_quest_file(shared_ptr<Client> c, const string& quest_name,
const string& basename, const string& contents, QuestFileType type) {
void send_open_quest_file(shared_ptr<Client> c, const string& quest_name,
const string& basename, shared_ptr<const string> contents, QuestFileType type) {
switch (c->version()) {
case GameVersion::DC:
send_quest_open_file_t<S_OpenFile_DC_44_A6>(
c, quest_name, basename, contents.size(), type);
c, quest_name, basename, contents->size(), type);
break;
case GameVersion::PC:
case GameVersion::GC:
case GameVersion::XB:
send_quest_open_file_t<S_OpenFile_PC_V3_44_A6>(
c, quest_name, basename, contents.size(), type);
c, quest_name, basename, contents->size(), type);
break;
case GameVersion::BB:
send_quest_open_file_t<S_OpenFile_BB_44_A6>(
c, quest_name, basename, contents.size(), type);
c, quest_name, basename, contents->size(), type);
break;
default:
throw logic_error("cannot send quest files to this version of client");
}
for (size_t offset = 0; offset < contents.size(); offset += 0x400) {
size_t chunk_bytes = contents.size() - offset;
if (chunk_bytes > 0x400) {
chunk_bytes = 0x400;
// For GC/XB/BB, we wait for acknowledgement commands before sending each
// chunk. For DC/PC, we send the entire quest all at once.
if ((c->version() == GameVersion::DC) || (c->version() == GameVersion::PC)) {
for (size_t offset = 0; offset < contents->size(); offset += 0x400) {
size_t chunk_bytes = contents->size() - offset;
if (chunk_bytes > 0x400) {
chunk_bytes = 0x400;
}
send_quest_file_chunk(c, basename.c_str(), offset / 0x400,
contents->data() + offset, chunk_bytes, (type != QuestFileType::ONLINE));
}
send_quest_file_chunk(c, basename.c_str(), offset / 0x400,
contents.data() + offset, chunk_bytes, type);
} else {
c->sending_files.emplace(basename, contents);
c->log.info("Opened file %s", basename.c_str());
}
}
void send_quest_barrier_if_all_clients_ready(shared_ptr<Lobby> l) {
if (!l || !l->is_game()) {
return;
}
// Check if any client is still loading
size_t x;
for (x = 0; x < l->max_clients; x++) {
if (!l->clients[x]) {
continue;
}
if (l->clients[x]->flags & Client::Flag::LOADING_QUEST) {
break;
}
}
// If they're all done, start the quest
if (x == l->max_clients) {
send_command(l, 0xAC, 0x00);
}
// Check if any client is still loading
for (x = 0; x < l->max_clients; x++) {
if (l->clients[x]) {
l->clients[x]->disconnect_hooks.erase(QUEST_BARRIER_DISCONNECT_HOOK_NAME);
}
}
}
void send_card_auction_if_all_clients_ready(
shared_ptr<ServerState> s, shared_ptr<Lobby> l) {
// Check if any client is still not ready
size_t x;
for (x = 0; x < l->max_clients; x++) {
if (!l->clients[x]) {
continue;
}
if (!(l->clients[x]->flags & Client::Flag::AWAITING_CARD_AUCTION)) {
break;
}
}
if (x != l->max_clients) {
return;
}
for (x = 0; x < l->max_clients; x++) {
if (l->clients[x]) {
l->clients[x]->flags &= ~Client::Flag::AWAITING_CARD_AUCTION;
}
}
if ((s->ep3_card_auction_points == 0) ||
(s->ep3_card_auction_min_size == 0) ||
(s->ep3_card_auction_max_size == 0)) {
throw runtime_error("card auctions are not configured on this server");
}
uint16_t num_cards;
if (s->ep3_card_auction_min_size == s->ep3_card_auction_max_size) {
num_cards = s->ep3_card_auction_min_size;
} else {
num_cards = s->ep3_card_auction_min_size +
(random_object<uint16_t>() % (s->ep3_card_auction_max_size - s->ep3_card_auction_min_size + 1));
}
num_cards = min<uint16_t>(num_cards, 0x14);
uint64_t distribution_size = 0;
for (const auto& it : s->ep3_card_auction_pool) {
distribution_size += it.second.first;
}
S_StartCardAuction_GC_Ep3_EF cmd;
cmd.points_available = s->ep3_card_auction_points;
for (size_t z = 0; z < num_cards; z++) {
uint64_t v = random_object<uint64_t>() % distribution_size;
for (const auto& it : s->ep3_card_auction_pool) {
if (v >= it.second.first) {
v -= it.second.first;
} else {
cmd.entries[z].card_id = s->ep3_data_index->definition_for_card_name(it.first)->def.card_id.load();
cmd.entries[z].min_price = it.second.second;
break;
}
}
}
send_command_t(l, 0xEF, num_cards, cmd);
for (auto c : l->clients) {
if (c) {
c->disconnect_hooks.erase(CARD_AUCTION_DISCONNECT_HOOK_NAME);
}
}
}
void send_server_time(shared_ptr<Client> c) {
uint64_t t = now();
+76 -5
View File
@@ -51,6 +51,18 @@ inline void send_command_excluding_client(std::shared_ptr<Lobby> l,
send_command_excluding_client(l, c, command, flag, nullptr, 0);
}
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) {
send_command_if_not_loading(l, command, flag, data.data(), data.size());
}
template <typename StructT>
inline void send_command_if_not_loading(std::shared_ptr<Lobby> l,
uint16_t command, uint32_t flag, const StructT& data) {
send_command_if_not_loading(l, command, flag, &data, sizeof(data));
}
void send_command(std::shared_ptr<Lobby> l, uint16_t command, uint32_t flag,
const void* data, size_t size);
@@ -163,6 +175,7 @@ void send_enter_directory_patch(std::shared_ptr<Client> c, const std::string& di
void send_patch_file(std::shared_ptr<Client> c, std::shared_ptr<PatchFileIndex::File> f);
void send_message_box(std::shared_ptr<Client> c, const std::u16string& text);
void send_ep3_timed_message_box(Channel& ch, uint32_t frames, const std::string& text);
void send_lobby_name(std::shared_ptr<Client> c, const std::u16string& text);
void send_quest_info(std::shared_ptr<Client> c, const std::u16string& text,
bool is_download_quest);
@@ -179,7 +192,10 @@ std::u16string prepare_chat_message(
const std::u16string& from_name,
const std::u16string& text,
char private_flags);
void send_chat_message(Channel& ch, const std::u16string& text);
void send_chat_message(
Channel& ch,
const std::u16string& text,
char private_flags);
void send_chat_message(
std::shared_ptr<Client> c,
uint32_t from_guild_card_number,
@@ -211,6 +227,9 @@ __attribute__((format(printf, 2, 3))) void send_text_message_printf(
return send_text_message(t, decoded.c_str());
}
__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_card_search_result(
@@ -230,7 +249,11 @@ void send_guild_card(
void send_guild_card(std::shared_ptr<Client> c, std::shared_ptr<Client> source);
void send_menu(std::shared_ptr<Client> c, const std::u16string& menu_name,
uint32_t menu_id, const std::vector<MenuItem>& items, bool is_info_menu = false);
void send_game_menu(std::shared_ptr<Client> c, std::shared_ptr<ServerState> s);
void send_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,
@@ -269,7 +292,7 @@ void send_player_stats_change(
void send_warp(Channel& ch, uint8_t client_id, uint32_t area);
void send_warp(std::shared_ptr<Client> c, uint32_t area);
void send_ep3_change_music(std::shared_ptr<Client> c, uint32_t song);
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);
@@ -278,6 +301,8 @@ void send_drop_item(Channel& ch, const ItemData& item,
bool from_enemy, uint8_t area, float x, float z, uint16_t request_id);
void send_drop_item(std::shared_ptr<Lobby> l, const ItemData& item,
bool from_enemy, uint8_t area, float x, float z, uint16_t request_id);
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,
@@ -300,6 +325,38 @@ void send_ep3_media_update(
const std::string& compressed_data);
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(
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament> t,
bool is_for_spectator_team_create);
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<Lobby> l,
std::shared_ptr<Client> c,
std::shared_ptr<const Episode3::Tournament::Match> match);
void send_ep3_tournament_match_result(
std::shared_ptr<Lobby> l,
std::shared_ptr<const Episode3::Tournament::Match> match);
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);
// Pass mask_key = 0 to unmask the command
void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key);
@@ -311,9 +368,23 @@ enum class QuestFileType {
GBA_DEMO,
};
void send_quest_file(std::shared_ptr<Client> c, const std::string& quest_name,
const std::string& basename, const std::string& contents,
void send_open_quest_file(
std::shared_ptr<Client> c,
const std::string& quest_name,
const std::string& basename,
std::shared_ptr<const std::string> contents,
QuestFileType type);
void send_quest_file_chunk(
shared_ptr<Client> c,
const string& filename,
size_t chunk_index,
const void* data,
size_t size,
bool is_download_quest);
void 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);
void send_server_time(std::shared_ptr<Client> c);
+86 -3
View File
@@ -46,8 +46,41 @@ void Server::disconnect_client(shared_ptr<Client> c) {
} catch (const exception& e) {
server_log.warning("Error during client disconnect cleanup: %s", e.what());
}
// c is destroyed here (on_disconnect should remove any other references
// to it, e.g. from Lobby objects)
// We can't just let c be destroyed here, since disconnect_client can be
// called from within the client's channel's receive handler. So, we instead
// move it to another set, which we'll clear in an immediately-enqueued
// callback after the current event. This will also call the client's
// disconnect hooks (if any).
this->clients_to_destroy.insert(move(c));
}
void Server::enqueue_destroy_clients() {
auto tv = usecs_to_timeval(0);
event_add(this->destroy_clients_ev.get(), &tv);
}
void Server::dispatch_destroy_clients(evutil_socket_t, short, void* ctx) {
reinterpret_cast<Server*>(ctx)->destroy_clients();
}
void Server::destroy_clients() {
for (auto c_it = this->clients_to_destroy.begin();
c_it != this->clients_to_destroy.end();
c_it = this->clients_to_destroy.erase(c_it)) {
auto c = *c_it;
// Note: It's important to move the disconnect hooks out of the client here
// because the hooks could modify c->disconnect_hooks while it's being
// iterated here, which would invalidate these iterators.
unordered_map<string, function<void()>> hooks = move(c->disconnect_hooks);
for (auto h_it : hooks) {
try {
h_it.second();
} catch (const exception& e) {
c->log.warning("Disconnect hook %s failed: %s", h_it.first.c_str(), e.what());
}
}
}
}
void Server::dispatch_on_listen_accept(
@@ -177,7 +210,9 @@ void Server::on_client_error(Channel& ch, short events) {
Server::Server(
shared_ptr<struct event_base> base,
shared_ptr<ServerState> state)
: base(base), state(state) { }
: base(base),
destroy_clients_ev(event_new(this->base.get(), -1, EV_TIMEOUT, &Server::dispatch_destroy_clients, this), event_free),
state(state) { }
void Server::listen(
const std::string& addr_str,
@@ -235,6 +270,54 @@ shared_ptr<Client> Server::get_client() const {
return this->channel_to_client.begin()->second;
}
vector<shared_ptr<Client>> Server::get_clients_by_identifier(const string& ident) const {
int64_t serial_number_hex = -1;
int64_t serial_number_dec = -1;
try {
serial_number_dec = stoul(ident, nullptr, 10);
} catch (const invalid_argument&) { }
try {
serial_number_hex = stoul(ident, nullptr, 16);
} catch (const invalid_argument&) { }
u16string u16name = decode_sjis(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) {
auto c = it.second;
if (c->license && c->license->serial_number == serial_number_dec) {
results.emplace_back(move(c));
continue;
}
if (c->license && c->license->serial_number == serial_number_hex) {
results.emplace_back(move(c));
continue;
}
if (c->license && c->license->username == ident) {
results.emplace_back(move(c));
continue;
}
auto p = c->game_data.player(false);
if (p && p->disp.name == u16name) {
results.emplace_back(move(c));
continue;
}
if (c->channel.name == ident) {
results.emplace_back(move(c));
continue;
}
if (starts_with(c->channel.name, ident + " ")) {
results.emplace_back(move(c));
continue;
}
}
return results;
}
shared_ptr<struct event_base> Server::get_base() const {
return this->base;
}
+8
View File
@@ -31,10 +31,13 @@ public:
GameVersion version, ServerBehavior initial_state);
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;
private:
std::shared_ptr<struct event_base> base;
std::shared_ptr<struct event> destroy_clients_ev;
struct ListeningSocket {
std::string addr_str;
@@ -52,9 +55,14 @@ private:
};
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;
void enqueue_destroy_clients();
static void dispatch_destroy_clients(evutil_socket_t, short, void* ctx);
void destroy_clients();
static void dispatch_on_listen_accept(struct evconnlistener* listener,
evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx);
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
+403 -63
View File
@@ -5,7 +5,9 @@
#include <string.h>
#include <phosg/Strings.hh>
#include <phosg/Random.hh>
#include "ReceiveCommands.hh"
#include "ServerState.hh"
#include "SendCommands.hh"
#include "StaticGameData.hh"
@@ -24,11 +26,14 @@ void ServerShell::print_prompt() {
fflush(stdout);
}
shared_ptr<ProxyServer::LinkedSession> ServerShell::get_proxy_session() {
shared_ptr<ProxyServer::LinkedSession> ServerShell::get_proxy_session(
const string& name) {
if (!this->state->proxy_server.get()) {
throw runtime_error("the proxy server is disabled");
}
return this->state->proxy_server->get_session();
return name.empty()
? this->state->proxy_server->get_session()
: this->state->proxy_server->get_session_by_name(name);
}
static void set_boolean(bool* target, const string& args) {
@@ -41,13 +46,50 @@ static void set_boolean(bool* target, const string& args) {
}
}
static string get_quoted_string(string& s) {
string ret;
char end_char = (s.at(0) == '\"') ? '\"' : ' ';
size_t z = (s.at(0) == '\"') ? 1 : 0;
for (; (z < s.size()) && (s[z] != end_char); z++) {
if (s[z] == '\\') {
if (z + 1 < s.size()) {
ret.push_back(s[z + 1]);
} else {
throw runtime_error("incomplete escape sequence");
}
} else {
ret.push_back(s[z]);
}
}
if (end_char != ' ') {
if (z >= s.size()) {
throw runtime_error("unterminated quoted string");
}
s = s.substr(skip_whitespace(s, z + 1));
} else {
s = s.substr(skip_whitespace(s, z));
}
return ret;
}
void ServerShell::execute_command(const string& command) {
// find the entry in the command table and run the command
// Find the entry in the command table and run the command
size_t command_end = skip_non_whitespace(command, 0);
size_t args_begin = skip_whitespace(command, command_end);
string command_name = command.substr(0, command_end);
string command_args = command.substr(args_begin);
string session_name;
if (command_name == "on") {
size_t session_name_end = skip_non_whitespace(command_args, 0);
size_t command_begin = skip_whitespace(command_args, session_name_end);
command_end = skip_non_whitespace(command_args, command_begin);
args_begin = skip_whitespace(command_args, command_end);
session_name = command_args.substr(0, session_name_end);
command_name = command_args.substr(command_begin, command_end - command_begin);
command_args = command_args.substr(args_begin);
}
if (command_name == "exit") {
throw exit_shell();
@@ -61,7 +103,7 @@ Server commands:\n\
exit (or ctrl+d)\n\
Shut down the server.\n\
reload <item> ...\n\
Reload data. <item> can be licenses, quests, functions, or programs.\n\
Reload data. <item> can be licenses, quests, functions, programs, or ep3.\n\
Reloading will not affect items that are in use; for example, if a client\'s\n\
license is deleted by reloading, they will not be disconnected immediately.\n\
add-license <parameters>\n\
@@ -72,6 +114,10 @@ Server commands:\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\
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\
command.\n\
delete-license <serial-number>\n\
Delete a license from the server.\n\
list-licenses\n\
@@ -89,12 +135,44 @@ Server commands:\n\
Song IDs are 0 through 51; the default song is -1.\n\
announce <message>\n\
Send an announcement message to all players.\n\
create-tournament \"Tournament Name\" \"Map Name\" <num-teams> [options...]\n\
Create an Episode 3 tournament. The quotes are required arount the\n\
tournament and map names, unless the names contain no spaces.\n\
Rules options:\n\
2v2: Set team size to 2 players (default is 1 without this option)\n\
dice=MIN-MAX: Set minimum and maximum dice rolls\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\
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\
hp-type=TEAM/PLAYER/COMMON: Set team HP type\n\
allow-assists=ON/OFF: Enable/disable assist cards\n\
dialogue=ON/OFF: Enable/disable dialogue\n\
dice-exchange=ATK/DEF/NONE: Set dice exchange mode\n\
dice-boost=ON/OFF: Enable/disable dice boost\n\
delete-tournament \"Tournament Name\"\n\
Delete a tournament. The quotes are required unless the tournament name\n\
contains no spaces.\n\
list-tournaments\n\
List the names and numbers of all existing tournaments.\n\
start-tournament \"Tournament Name\"\n\
End registration for a tournament and allow matches to begin. The quotes\n\
are required unless the tournament name contains no spaces.\n\
tournament-state \"Tournament Name\"\n\
Show the current state of a tournament. The quotes are required unless the\n\
tournament name contains no spaces.\n\
\n\
Proxy commands (these will only work when exactly one client is connected):\n\
Proxy commands:\n\
sc <data>\n\
Send a command to the client.\n\
Send a command to the client. This command also can be used to send data to\n\
a client on the game server.\n\
ss <data>\n\
Send a command to the server.\n\
show-slots\n\
Show the player names, Guild Card numbers, and client IDs of all players in\n\
the current lobby or game.\n\
chat <text>\n\
Send a chat message to the server.\n\
dchat <data>\n\
@@ -145,10 +223,21 @@ Proxy commands (these will only work when exactly one client is connected):\n\
responds as if the function was called (with the given return value), but\n\
does not send the code to the client. To stop blocking function calls, omit\n\
the return value.\n\
set-next-item <code>\n\
Set the next item to be dropped as if the client had run the $item command.\n\
create-item <data>\n\
Create an item as if the client had run the $item command.\n\
set-next-item <data>\n\
Set the next item to be dropped.\n\
close-idle-sessions\n\
Closes all sessions that don\'t have a client and server connected.\n\
\n\
If there are multiple clients connected, or multiple proxy sessions open, many\n\
of the above commands will fail since they can\'t determine which session should\n\
be affected. To specify a session, prefix the command with `on <ident>`. For\n\
game server sessions, <ident> may be the client\'s ID (e.g. C-3), player name,\n\
license serial number (specified in hex or in decimal), or BB account username.\n\
For proxy sessions, <ident> may only be the session ID (which is generally the\n\
same as the client\'s serial number). For example, to send a ping to the proxy\n\
session with ID 17205AE4, run the command `on 17205AE4 sc 1D 00 04 00`.\n\
");
@@ -173,6 +262,10 @@ Proxy commands (these will only work when exactly one client is connected):\n\
} else if (type == "programs") {
shared_ptr<DOLFileIndex> dfi(new DOLFileIndex("system/dol"));
this->state->dol_file_index = dfi;
} else if (type == "programs") {
shared_ptr<Episode3::DataIndex> data_index(new Episode3::DataIndex(
"system/ep3", this->state->ep3_behavior_flags));
this->state->ep3_data_index = data_index;
} else {
throw invalid_argument("incorrect data type");
}
@@ -224,7 +317,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\
}
} else {
throw invalid_argument("incorrect field");
throw invalid_argument("incorrect field: " + token);
}
}
@@ -235,6 +328,70 @@ Proxy commands (these will only work when exactly one client is connected):\n\
this->state->license_manager->add(l);
fprintf(stderr, "license added\n");
} else if (command_name == "update-license") {
auto tokens = split(command_args, ' ');
if (tokens.size() < 2) {
throw runtime_error("not enough arguments");
}
uint32_t serial_number = stoul(tokens[0]);
tokens.erase(tokens.begin());
auto orig_l = this->state->license_manager->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);
} 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, "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, "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);
}
} else {
throw invalid_argument("incorrect field: " + token);
}
}
if (!l->serial_number) {
throw invalid_argument("license does not contain serial number");
}
this->state->license_manager->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);
@@ -276,12 +433,136 @@ Proxy commands (these will only work when exactly one client is connected):\n\
u16string message16 = decode_sjis(command_args);
send_text_message(this->state, message16.c_str());
} 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_data_index->definition_for_map_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;
if (!command_args.empty()) {
auto tokens = split(command_args, ' ');
for (auto& token : tokens) {
token = tolower(token);
if (token == "2v2") {
is_2v2 = true;
} 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");
}
rules.min_dice = stoul(subtokens[0]);
rules.max_dice = stoul(subtokens[0]);
} else if (starts_with(token, "overall-time-limit=")) {
uint32_t limit = stoul(token.substr(19));
if (limit > 600) {
throw runtime_error("overall-time-limit must be 600 or fewer minutes");
}
if (limit % 5) {
throw runtime_error("overall-time-limit must be a multiple of 5 minutes");
}
rules.overall_time_limit = limit;
} else if (starts_with(token, "phase-time-limit=")) {
rules.phase_time_limit = stoul(token.substr(17));
} else if (starts_with(token, "hp=")) {
rules.char_hp = stoul(token.substr(3));
} else if (token == "allowed-cards=all") {
rules.allowed_cards = Episode3::AllowedCards::ALL;
} else if (token == "allowed-cards=n") {
rules.allowed_cards = Episode3::AllowedCards::N_ONLY;
} else if (token == "allowed-cards=nr") {
rules.allowed_cards = Episode3::AllowedCards::N_R_ONLY;
} else if (token == "allowed-cards=nrs") {
rules.allowed_cards = Episode3::AllowedCards::N_R_S_ONLY;
} else if (token == "deck-shuffle=on") {
rules.disable_deck_shuffle = 0;
} else if (token == "deck-shuffle=off") {
rules.disable_deck_shuffle = 1;
} else if (token == "deck-loop=on") {
rules.disable_deck_loop = 0;
} else if (token == "deck-loop=off") {
rules.disable_deck_loop = 1;
} else if (token == "allow-assists=on") {
rules.no_assist_cards = 0;
} else if (token == "allow-assists=off") {
rules.no_assist_cards = 1;
} else if (token == "dialogue=on") {
rules.disable_dialogue = 0;
} else if (token == "dialogue=off") {
rules.disable_dialogue = 1;
} else if (token == "dice-boost=on") {
rules.disable_dice_boost = 0;
} else if (token == "dice-boost=off") {
rules.disable_dice_boost = 1;
} else if (token == "hp-type=player") {
rules.hp_type = Episode3::HPType::DEFEAT_PLAYER;
} else if (token == "hp-type=team") {
rules.hp_type = Episode3::HPType::DEFEAT_TEAM;
} else if (token == "hp-type=common") {
rules.hp_type = Episode3::HPType::COMMON_HP;
} else if (token == "dice-exchange=atk") {
rules.dice_exchange_mode = Episode3::DiceExchangeMode::HIGH_ATK;
} else if (token == "dice-exchange=def") {
rules.dice_exchange_mode = Episode3::DiceExchangeMode::HIGH_DEF;
} else if (token == "dice-exchange=none") {
rules.dice_exchange_mode = Episode3::DiceExchangeMode::NONE;
} else {
throw runtime_error("invalid rules option: " + token);
}
}
}
if (rules.check_and_reset_invalid_fields()) {
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());
} 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();
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());
}
} else if (command_name == "start-tournament") {
string name = get_quoted_string(command_args);
auto tourn = this->state->ep3_tournament_index->get_tournament(name);
if (tourn) {
tourn->start();
this->state->ep3_tournament_index->save();
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") {
string name = get_quoted_string(command_args);
auto tourn = this->state->ep3_tournament_index->get_tournament(name);
if (tourn) {
tourn->print_bracket(stderr);
} else {
fprintf(stderr, "no such tournament exists\n");
}
// PROXY COMMANDS
} else if ((command_name == "sc") || (command_name == "ss")) {
string data = parse_data_string(command_args);
string data = parse_data_string(command_args, nullptr, ParseDataFlags::ALLOW_FILES);
if (data.size() & 3) {
throw invalid_argument("data size is not a multiple of 4");
}
@@ -291,7 +572,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\
shared_ptr<ProxyServer::LinkedSession> proxy_session;
try {
proxy_session = this->get_proxy_session();
proxy_session = this->get_proxy_session(session_name);
} catch (const exception&) { }
if (proxy_session.get()) {
@@ -302,47 +583,89 @@ Proxy commands (these will only work when exactly one client is connected):\n\
}
} else {
if (command_name [1] == 's') {
throw runtime_error("cannot send to server in non-proxy session");
shared_ptr<Client> c;
if (session_name.empty()) {
c = this->state->game_server->get_client();
} else {
auto clients = this->state->game_server->get_clients_by_identifier(
session_name);
if (clients.empty()) {
throw runtime_error("no such client");
}
if (clients.size() > 1) {
throw runtime_error("multiple clients found");
}
c = move(clients[0]);
}
auto c = this->state->game_server->get_client();
send_command_with_header(c->channel, data.data(), data.size());
if (c) {
if (command_name[1] == 's') {
on_command_with_header(this->state, c, data);
} else {
send_command_with_header(c->channel, data.data(), data.size());
}
} else {
throw runtime_error("no client available");
}
}
} else if (command_name == "show-slots") {
auto session = this->get_proxy_session(session_name);
for (size_t z = 0; z < session->lobby_players.size(); z++) {
const auto& player = session->lobby_players[z];
if (player.guild_card_number) {
auto secid_name = name_for_section_id(player.section_id);
fprintf(stderr, " %zu: %" PRIu32 " => %s (%s, %s)\n",
z, player.guild_card_number, player.name.c_str(),
name_for_char_class(player.char_class), secid_name.c_str());
} else {
fprintf(stderr, " %zu: (no player)\n", z);
}
}
} else if ((command_name == "chat") || (command_name == "dchat")) {
auto session = this->get_proxy_session();
auto session = this->get_proxy_session(session_name);
bool is_dchat = (command_name == "dchat");
string data(8, '\0');
data.push_back('\x09');
data.push_back('E');
if (command_name == "dchat") {
data += parse_data_string(command_args);
if (!is_dchat && (session->version == GameVersion::PC || session->version == GameVersion::BB)) {
u16string data(4, u'\0');
data.push_back(u'\x09');
data.push_back(u'E');
data += decode_sjis(command_args);
data.push_back(u'\0');
data.resize((data.size() + 1) & (~1));
session->server_channel.send(0x06, 0x00, data.data(), data.size() * sizeof(char16_t));
} else {
data += command_args;
string data(8, '\0');
data.push_back('\x09');
data.push_back('E');
if (is_dchat) {
data += parse_data_string(command_args, nullptr, ParseDataFlags::ALLOW_FILES);
} else {
data += command_args;
data.push_back('\0');
}
data.resize((data.size() + 3) & (~3));
session->server_channel.send(0x06, 0x00, data);
}
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();
auto session = this->get_proxy_session(session_name);
session->server_channel.send(0x89, stoul(command_args));
} else if (command_name == "warp") {
auto session = this->get_proxy_session();
auto session = this->get_proxy_session(session_name);
uint8_t area = stoul(command_args);
send_warp(session->client_channel, session->lobby_client_id, area);
send_warp(session->server_channel, session->lobby_client_id, area);
} else if ((command_name == "info-board") || (command_name == "info-board-data")) {
auto session = this->get_proxy_session();
auto session = this->get_proxy_session(session_name);
string data;
if (command_name == "info-board-data") {
data += parse_data_string(command_args);
data += parse_data_string(command_args, nullptr, ParseDataFlags::ALLOW_FILES);
} else {
data += command_args;
}
@@ -352,60 +675,65 @@ Proxy commands (these will only work when exactly one client is connected):\n\
session->server_channel.send(0xD9, 0x00, data);
} else if (command_name == "set-override-section-id") {
auto session = this->get_proxy_session();
auto session = this->get_proxy_session(session_name);
if (command_args.empty()) {
session->override_section_id = -1;
session->options.override_section_id = -1;
} else {
session->override_section_id = section_id_for_name(command_args);
session->options.override_section_id = section_id_for_name(command_args);
}
} else if (command_name == "set-override-event") {
auto session = this->get_proxy_session();
auto session = this->get_proxy_session(session_name);
if (command_args.empty()) {
session->override_lobby_event = -1;
session->options.override_lobby_event = -1;
} else {
session->override_lobby_event = event_for_name(command_args);
session->client_channel.send(0xDA, session->override_lobby_event);
session->options.override_lobby_event = event_for_name(command_args);
if ((session->version != GameVersion::DC) &&
(session->version != GameVersion::PC) && (
!((session->version == GameVersion::GC) &&
(session->newserv_client_config.cfg.flags & Client::Flag::IS_TRIAL_EDITION)))) {
session->client_channel.send(0xDA, session->options.override_lobby_event);
}
}
} else if (command_name == "set-override-lobby-number") {
auto session = this->get_proxy_session();
auto session = this->get_proxy_session(session_name);
if (command_args.empty()) {
session->override_lobby_number = -1;
session->options.override_lobby_number = -1;
} else {
session->override_lobby_number = lobby_type_for_name(command_args);
session->options.override_lobby_number = lobby_type_for_name(command_args);
}
} else if (command_name == "set-chat-filter") {
auto session = this->get_proxy_session();
set_boolean(&session->enable_chat_filter, command_args);
auto session = this->get_proxy_session(session_name);
set_boolean(&session->options.enable_chat_filter, command_args);
} else if (command_name == "set-infinite-hp") {
auto session = this->get_proxy_session();
set_boolean(&session->infinite_hp, command_args);
auto session = this->get_proxy_session(session_name);
set_boolean(&session->options.infinite_hp, command_args);
} else if (command_name == "set-infinite-tp") {
auto session = this->get_proxy_session();
set_boolean(&session->infinite_tp, command_args);
auto session = this->get_proxy_session(session_name);
set_boolean(&session->options.infinite_tp, command_args);
} else if (command_name == "set-switch-assist") {
auto session = this->get_proxy_session();
set_boolean(&session->switch_assist, command_args);
auto session = this->get_proxy_session(session_name);
set_boolean(&session->options.switch_assist, command_args);
} else if (command_name == "set-save-files") {
auto session = this->get_proxy_session();
set_boolean(&session->save_files, command_args);
} else if (command_name == "set-save-files" && this->state->proxy_allow_save_files) {
auto session = this->get_proxy_session(session_name);
set_boolean(&session->options.save_files, command_args);
} else if (command_name == "set-block-function-calls") {
auto session = this->get_proxy_session();
auto session = this->get_proxy_session(session_name);
if (command_args.empty()) {
session->function_call_return_value = -1;
session->options.function_call_return_value = -1;
} else {
session->function_call_return_value = stoul(command_args);
session->options.function_call_return_value = stoul(command_args);
}
} else if (command_name == "set-next-item") {
auto session = this->get_proxy_session();
} else if ((command_name == "create-item") || (command_name == "set-next-item")) {
auto session = this->get_proxy_session(session_name);
if (session->version == GameVersion::BB) {
throw runtime_error("proxy session is BB");
@@ -417,7 +745,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\
throw runtime_error("proxy session is not game leader");
}
string data = parse_data_string(command_args);
string data = parse_data_string(command_args, nullptr, ParseDataFlags::ALLOW_FILES);
if (data.size() < 2) {
throw runtime_error("data too short");
}
@@ -425,16 +753,28 @@ Proxy commands (these will only work when exactly one client is connected):\n\
throw runtime_error("data too long");
}
session->next_drop_item.clear();
PlayerInventoryItem item;
item.data.id = random_object<uint32_t>();
if (data.size() <= 12) {
memcpy(&session->next_drop_item.data.data1, data.data(), data.size());
memcpy(&item.data.data1, data.data(), data.size());
} else {
memcpy(&session->next_drop_item.data.data1, data.data(), 12);
memcpy(&session->next_drop_item.data.data2, data.data() + 12, data.size() - 12);
memcpy(&item.data.data1, data.data(), 12);
memcpy(&item.data.data2, data.data() + 12, data.size() - 12);
}
string name = name_for_item(session->next_drop_item.data, true);
send_text_message(session->client_channel, u"$C7Next drop:\n" + decode_sjis(name));
if (command_name == "set-next-item") {
session->next_drop_item = item;
string name = name_for_item(session->next_drop_item.data, 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);
string name = name_for_item(item.data, true);
send_text_message(session->client_channel, u"$C7Item created:\n" + decode_sjis(name));
}
} else if (command_name == "close-idle-sessions") {
size_t count = this->state->proxy_server->delete_disconnected_sessions();
+2 -1
View File
@@ -26,7 +26,8 @@ public:
protected:
std::shared_ptr<ServerState> state;
std::shared_ptr<ProxyServer::LinkedSession> get_proxy_session();
std::shared_ptr<ProxyServer::LinkedSession> get_proxy_session(
const std::string& name);
virtual void print_prompt();
virtual void execute_command(const std::string& command);
+26 -12
View File
@@ -31,7 +31,11 @@ ServerState::ServerState()
ep3_card_auction_max_size(0),
next_lobby_id(1),
pre_lobby_event(0),
ep3_menu_song(-1) {
ep3_menu_song(-1),
local_address(0),
external_address(0),
proxy_allow_save_files(true),
proxy_enable_login_options(false) {
vector<shared_ptr<Lobby>> non_v1_only_lobbies;
vector<shared_ptr<Lobby>> ep3_only_lobbies;
@@ -120,15 +124,19 @@ void ServerState::remove_client_from_lobby(shared_ptr<Client> c) {
}
}
bool ServerState::change_client_lobby(shared_ptr<Client> c, shared_ptr<Lobby> new_lobby) {
bool ServerState::change_client_lobby(
shared_ptr<Client> c,
shared_ptr<Lobby> new_lobby,
bool send_join_notification,
ssize_t required_client_id) {
uint8_t old_lobby_client_id = c->lobby_client_id;
shared_ptr<Lobby> current_lobby = this->find_lobby(c->lobby_id);
try {
if (current_lobby) {
current_lobby->move_client_to_lobby(new_lobby, c);
current_lobby->move_client_to_lobby(new_lobby, c, required_client_id);
} else {
new_lobby->add_client(c);
new_lobby->add_client(c, required_client_id);
}
} catch (const out_of_range&) {
return false;
@@ -141,7 +149,9 @@ bool ServerState::change_client_lobby(shared_ptr<Client> c, shared_ptr<Lobby> ne
send_player_leave_notification(current_lobby, old_lobby_client_id);
}
}
this->send_lobby_join_notifications(new_lobby, c);
if (send_join_notification) {
this->send_lobby_join_notifications(new_lobby, c);
}
return true;
}
@@ -179,10 +189,11 @@ vector<shared_ptr<Lobby>> ServerState::all_lobbies() {
}
shared_ptr<Lobby> ServerState::create_lobby() {
shared_ptr<Lobby> l(new Lobby(this->next_lobby_id++));
if (!this->id_to_lobby.emplace(l->lobby_id, l).second) {
throw logic_error("lobby already exists with the given id");
while (this->id_to_lobby.count(this->next_lobby_id)) {
this->next_lobby_id++;
}
shared_ptr<Lobby> l(new Lobby(this->next_lobby_id++));
this->id_to_lobby.emplace(l->lobby_id, l);
l->log.info("Created lobby");
return l;
}
@@ -379,7 +390,11 @@ void ServerState::create_menus(shared_ptr<const JSONObject> config_json) {
vector<pair<string, uint16_t>>& ret_pds,
const char* key) {
try {
const auto& items = d.at(key);
map<string, shared_ptr<JSONObject>> sorted_jsons;
for (const auto& it : d.at(key)->as_dict()) {
sorted_jsons.emplace(it.first, it.second);
}
ret_menu.clear();
ret_pds.clear();
@@ -389,7 +404,7 @@ void ServerState::create_menus(shared_ptr<const JSONObject> config_json) {
u"Set proxy options", 0);
uint32_t item_id = 0;
for (const auto& item : items->as_dict()) {
for (const auto& item : sorted_jsons) {
const string& netloc_str = item.second->as_string();
const string& description = "$C7Remote server:\n$C6" + netloc_str;
ret_menu.emplace_back(item_id, decode_sjis(item.first),
@@ -538,8 +553,7 @@ shared_ptr<const string> ServerState::load_bb_file(
try {
auto ret = cache.get_or_load("system/blueburst/" + effective_bb_directory_filename);
static_game_data_log.info("Loaded %s", effective_bb_directory_filename.c_str());
// TODO: It's also not great that we copy the data here... sigh
return shared_ptr<string>(new string(ret.file->data));
return ret.file->data;
} catch (const exception& e) {
static_game_data_log.info("%s missing from system/blueburst", effective_bb_directory_filename.c_str());
static_game_data_log.error("%s not found in any source", patch_index_filename.c_str());
+12 -5
View File
@@ -9,6 +9,8 @@
#include <unordered_map>
#include <vector>
#include "Episode3/DataIndex.hh"
#include "Episode3/Tournament.hh"
#include "Client.hh"
#include "FunctionCompiler.hh"
#include "GSLArchive.hh"
@@ -66,6 +68,8 @@ struct ServerState {
std::shared_ptr<const GSLArchive> bb_data_gsl;
std::shared_ptr<const RareItemSet> rare_item_set;
std::shared_ptr<Episode3::TournamentIndex> ep3_tournament_index;
uint16_t ep3_card_auction_points;
uint16_t ep3_card_auction_min_size;
uint16_t ep3_card_auction_max_size;
@@ -95,9 +99,6 @@ struct ServerState {
std::vector<std::shared_ptr<Lobby>> public_lobby_search_order_v1;
std::vector<std::shared_ptr<Lobby>> public_lobby_search_order_non_v1;
std::vector<std::shared_ptr<Lobby>> public_lobby_search_order_ep3;
// TODO: Use a free-list instead of an incrementer to prevent wrap-around
// behavioral bugs. This... will probably never be an issue for anyone, but we
// technically should handle it.
std::atomic<int32_t> next_lobby_id;
uint8_t pre_lobby_event;
int32_t ep3_menu_song;
@@ -106,6 +107,9 @@ struct ServerState {
uint32_t local_address;
uint32_t external_address;
bool proxy_allow_save_files;
bool proxy_enable_login_options;
std::shared_ptr<ProxyServer> proxy_server;
std::shared_ptr<Server> game_server;
std::shared_ptr<FileContentsCache> client_options_cache;
@@ -114,8 +118,11 @@ struct ServerState {
void add_client_to_available_lobby(std::shared_ptr<Client> c);
void remove_client_from_lobby(std::shared_ptr<Client> c);
bool change_client_lobby(std::shared_ptr<Client> c,
std::shared_ptr<Lobby> new_lobby);
bool change_client_lobby(
std::shared_ptr<Client> c,
std::shared_ptr<Lobby> new_lobby,
bool send_join_notification = true,
ssize_t required_client_id = -1);
void send_lobby_join_notifications(std::shared_ptr<Lobby> l,
std::shared_ptr<Client> joining_client);
+20 -1
View File
@@ -327,6 +327,25 @@ char abbreviation_for_difficulty(uint8_t difficulty) {
char char_for_language_code(uint8_t language) {
switch (language) {
case 0:
return 'J';
case 1:
return 'E';
case 2:
return 'G';
case 3:
return 'F';
case 4:
return 'S';
default:
return '?';
}
}
size_t stack_size_for_item(uint8_t data0, uint8_t data1) {
if (data0 == 4) {
return 999999;
@@ -425,7 +444,7 @@ const unordered_map<uint32_t, ItemNameInfo> name_info_for_primary_identifier({
{0x000103, {"Pallasch", false}},
{0x000104, {"Gladius", false}},
{0x000105, "DB\'s SABER"},
{0x000106, "KALADGOLG"},
{0x000106, "KALADBOLG"},
{0x000107, "DURANDAL"},
{0x000108, "GALATINE"},
{0x000200, {"Sword", false}},
+2
View File
@@ -47,4 +47,6 @@ const char* abbreviation_for_char_class(uint8_t cls);
const char* name_for_difficulty(uint8_t difficulty);
char abbreviation_for_difficulty(uint8_t difficulty);
char char_for_language_code(uint8_t language);
std::string name_for_item(const ItemData& item, bool include_color_codes);
+6 -1
View File
@@ -39,6 +39,11 @@ inline std::u16string decode_sjis(const std::string& s) {
return decode_sjis(s.data(), s.size());
}
// These functions exist so that decode_sjis and encode_sjis can be
// indiscriminately used within templates that use different char types.
inline const std::string& encode_sjis(const std::string& s) { return s; }
inline const std::u16string& decode_sjis(const std::u16string& s) { return s; }
// (1b) Type-independent utility functions
template <typename T>
@@ -51,7 +56,7 @@ size_t text_strlen_t(const T* s) {
template <typename T>
size_t text_strnlen_t(const T* s, size_t count) {
size_t ret = 0;
for (; s[ret] != 0 && ret < count; ret++) { }
for (; (ret < count) && (s[ret] != 0); ret++) { }
return ret;
}
+9 -1
View File
@@ -10,6 +10,13 @@ using namespace std;
const vector<string> version_to_login_port_name({
"bb-patch", "console-login", "pc-login", "console-login", "console-login", "bb-init"});
const vector<string> version_to_lobby_port_name({
"bb-patch", "console-lobby", "pc-lobby", "console-lobby", "console-lobby", "bb-lobby"});
const vector<string> version_to_proxy_port_name({
"", "dc-proxy", "pc-proxy", "gc-proxy", "xb-proxy", "bb-proxy"});
uint16_t flags_for_version(GameVersion version, int64_t sub_version) {
switch (sub_version) {
case -1: // Initial check (before sub_version recognition)
@@ -64,7 +71,8 @@ uint16_t flags_for_version(GameVersion version, int64_t sub_version) {
case 0x40: // GC Ep3 trial
return Client::Flag::NO_D6_AFTER_LOBBY |
Client::Flag::IS_EPISODE_3;
Client::Flag::IS_EPISODE_3 |
Client::Flag::ENCRYPTED_SEND_FUNCTION_CALL;
case 0x42: // GC Ep3 JP
return Client::Flag::NO_D6_AFTER_LOBBY |
Client::Flag::IS_EPISODE_3 |
+7
View File
@@ -2,6 +2,9 @@
#include <inttypes.h>
#include <vector>
#include <string>
enum class GameVersion {
@@ -23,6 +26,10 @@ enum class ServerBehavior {
PROXY_SERVER,
};
extern const std::vector<std::string> version_to_login_port_name;
extern const std::vector<std::string> version_to_lobby_port_name;
extern const std::vector<std::string> version_to_proxy_port_name;
uint16_t flags_for_version(GameVersion version, int64_t sub_version);
const char* name_for_version(GameVersion version);
GameVersion version_for_name(const char* name);
Binary file not shown.
Binary file not shown.
+18 -9
View File
@@ -123,6 +123,12 @@
// connect to newserv will be proxied to this destination.
// "ProxyDestination-BB": "",
// There is a proxy option that allows users to save copies of various game
// files on the server side. If you have external clients connecting to your
// server, you can disable this option to prevent clients from generating
// files on the server side which they will never be able to access.
"ProxyAllowSaveFiles": true,
// By default, the interactive shell runs if stdin is a terminal, and doesn't
// run if it's not. This option, if present, overrides that behavior.
// "RunInteractiveShell": false,
@@ -157,8 +163,9 @@
"FunctionCompiler": "info",
// IP stack simulator messages describe clients connecting and disconnecting
// via the IP stack interface, and errors that occur at the simulated
// network level within the simulator.
"IPStackSimulator": "info",
// network level within the simulator. This log is fairly verbose at the
// info level, so by default we suppress those messages.
"IPStackSimulator": "warning",
// License manager messages describe the creation of new license files.
"LicenseManager": "info",
// Lobby messages describe creation and deletion of lobbies and games, as
@@ -253,18 +260,20 @@
// by bitwise-OR'ing together the following values:
// 0x00000001 => Disable deck verification entirely
// 0x00000002 => Disable owned card count check during deck verification (this
// enables the use of the non-saveable Have All Cards code, but
// retains all the other validity checks)
// 0x00000004 => Allow card with the D1 and D2 ranks to be used in battle
// enables the use of the non-saveable Have All Cards Action
// Replay code, but retains all the other validity checks)
// 0x00000004 => Allow cards with the D1 and D2 ranks to be used in battle
// 0x00000008 => Disable overall and per-phase battle time limits, regardless
// of the options chosen during battle setup
// of the values chosen during battle rules setup
// 0x00000010 => Enable debug messages in Episode 3 games and battles
// 0x00000020 => Load card text as well as card definitions (has no behavioral
// effects; this flag exists to be used internally when the
// --show-ep3-data option is given)
// effects in games; this flag exists to be used internally when
// the --show-ep3-data option is given)
// 0x00000040 => Enable battle recording (after a battle, players can save the
// recording with the $saverec <filename> command)
// 0x00000080 => Enable command masking during battles
// 0x00000080 => Disable command masking during battles
// 0x00000100 => Disable interference (COMs randomly coming to each other's
// rescue)
"Episode3BehaviorFlags": 0x00000002,
// Episode 3 card auction configuration. CardAuctionPoints specifies how many
+198
View File
@@ -0,0 +1,198 @@
[
// Episode 3 tournament COM decks. These are randomly chosen for each COM
// player in a tournament.
// Entries are [PlayerName, DeckName, [CardID, ...]]
// These decks are from session logs from Sega's servers.
["COM:D02", "Tremble", [0x0007, 0x006F, 0x006F, 0x006F, 0x0070, 0x0070, 0x01DC, 0x01DC, 0x01DC, 0x01DD, 0x01F1, 0x01F1, 0x01F1, 0x020B, 0x020B, 0x020E, 0x020E, 0x00ED, 0x00ED, 0x00ED, 0x00C6, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x0220, 0x0220, 0x0107]],
["COM:D08", "Earthquake", [0x011E, 0x01E6, 0x01E6, 0x01E6, 0x01FB, 0x01FB, 0x01FB, 0x00C5, 0x00C5, 0x00C5, 0x00CA, 0x00CA, 0x00CA, 0x00CB, 0x00CB, 0x00CC, 0x00CC, 0x022C, 0x022C, 0x022C, 0x00E4, 0x00E4, 0x00E4, 0x00EF, 0x00EF, 0x00EF, 0x0215, 0x0215, 0x0215, 0x00ED, 0x00ED]],
["COM:D10", "ONIGAMI", [0x011E, 0x01F1, 0x01F1, 0x01F1, 0x0078, 0x0078, 0x0078, 0x0079, 0x0079, 0x0079, 0x005B, 0x01E7, 0x01E7, 0x01E7, 0x0215, 0x0215, 0x0215, 0x00CF, 0x00CF, 0x00CF, 0x00C6, 0x00C6, 0x00C6, 0x00C5, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA]],
["COM:D15", "GAGIGAGI!", [0x0005, 0x0155, 0x0155, 0x0155, 0x000A, 0x000A, 0x015C, 0x015C, 0x015C, 0x01A3, 0x01A3, 0x01A3, 0x0016, 0x0016, 0x0016, 0x008C, 0x008C, 0x00C6, 0x00C6, 0x00C6, 0x00C5, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x012E, 0x012E, 0x012E]],
["COM:H01", "Rumble!", [0x0002, 0x0279, 0x0279, 0x0184, 0x0184, 0x0184, 0x000C, 0x000C, 0x000C, 0x0284, 0x0284, 0x0016, 0x0016, 0x0016, 0x0214, 0x0214, 0x00ED, 0x00ED, 0x00ED, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x0232, 0x0232, 0x0107, 0x0107, 0x0107]],
["COM:H06", "Helpless!", [0x0112, 0x0169, 0x0169, 0x0169, 0x0256, 0x0256, 0x0041, 0x0041, 0x0041, 0x018F, 0x018F, 0x018F, 0x00D8, 0x00D9, 0x00D9, 0x00DA, 0x00DF, 0x00DF, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x0234, 0x0234, 0x00F6, 0x012B, 0x012B, 0x012B]],
["COM:H07", "Storm", [0x0004, 0x00C5, 0x00C5, 0x00C5, 0x00C7, 0x00C7, 0x00CA, 0x00CA, 0x00CF, 0x00CF, 0x00D4, 0x00D4, 0x0215, 0x0215, 0x0215, 0x00EF, 0x00EF, 0x00EF, 0x00F0, 0x00ED, 0x00D9, 0x00D9, 0x00DA, 0x00DA, 0x01A4, 0x01A4, 0x01A4, 0x0017, 0x0017, 0x0017, 0x01A8]],
["COM:H09", "Blow", [0x0004, 0x016B, 0x016B, 0x016B, 0x0171, 0x0171, 0x01A3, 0x01A3, 0x0017, 0x0017, 0x0017, 0x01A9, 0x01A9, 0x01A9, 0x0016, 0x0016, 0x0215, 0x0215, 0x0215, 0x00C6, 0x00C6, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x00CF, 0x00CF, 0x00CF]],
["COM:H16", "Struggle", [0x0110, 0x000A, 0x000A, 0x000A, 0x015C, 0x015C, 0x015C, 0x01A3, 0x01A3, 0x01A3, 0x0017, 0x0017, 0x0017, 0x0016, 0x0016, 0x0016, 0x008B, 0x008B, 0x0093, 0x0093, 0x00C5, 0x00C5, 0x00C5, 0x00CB, 0x00CB, 0x00CB, 0x00CA, 0x00CA, 0x00CA, 0x0104, 0x0104]],
// These decks are from the USA Episode 3 client.
["COM:X03", "Default SW", [0x0004, 0x008E, 0x00D8, 0x0009, 0x0028, 0x008C, 0x0093, 0x0028, 0x002C, 0x0093, 0x0029, 0x008A, 0x0009, 0x008E, 0x0017, 0x0029, 0x0017, 0x0093, 0x00D8, 0x002C, 0x008A, 0x002C, 0x008C, 0x0017, 0x0029, 0x008C, 0x0028, 0x0009, 0x00D8, 0x008A, 0x008E]],
["COM:X04", "F Slash", [0x0004, 0x0009, 0x0246, 0x00C6, 0x0028, 0x000B, 0x002C, 0x008A, 0x008C, 0x0028, 0x002C, 0x0246, 0x009C, 0x000B, 0x0210, 0x008C, 0x0210, 0x00C6, 0x002C, 0x009C, 0x008E, 0x00C5, 0x009C, 0x0246, 0x0009, 0x008A, 0x0210, 0x0028, 0x00C5, 0x008C, 0x008E]],
["COM:X05", "Double Hit", [0x0004, 0x002C, 0x00C6, 0x002C, 0x015A, 0x015A, 0x00CC, 0x0014, 0x01A3, 0x0014, 0x00C5, 0x0016, 0x002C, 0x01B0, 0x008A, 0x01B0, 0x015A, 0x003D, 0x00CC, 0x01A3, 0x01A3, 0x0014, 0x008A, 0x0016, 0x008A, 0x00CC, 0x003D, 0x01B0, 0x00C6, 0x003D, 0x0016]],
["COM:X06", "Twin Dance", [0x0004, 0x002C, 0x00C6, 0x00C5, 0x015A, 0x0222, 0x01AF, 0x01B0, 0x015A, 0x0014, 0x00C5, 0x0016, 0x002C, 0x01B0, 0x0016, 0x015A, 0x01AF, 0x0142, 0x0014, 0x00C6, 0x0142, 0x00CB, 0x0222, 0x0222, 0x002C, 0x01B0, 0x01AF, 0x00C6, 0x0014, 0x0142, 0x00C5]],
["COM:X07", "Skill EX", [0x0004, 0x0254, 0x00CB, 0x0247, 0x0247, 0x00CB, 0x0093, 0x000D, 0x00CA, 0x0210, 0x00F6, 0x0017, 0x0254, 0x00C6, 0x00CB, 0x008C, 0x0247, 0x000D, 0x00CA, 0x0210, 0x0222, 0x00C6, 0x0254, 0x0017, 0x0093, 0x00CA, 0x008C, 0x000D, 0x00F6, 0x0210, 0x0017]],
["COM:X08", "Double Edge", [0x0004, 0x000D, 0x0177, 0x00CB, 0x0177, 0x00DB, 0x0246, 0x0009, 0x0210, 0x0232, 0x01A5, 0x000D, 0x00CB, 0x00DB, 0x00CA, 0x00D8, 0x0246, 0x0210, 0x0230, 0x0210, 0x00CA, 0x0232, 0x01A5, 0x000D, 0x00CB, 0x01A5, 0x00CA, 0x0230, 0x00D8, 0x0009, 0x0232]],
["COM:X09", "Attack Blow", [0x0004, 0x00C5, 0x0177, 0x00C6, 0x016C, 0x009A, 0x016C, 0x01A8, 0x008D, 0x00C5, 0x0017, 0x0177, 0x009A, 0x0230, 0x00C6, 0x016C, 0x008C, 0x00C7, 0x008D, 0x01A8, 0x00C6, 0x0177, 0x0017, 0x0230, 0x0232, 0x0232, 0x008C, 0x01A8, 0x00C7, 0x008C, 0x00C5]],
["COM:X0A", "Red Weapon", [0x0004, 0x0230, 0x00C6, 0x0243, 0x026C, 0x00A3, 0x026C, 0x00C6, 0x0253, 0x0099, 0x024E, 0x0243, 0x025C, 0x00C6, 0x00A3, 0x026C, 0x00C5, 0x0232, 0x0099, 0x0230, 0x008F, 0x025C, 0x0243, 0x00C5, 0x0099, 0x0253, 0x0232, 0x008F, 0x008F, 0x024E, 0x0232]],
["COM:X0B", "Last Deck", [0x0004, 0x0252, 0x0165, 0x00C6, 0x0165, 0x0107, 0x00C5, 0x002C, 0x0252, 0x00CA, 0x0107, 0x0016, 0x0165, 0x01AF, 0x0230, 0x00C5, 0x002C, 0x00CA, 0x012E, 0x0016, 0x0232, 0x0230, 0x0016, 0x00FB, 0x002C, 0x00FB, 0x012E, 0x00CA, 0x012E, 0x0107, 0x00C6]],
["COM:X0C", "Gun Judge", [0x0002, 0x008A, 0x000A, 0x0034, 0x0017, 0x008C, 0x0034, 0x0035, 0x00D8, 0x008E, 0x0151, 0x000A, 0x0151, 0x008A, 0x008C, 0x00D8, 0x008E, 0x0017, 0x0034, 0x0093, 0x0035, 0x008A, 0x0151, 0x008C, 0x0035, 0x0093, 0x0017, 0x0093, 0x008E, 0x00D8, 0x000A]],
["COM:X0D", "Shocked?", [0x0002, 0x0151, 0x00C5, 0x0017, 0x017C, 0x008E, 0x00A3, 0x00CA, 0x0162, 0x0207, 0x00C5, 0x0017, 0x0151, 0x00A3, 0x00CB, 0x00A3, 0x017C, 0x0207, 0x0162, 0x00CA, 0x008A, 0x008E, 0x0017, 0x0151, 0x00CB, 0x017C, 0x00CA, 0x0162, 0x0207, 0x008A, 0x00C5]],
["COM:X0E", "Soul of Lead", [0x0002, 0x00C6, 0x0186, 0x00CA, 0x0034, 0x0151, 0x0016, 0x008C, 0x00C5, 0x00CA, 0x0017, 0x0186, 0x00C5, 0x0151, 0x0034, 0x008A, 0x00CB, 0x0017, 0x008C, 0x00C6, 0x0016, 0x0186, 0x0017, 0x00C5, 0x00CB, 0x0034, 0x008A, 0x00CB, 0x00CA, 0x00C6, 0x0016]],
["COM:X0F", "Soul Bullet", [0x0002, 0x0283, 0x0271, 0x000A, 0x00BA, 0x000C, 0x008A, 0x0223, 0x0271, 0x0283, 0x0150, 0x0223, 0x00CB, 0x000C, 0x00BA, 0x000A, 0x008A, 0x00CA, 0x0211, 0x0271, 0x00C6, 0x0283, 0x00CB, 0x00CB, 0x00CA, 0x000C, 0x00BA, 0x00CA, 0x0211, 0x0223, 0x0211]],
["COM:X10", "Got EX!", [0x0002, 0x00CA, 0x0247, 0x0279, 0x00CB, 0x01A8, 0x009A, 0x017D, 0x008E, 0x00F6, 0x0017, 0x0247, 0x01A8, 0x00C6, 0x008A, 0x0279, 0x017D, 0x008E, 0x00CA, 0x009A, 0x00C6, 0x0247, 0x0017, 0x00CB, 0x00CB, 0x0279, 0x008A, 0x00CA, 0x0017, 0x009A, 0x00F6]],
["COM:X11", "More EX!", [0x0002, 0x009A, 0x0247, 0x0279, 0x01A8, 0x017D, 0x00C6, 0x009A, 0x00C5, 0x0017, 0x0099, 0x0247, 0x01A8, 0x00C6, 0x008A, 0x0279, 0x00C5, 0x00CC, 0x009A, 0x017D, 0x0017, 0x0099, 0x0247, 0x0017, 0x00C6, 0x0279, 0x00CC, 0x008A, 0x00CC, 0x00C5, 0x0099]],
["COM:X12", "Heart!", [0x0002, 0x0247, 0x00CA, 0x0279, 0x00C5, 0x009A, 0x00C6, 0x0017, 0x0211, 0x0279, 0x00CA, 0x0016, 0x0247, 0x009A, 0x00C5, 0x009A, 0x0017, 0x0279, 0x0211, 0x00CB, 0x00CC, 0x0016, 0x00C6, 0x0016, 0x0247, 0x00C5, 0x00CB, 0x0211, 0x00CB, 0x00CA, 0x00C6]],
["COM:X13", "Enthusiasm", [0x0002, 0x0236, 0x00C5, 0x0247, 0x01B6, 0x008E, 0x0232, 0x0185, 0x0236, 0x00C6, 0x0011, 0x00C5, 0x0247, 0x01A5, 0x0230, 0x008E, 0x01B6, 0x0185, 0x0232, 0x00C6, 0x00C5, 0x0011, 0x0247, 0x01A5, 0x0230, 0x0232, 0x008E, 0x0185, 0x0236, 0x00C6, 0x01B6]],
["COM:X14", "Custom Burn", [0x0002, 0x0150, 0x0234, 0x01AA, 0x00CA, 0x00C6, 0x008F, 0x014A, 0x00CB, 0x00F7, 0x00F7, 0x0211, 0x0150, 0x0298, 0x00CA, 0x00C6, 0x01AA, 0x008F, 0x014A, 0x00CB, 0x0234, 0x0150, 0x0298, 0x00CA, 0x01AA, 0x014A, 0x00C6, 0x00CB, 0x00F7, 0x0234, 0x0211]],
["COM:X15", "Last Attack", [0x0002, 0x0107, 0x00C6, 0x0211, 0x00CA, 0x0279, 0x0186, 0x0230, 0x0236, 0x01A9, 0x00C5, 0x0211, 0x0279, 0x0232, 0x0236, 0x01A9, 0x0273, 0x00C5, 0x010D, 0x0107, 0x00CA, 0x0211, 0x0230, 0x0186, 0x010D, 0x0236, 0x0107, 0x01A9, 0x00CA, 0x0273, 0x0279]],
["COM:X16", "Green Steel", [0x0001, 0x008C, 0x0009, 0x000B, 0x00C6, 0x002C, 0x0028, 0x0017, 0x00C5, 0x008A, 0x0029, 0x0009, 0x0029, 0x008C, 0x002C, 0x000B, 0x0028, 0x0017, 0x00CB, 0x00C6, 0x0009, 0x008C, 0x0029, 0x00C6, 0x002C, 0x000B, 0x00CB, 0x00C5, 0x008A, 0x0028, 0x00CA]],
["COM:X17", "Death Trail", [0x0001, 0x00CC, 0x000D, 0x00C6, 0x0009, 0x0017, 0x000B, 0x00B8, 0x0016, 0x022B, 0x000D, 0x0017, 0x00C6, 0x0099, 0x0009, 0x000B, 0x0099, 0x00C5, 0x00B8, 0x022B, 0x00CC, 0x0016, 0x000D, 0x00C6, 0x00C5, 0x0099, 0x000B, 0x00B8, 0x0016, 0x00C5, 0x022B]],
["COM:X18", "Hungry Soul", [0x0001, 0x00C5, 0x0010, 0x00C6, 0x0155, 0x0017, 0x008A, 0x002C, 0x008C, 0x00CA, 0x0028, 0x0010, 0x0017, 0x00C6, 0x008A, 0x0155, 0x00CB, 0x008C, 0x0028, 0x00C5, 0x00CA, 0x0017, 0x0010, 0x00C6, 0x00CB, 0x002C, 0x008A, 0x0155, 0x00CB, 0x008C, 0x00CA]],
["COM:X19", "Despair", [0x0001, 0x0010, 0x00CA, 0x0230, 0x002C, 0x0154, 0x0009, 0x00CB, 0x0155, 0x00B8, 0x002C, 0x0010, 0x0232, 0x0009, 0x0154, 0x00B8, 0x00CB, 0x0155, 0x00CA, 0x00B1, 0x0230, 0x0009, 0x0010, 0x002C, 0x0232, 0x0154, 0x00CB, 0x0155, 0x00B8, 0x00B1, 0x00CA]],
["COM:X1A", "God's Power", [0x0001, 0x0232, 0x000D, 0x00CA, 0x00C6, 0x0155, 0x0009, 0x0155, 0x002C, 0x00CB, 0x00C6, 0x000D, 0x0154, 0x0232, 0x0099, 0x0155, 0x0099, 0x00C5, 0x00CB, 0x002C, 0x0232, 0x0154, 0x00CA, 0x000D, 0x00C6, 0x0154, 0x00CB, 0x0099, 0x00C5, 0x002C, 0x00CA]],
["COM:X1B", "Feared Hand", [0x0001, 0x00F7, 0x0154, 0x0155, 0x0028, 0x00CA, 0x0009, 0x00F7, 0x00CB, 0x002C, 0x0154, 0x0230, 0x0028, 0x0232, 0x008F, 0x0155, 0x0009, 0x008F, 0x0104, 0x00CB, 0x0230, 0x00CA, 0x002C, 0x0154, 0x0155, 0x0232, 0x002C, 0x008F, 0x00F7, 0x00CA, 0x0009]],
["COM:X1C", "Final Chapt.", [0x0001, 0x000F, 0x00C6, 0x00CB, 0x0234, 0x0022, 0x008F, 0x0232, 0x00CA, 0x00FD, 0x008D, 0x000F, 0x0234, 0x00CB, 0x0022, 0x0232, 0x008D, 0x00CA, 0x0232, 0x008F, 0x00FD, 0x00C6, 0x000F, 0x008D, 0x0022, 0x0234, 0x00CA, 0x008F, 0x00CB, 0x00FD, 0x00C6]],
["COM:X1D", "WeakAttack", [0x0110, 0x01AF, 0x0243, 0x00CB, 0x000C, 0x00C5, 0x0099, 0x009A, 0x0173, 0x008C, 0x01AF, 0x0243, 0x0099, 0x00CC, 0x00C6, 0x000C, 0x0173, 0x009A, 0x00C5, 0x008C, 0x01AF, 0x00CB, 0x00CC, 0x0243, 0x0099, 0x000C, 0x00C6, 0x00C6, 0x0173, 0x009A, 0x00C5]],
["COM:X1E", "Halfguard", [0x0110, 0x00C5, 0x0243, 0x00CB, 0x01AF, 0x008C, 0x00C6, 0x01A5, 0x00C5, 0x00BC, 0x0247, 0x00CB, 0x0243, 0x008C, 0x00C7, 0x01AF, 0x009C, 0x00C6, 0x00BC, 0x008C, 0x00BC, 0x01A5, 0x0243, 0x0247, 0x01AF, 0x00C7, 0x00C6, 0x009C, 0x00C5, 0x01A5, 0x0247]],
["COM:X1F", "High-cost", [0x0110, 0x00F6, 0x00CA, 0x01BC, 0x00C6, 0x0014, 0x00CB, 0x00B7, 0x0249, 0x00C5, 0x01BC, 0x0168, 0x00B7, 0x00CB, 0x008E, 0x0014, 0x0249, 0x00C5, 0x00CA, 0x00F6, 0x00C6, 0x00C6, 0x01BC, 0x0168, 0x0014, 0x00CB, 0x008E, 0x00CA, 0x0249, 0x00C5, 0x0168]],
["COM:X20", "Combo", [0x0110, 0x0230, 0x00C5, 0x000A, 0x000C, 0x00C6, 0x00BA, 0x0173, 0x01A5, 0x00CA, 0x00CF, 0x01A5, 0x000A, 0x00BA, 0x0232, 0x000C, 0x00C1, 0x0173, 0x00C6, 0x00CA, 0x01A5, 0x00CF, 0x0230, 0x000A, 0x00BA, 0x0232, 0x00C6, 0x00C1, 0x00C5, 0x00CA, 0x00C1]],
["COM:X21", "Link", [0x0110, 0x014A, 0x000F, 0x0232, 0x008C, 0x0022, 0x0208, 0x0099, 0x0209, 0x014A, 0x008D, 0x0298, 0x000F, 0x008C, 0x0230, 0x0099, 0x0022, 0x0209, 0x00F6, 0x008D, 0x0298, 0x0230, 0x008D, 0x0232, 0x0022, 0x0232, 0x0208, 0x00F6, 0x014A, 0x0298, 0x000F]],
["COM:X22", "Icy Joyjoy", [0x0112, 0x018F, 0x00C5, 0x00DB, 0x0195, 0x00DE, 0x0030, 0x01A4, 0x00D8, 0x00C5, 0x01A3, 0x00C6, 0x018F, 0x00DE, 0x0030, 0x0195, 0x01A3, 0x00D8, 0x00C6, 0x01A4, 0x00DB, 0x018F, 0x01A3, 0x00DE, 0x00C6, 0x0030, 0x00D8, 0x0195, 0x01A4, 0x00DB, 0x00C5]],
["COM:X23", "Growing TP", [0x0112, 0x00CA, 0x00C5, 0x0195, 0x0290, 0x00D8, 0x01A5, 0x00DB, 0x00CA, 0x00DE, 0x01A3, 0x0195, 0x01A3, 0x00C6, 0x00D8, 0x0290, 0x00DB, 0x00CB, 0x00DE, 0x01A5, 0x00DE, 0x00CB, 0x0195, 0x00C6, 0x00C6, 0x00CB, 0x01A5, 0x0290, 0x00DB, 0x00CA, 0x01A3]],
["COM:X24", "As Planned", [0x0112, 0x00C5, 0x00F6, 0x018F, 0x00DB, 0x00CB, 0x019D, 0x01A4, 0x00CC, 0x00CA, 0x00F6, 0x01AE, 0x00CC, 0x018F, 0x00D8, 0x00CB, 0x00DB, 0x019D, 0x00CA, 0x00C6, 0x01AE, 0x00C5, 0x00D8, 0x00CB, 0x018F, 0x00CA, 0x01A4, 0x00CC, 0x00F6, 0x00C6, 0x019D]],
["COM:X25", "TP Up,Up", [0x0112, 0x00CA, 0x01A4, 0x0195, 0x00C5, 0x00D8, 0x018D, 0x01A5, 0x01A5, 0x00DB, 0x00CA, 0x01A4, 0x0195, 0x00C6, 0x00D8, 0x018D, 0x00DE, 0x00CB, 0x00DB, 0x01A5, 0x00C6, 0x0195, 0x01A4, 0x00C5, 0x018D, 0x00CB, 0x00DE, 0x00CB, 0x00CA, 0x00DB, 0x00C6]],
["COM:X26", "You Little!", [0x0112, 0x00CA, 0x0042, 0x01AA, 0x0291, 0x00C5, 0x000C, 0x0291, 0x00DB, 0x00CA, 0x025D, 0x000C, 0x0243, 0x00C6, 0x01AA, 0x0042, 0x00D8, 0x00CB, 0x00DB, 0x025D, 0x00DE, 0x00CC, 0x0243, 0x00C6, 0x00C5, 0x0291, 0x00D8, 0x00CB, 0x00CA, 0x00DE, 0x000C]],
["COM:X27", "Madness 6", [0x0112, 0x00C5, 0x0190, 0x00DC, 0x008F, 0x00CB, 0x0294, 0x00D9, 0x018F, 0x0224, 0x00A1, 0x00DC, 0x00C5, 0x0293, 0x0294, 0x00CB, 0x018F, 0x0224, 0x00A1, 0x00DC, 0x00CB, 0x008F, 0x0294, 0x0190, 0x018F, 0x00A1, 0x00D9, 0x0224, 0x0293, 0x00C5, 0x008F]],
["COM:X28", "Secret-Pow", [0x0112, 0x00C6, 0x0161, 0x00D8, 0x028B, 0x0230, 0x028C, 0x00DC, 0x0109, 0x00C3, 0x0109, 0x0161, 0x0030, 0x00C6, 0x00D8, 0x028B, 0x00DC, 0x0232, 0x00C3, 0x01A4, 0x00C3, 0x0161, 0x0030, 0x028B, 0x00C6, 0x0230, 0x028C, 0x00DC, 0x0232, 0x0109, 0x01A4]],
["COM:X29", "Frozen Eyes", [0x0112, 0x0161, 0x00C6, 0x00CA, 0x028B, 0x0232, 0x00CB, 0x028B, 0x028C, 0x00D9, 0x01A4, 0x0161, 0x00D8, 0x00C6, 0x00DC, 0x028B, 0x00DC, 0x00CB, 0x00C7, 0x01A4, 0x00C6, 0x028C, 0x00D8, 0x0161, 0x0232, 0x00DC, 0x00D8, 0x00CB, 0x00CA, 0x00C7, 0x00CA]],
["COM:X2A", "Too Weak", [0x0113, 0x00CA, 0x0177, 0x0249, 0x008A, 0x00CB, 0x0017, 0x008C, 0x0099, 0x00CA, 0x01A5, 0x00C6, 0x0177, 0x008A, 0x00C6, 0x0249, 0x0249, 0x0017, 0x00CB, 0x0099, 0x0210, 0x0210, 0x0177, 0x01A5, 0x00C6, 0x00CB, 0x008C, 0x0099, 0x0017, 0x00CA, 0x01A5]],
["COM:X2B", "Burdened", [0x0113, 0x00CA, 0x0243, 0x01A5, 0x00C5, 0x0028, 0x0210, 0x0009, 0x0017, 0x00CA, 0x01A5, 0x00C6, 0x00CB, 0x0243, 0x00C5, 0x0028, 0x0210, 0x0009, 0x008A, 0x00CB, 0x0017, 0x00C6, 0x0243, 0x01A5, 0x00C5, 0x00CB, 0x0210, 0x00CA, 0x00C6, 0x008A, 0x0017]],
["COM:X2C", "Scar-free", [0x0113, 0x00C5, 0x0246, 0x0176, 0x0017, 0x00C6, 0x0176, 0x0016, 0x0210, 0x0099, 0x00C5, 0x01A4, 0x0246, 0x0017, 0x00C6, 0x0210, 0x0176, 0x0210, 0x00CC, 0x0099, 0x01A4, 0x0093, 0x0246, 0x01A4, 0x00C6, 0x00CC, 0x0016, 0x00CC, 0x00C5, 0x0099, 0x0093]],
["COM:X2D", "Useless", [0x0113, 0x01A5, 0x0177, 0x00C6, 0x00CB, 0x0249, 0x0210, 0x0017, 0x00CA, 0x00C7, 0x01A5, 0x00C6, 0x00CA, 0x0177, 0x0210, 0x00C5, 0x008E, 0x0249, 0x00CB, 0x00C7, 0x00C6, 0x00C7, 0x0177, 0x01A5, 0x0249, 0x00C5, 0x00CB, 0x008E, 0x0017, 0x00CA, 0x0017]],
["COM:X2E", "Young One", [0x0113, 0x00CA, 0x00CB, 0x0254, 0x0249, 0x00C6, 0x009C, 0x0016, 0x0210, 0x00CA, 0x0017, 0x0099, 0x0249, 0x01A5, 0x00C6, 0x0254, 0x0099, 0x00CB, 0x0210, 0x0017, 0x00C6, 0x01A5, 0x00C5, 0x00CB, 0x0099, 0x0254, 0x0016, 0x00CA, 0x0210, 0x00C5, 0x0249]],
["COM:X2F", "Surrender", [0x0113, 0x00C6, 0x0249, 0x0254, 0x0099, 0x0016, 0x00C5, 0x00C7, 0x0210, 0x0254, 0x00CA, 0x0017, 0x0249, 0x0099, 0x00C6, 0x0099, 0x0016, 0x00CA, 0x00CB, 0x00C7, 0x00C6, 0x00CA, 0x0017, 0x00C5, 0x0254, 0x00CB, 0x0210, 0x00CB, 0x00C7, 0x0017, 0x0249]],
["COM:X30", "Too Heavy", [0x0113, 0x00C6, 0x0029, 0x0171, 0x01A5, 0x00C5, 0x008C, 0x0038, 0x0099, 0x00CA, 0x0171, 0x00CA, 0x0029, 0x01A5, 0x00C6, 0x008C, 0x00CB, 0x0171, 0x0099, 0x0269, 0x00AE, 0x0029, 0x00C5, 0x00CB, 0x008C, 0x0269, 0x00CB, 0x0038, 0x00CA, 0x0099, 0x0269]],
["COM:X31", "Crushed", [0x0113, 0x000D, 0x00C5, 0x024D, 0x00C6, 0x0099, 0x01A8, 0x00B1, 0x0232, 0x024D, 0x000D, 0x0017, 0x00C6, 0x0099, 0x024D, 0x00B1, 0x0232, 0x008C, 0x01A8, 0x0017, 0x008A, 0x00C6, 0x000D, 0x0230, 0x0232, 0x00B1, 0x01A8, 0x0017, 0x00C5, 0x008C, 0x00C5]],
["COM:X32", "Sword-tale", [0x0111, 0x00CA, 0x0009, 0x00C6, 0x0042, 0x008A, 0x0042, 0x01A0, 0x01A0, 0x00DB, 0x00C5, 0x00D8, 0x000B, 0x0009, 0x000B, 0x00DE, 0x0042, 0x00D8, 0x00CB, 0x00DB, 0x01A0, 0x00DE, 0x000B, 0x0009, 0x00C6, 0x00D8, 0x00CB, 0x00C5, 0x00DB, 0x00C6, 0x00DE]],
["COM:X33", "Tenacity", [0x0111, 0x00C5, 0x028C, 0x0042, 0x0290, 0x00D0, 0x00D8, 0x028D, 0x0042, 0x00DE, 0x00DB, 0x028C, 0x0290, 0x00D0, 0x00C6, 0x0224, 0x00A5, 0x00DB, 0x0042, 0x028C, 0x00C5, 0x00AE, 0x0290, 0x00D0, 0x028D, 0x0224, 0x00DB, 0x00C6, 0x00A5, 0x00AE, 0x028D]],
["COM:X34", "Melodic", [0x0111, 0x00C6, 0x01A1, 0x01A0, 0x00D8, 0x01A1, 0x0040, 0x00DB, 0x00C6, 0x01A0, 0x0041, 0x00DE, 0x0042, 0x00CC, 0x00D8, 0x01A0, 0x00DB, 0x00C5, 0x00DB, 0x0041, 0x00DE, 0x01A1, 0x0042, 0x00CC, 0x00CC, 0x00C5, 0x00D8, 0x00C5, 0x00C6, 0x00DE, 0x0040]],
["COM:X35", "Unspoken", [0x0111, 0x00C6, 0x00CA, 0x019D, 0x00C5, 0x01A4, 0x028C, 0x0099, 0x01A5, 0x00C7, 0x00CA, 0x01A4, 0x019D, 0x00C6, 0x0099, 0x00CB, 0x028C, 0x00DB, 0x00CB, 0x00C7, 0x01A5, 0x00C6, 0x01A5, 0x01A4, 0x00C5, 0x00CB, 0x028C, 0x00DB, 0x00CA, 0x00C7, 0x019D]],
["COM:X36", "North Star", [0x0111, 0x00F6, 0x00C6, 0x01A0, 0x0155, 0x00CA, 0x000B, 0x0009, 0x0042, 0x00DE, 0x00F6, 0x0042, 0x00C6, 0x01A0, 0x000B, 0x0155, 0x00CB, 0x00DB, 0x0009, 0x00DE, 0x00CA, 0x0042, 0x00CB, 0x000B, 0x00CB, 0x00CA, 0x00DB, 0x0009, 0x00F6, 0x00C6, 0x0155]],
["COM:X37", "Faultless", [0x0111, 0x013D, 0x019D, 0x018F, 0x00DF, 0x018D, 0x0230, 0x018D, 0x00DC, 0x013D, 0x01AF, 0x00CA, 0x019D, 0x01A4, 0x0230, 0x00DF, 0x018F, 0x00DC, 0x0232, 0x00DC, 0x01AF, 0x00CA, 0x019D, 0x01A4, 0x0230, 0x018F, 0x0232, 0x00DF, 0x0232, 0x013D, 0x00CA]],
["COM:X38", "Devotion", [0x0111, 0x0154, 0x0155, 0x00D8, 0x0230, 0x00CA, 0x0155, 0x0009, 0x00CB, 0x0104, 0x0042, 0x00CA, 0x0154, 0x0042, 0x0232, 0x00DC, 0x0155, 0x00CB, 0x00CB, 0x0009, 0x0104, 0x0230, 0x0154, 0x0232, 0x0042, 0x0232, 0x00CA, 0x00DC, 0x00F7, 0x0104, 0x0009]],
["COM:X39", "Formless", [0x0111, 0x0046, 0x00C6, 0x0293, 0x01B3, 0x0045, 0x00DC, 0x010E, 0x01A4, 0x0293, 0x01B3, 0x00C6, 0x0045, 0x00CC, 0x010E, 0x00D8, 0x0046, 0x00DC, 0x00CF, 0x021C, 0x01A4, 0x00C5, 0x00CC, 0x01A4, 0x00C6, 0x0045, 0x00D8, 0x00CF, 0x010E, 0x00C5, 0x0293]],
["COM:X3A", "Patched-up", [0x0114, 0x0138, 0x000C, 0x00C5, 0x0034, 0x008A, 0x0038, 0x017C, 0x008E, 0x0138, 0x000A, 0x01A4, 0x00CB, 0x008A, 0x0151, 0x008E, 0x00C5, 0x0186, 0x0211, 0x017D, 0x01A5, 0x0211, 0x00CB, 0x0017, 0x00CB, 0x0035, 0x008E, 0x00C5, 0x0138, 0x0211, 0x00CA]],
["COM:X3B", "Twin Guns!", [0x0114, 0x00C5, 0x000C, 0x00C6, 0x0151, 0x0211, 0x0151, 0x0186, 0x008F, 0x0143, 0x0143, 0x000C, 0x027E, 0x00C5, 0x0211, 0x0151, 0x008F, 0x00C6, 0x0223, 0x00CB, 0x0186, 0x0211, 0x00C5, 0x027E, 0x000C, 0x00C6, 0x0223, 0x0186, 0x0143, 0x00CB, 0x027E]],
["COM:X3C", "Reboot Nyah", [0x0114, 0x00C6, 0x0185, 0x00CB, 0x0249, 0x01A5, 0x0184, 0x00C5, 0x00CA, 0x00F6, 0x0181, 0x0185, 0x01A5, 0x00CB, 0x00C6, 0x008E, 0x0249, 0x008E, 0x00CA, 0x00C5, 0x0184, 0x00C6, 0x01A5, 0x0185, 0x00CB, 0x008E, 0x00CA, 0x0249, 0x0184, 0x00C5, 0x00F6]],
["COM:X3D", "Bang Nyah", [0x0114, 0x00C6, 0x000C, 0x0151, 0x00CB, 0x0151, 0x00CA, 0x0186, 0x00C5, 0x0143, 0x027E, 0x000C, 0x027E, 0x00CB, 0x0211, 0x0151, 0x00C5, 0x00CA, 0x00C5, 0x0186, 0x0143, 0x00C6, 0x027E, 0x000C, 0x00CA, 0x0211, 0x0186, 0x00CB, 0x0143, 0x00C6, 0x0211]],
["COM:X3E", "Combo Nyah", [0x0114, 0x0230, 0x00C5, 0x0270, 0x00BA, 0x00C6, 0x000A, 0x0034, 0x0283, 0x00CA, 0x00CF, 0x0283, 0x00BA, 0x0232, 0x00C1, 0x000A, 0x00C1, 0x00C6, 0x00CA, 0x0283, 0x00CF, 0x0230, 0x00C6, 0x0270, 0x00BA, 0x0232, 0x000A, 0x00C1, 0x00C5, 0x00CA, 0x0270]],
["COM:X3F", "Collection", [0x0114, 0x00C6, 0x00CA, 0x0184, 0x008D, 0x00C7, 0x0150, 0x01AA, 0x008C, 0x00CA, 0x01A8, 0x0150, 0x01A8, 0x0232, 0x008D, 0x0184, 0x008C, 0x00CB, 0x00C6, 0x01AA, 0x0232, 0x01A8, 0x00C7, 0x0184, 0x00CB, 0x008D, 0x00CB, 0x01AA, 0x00CA, 0x00C6, 0x0150]],
["COM:X40", "Surprise!", [0x0114, 0x00F6, 0x0151, 0x0232, 0x00CA, 0x017C, 0x00A3, 0x0162, 0x008F, 0x00F6, 0x0228, 0x0017, 0x0151, 0x00CA, 0x00A3, 0x017C, 0x008F, 0x00CB, 0x0162, 0x0228, 0x0151, 0x0232, 0x0017, 0x00CA, 0x017C, 0x00A3, 0x0162, 0x0228, 0x00F6, 0x00CB, 0x0017]],
["COM:X41", "Love Revo.", [0x0003, 0x00DE, 0x0040, 0x01A3, 0x00D8, 0x0041, 0x0042, 0x008A, 0x00DE, 0x0042, 0x018F, 0x0040, 0x018F, 0x00D8, 0x01A3, 0x0041, 0x008A, 0x00DB, 0x008A, 0x0042, 0x008C, 0x0040, 0x018F, 0x0041, 0x00D8, 0x00DB, 0x01A3, 0x00DB, 0x00DE, 0x008C, 0x008C]],
["COM:X42", "Love Date", [0x0003, 0x00CA, 0x0162, 0x0041, 0x00C5, 0x00CB, 0x00D8, 0x01A0, 0x00A3, 0x00CA, 0x0207, 0x01A3, 0x0162, 0x00D8, 0x0041, 0x00DE, 0x01A3, 0x0207, 0x00C5, 0x00A3, 0x01A0, 0x0207, 0x0162, 0x01A3, 0x00CB, 0x0041, 0x00DE, 0x00C5, 0x00CA, 0x00A3, 0x01A0]],
["COM:X43", "How Cute!", [0x0003, 0x019D, 0x0041, 0x00C5, 0x0042, 0x00CA, 0x00DB, 0x0042, 0x018F, 0x00D8, 0x00C5, 0x0041, 0x00C6, 0x019D, 0x00CA, 0x00DB, 0x0042, 0x00D8, 0x00C6, 0x00D8, 0x018F, 0x00CB, 0x019D, 0x0041, 0x00CA, 0x00C6, 0x00DB, 0x00C5, 0x00CB, 0x00CB, 0x018F]],
["COM:X44", "Leafy", [0x0003, 0x00CB, 0x0042, 0x028C, 0x00C5, 0x0030, 0x0290, 0x00D8, 0x00CB, 0x028B, 0x01A3, 0x00DB, 0x0042, 0x0030, 0x00C5, 0x0030, 0x028B, 0x00D8, 0x00C6, 0x00DE, 0x0290, 0x00DB, 0x01A3, 0x00C5, 0x028B, 0x00C6, 0x00D8, 0x00CC, 0x00CC, 0x00DE, 0x028C]],
["COM:X45", "Cutie", [0x0003, 0x00C5, 0x0041, 0x00D8, 0x0224, 0x018F, 0x019D, 0x00DB, 0x00C5, 0x00DE, 0x0041, 0x01A4, 0x0224, 0x00D8, 0x018F, 0x00DB, 0x00C6, 0x00DB, 0x019D, 0x00C6, 0x00DE, 0x01A4, 0x0224, 0x018F, 0x00C6, 0x00D8, 0x019D, 0x01A4, 0x00C5, 0x00DE, 0x0041]],
["COM:X46", "3,2,1", [0x0003, 0x00C5, 0x000E, 0x00CA, 0x019D, 0x01A5, 0x0290, 0x00C6, 0x00D8, 0x00C5, 0x01A4, 0x00CB, 0x000E, 0x01A5, 0x00CA, 0x0290, 0x019D, 0x00D8, 0x00C6, 0x00DB, 0x01A4, 0x00CB, 0x01A4, 0x00CA, 0x01A5, 0x00C6, 0x0290, 0x00D8, 0x00C5, 0x00CB, 0x019D]],
["COM:X47", "Diet", [0x0003, 0x028D, 0x00C5, 0x00DB, 0x00D8, 0x00C6, 0x01A3, 0x0040, 0x00C5, 0x0041, 0x00D8, 0x00CB, 0x0041, 0x028B, 0x00C6, 0x00DB, 0x01A3, 0x00C5, 0x00D8, 0x00CF, 0x00DE, 0x00CB, 0x00DB, 0x01A3, 0x00DE, 0x00C6, 0x0041, 0x00CF, 0x00CB, 0x00DE, 0x028B]],
["COM:X48", "Auto Sweep", [0x0006, 0x00C6, 0x0017, 0x000A, 0x008C, 0x0211, 0x0038, 0x00C5, 0x0093, 0x00C6, 0x0038, 0x0034, 0x008C, 0x0017, 0x00C5, 0x00CC, 0x00C6, 0x0093, 0x0017, 0x0038, 0x0211, 0x000A, 0x00C5, 0x0099, 0x00CC, 0x0034, 0x0093, 0x000A, 0x008C, 0x0211, 0x0034]],
["COM:X49", "Raw Bullet", [0x0006, 0x00CB, 0x0016, 0x0011, 0x01B0, 0x0186, 0x00CC, 0x00C6, 0x003D, 0x003D, 0x0016, 0x0011, 0x00CC, 0x0186, 0x01B0, 0x00C5, 0x0017, 0x00C6, 0x0017, 0x00CB, 0x00CB, 0x0016, 0x0011, 0x00CC, 0x0186, 0x00C6, 0x0017, 0x003D, 0x00C5, 0x01B0, 0x00C5]],
["COM:X4A", "Simul-Fire", [0x0006, 0x0034, 0x00C5, 0x0035, 0x0186, 0x00C5, 0x0186, 0x017C, 0x00C6, 0x00C6, 0x0017, 0x00CB, 0x01A5, 0x0035, 0x0211, 0x00CA, 0x0211, 0x0186, 0x00CA, 0x008C, 0x00C6, 0x0034, 0x0017, 0x00C5, 0x01A5, 0x00CB, 0x0211, 0x00CB, 0x017C, 0x008C, 0x00CA]],
["COM:X4B", "Acid Storm", [0x0006, 0x00F6, 0x014F, 0x0211, 0x026C, 0x00C6, 0x00C5, 0x01A5, 0x017D, 0x00CC, 0x0094, 0x00F6, 0x014F, 0x00CC, 0x026C, 0x00C5, 0x01A5, 0x017D, 0x0211, 0x00C5, 0x026C, 0x00F6, 0x0094, 0x014F, 0x0211, 0x00C6, 0x01A5, 0x00CC, 0x017D, 0x0094, 0x00C6]],
["COM:X4C", "Guns Set", [0x0006, 0x0223, 0x00C6, 0x017C, 0x0211, 0x017C, 0x0034, 0x00BA, 0x00C6, 0x0034, 0x000A, 0x0283, 0x000A, 0x0211, 0x00CB, 0x0211, 0x00CA, 0x00BA, 0x00BA, 0x0223, 0x00C6, 0x000A, 0x00CB, 0x0283, 0x00CB, 0x017C, 0x00CA, 0x0034, 0x0223, 0x0283, 0x00CA]],
["COM:X4D", "Gun's Soul", [0x0006, 0x0283, 0x000A, 0x00C6, 0x000A, 0x0143, 0x000C, 0x00CB, 0x0143, 0x00CA, 0x0034, 0x0283, 0x0211, 0x00CA, 0x00C5, 0x0232, 0x00CB, 0x00CB, 0x0232, 0x000C, 0x00CA, 0x0211, 0x00C6, 0x0034, 0x00C6, 0x0283, 0x0232, 0x00C5, 0x000C, 0x000A, 0x0143]],
["COM:X4E", "Gun Blast", [0x0006, 0x0143, 0x000A, 0x0151, 0x000A, 0x00CB, 0x0034, 0x0211, 0x00CA, 0x000C, 0x000C, 0x0283, 0x00CB, 0x0283, 0x0017, 0x0223, 0x000A, 0x0034, 0x0211, 0x00C6, 0x0283, 0x000C, 0x00CB, 0x0151, 0x00CA, 0x0017, 0x00CA, 0x0211, 0x0223, 0x0143, 0x00C6]],
["COM:X4F", "Play Safe", [0x0115, 0x0138, 0x00CB, 0x0291, 0x018F, 0x028C, 0x00C5, 0x00DB, 0x0138, 0x018D, 0x0290, 0x0040, 0x028D, 0x00CB, 0x00D8, 0x019D, 0x00D8, 0x00C5, 0x028B, 0x00DE, 0x00DE, 0x0255, 0x00CB, 0x0042, 0x00CA, 0x00C5, 0x00DB, 0x0195, 0x00DB, 0x0138, 0x0041]],
["COM:X50", "Mag Solo", [0x0115, 0x0016, 0x00DE, 0x00CB, 0x01AE, 0x00CB, 0x01B0, 0x008C, 0x01B0, 0x00C6, 0x01B0, 0x0016, 0x003D, 0x00C5, 0x01AE, 0x008C, 0x00C5, 0x0099, 0x00D8, 0x00DB, 0x00DB, 0x003D, 0x00CB, 0x0030, 0x008A, 0x00C5, 0x01AE, 0x00C6, 0x0030, 0x00D8, 0x00C6]],
["COM:X51", "Silence", [0x0115, 0x0249, 0x00CC, 0x00A3, 0x0255, 0x00C5, 0x00B7, 0x028D, 0x008F, 0x028D, 0x01A4, 0x0249, 0x00A3, 0x00C5, 0x0255, 0x00B7, 0x00C6, 0x00F6, 0x008F, 0x00F6, 0x00CC, 0x01A4, 0x00C5, 0x0249, 0x00B7, 0x00C6, 0x0255, 0x00C6, 0x028D, 0x008F, 0x00F6]],
["COM:X52", "Letting Go", [0x0115, 0x00C6, 0x00C5, 0x0190, 0x018F, 0x01A5, 0x00C5, 0x01A4, 0x00CB, 0x00C7, 0x01B0, 0x0190, 0x00D8, 0x00CA, 0x00DB, 0x01A4, 0x01A5, 0x00C7, 0x00CA, 0x00DB, 0x00C6, 0x01B0, 0x0190, 0x00C5, 0x00D8, 0x00CB, 0x00DB, 0x00CB, 0x00C7, 0x00CA, 0x018F]],
["COM:X53", "Serious", [0x0115, 0x028A, 0x00C5, 0x00DE, 0x0040, 0x0040, 0x00C6, 0x0041, 0x00DC, 0x00C5, 0x0042, 0x028A, 0x00DE, 0x00CB, 0x00DB, 0x0040, 0x00D8, 0x0041, 0x00DC, 0x00C6, 0x00A1, 0x0042, 0x00C5, 0x00A1, 0x028A, 0x00CB, 0x00DB, 0x00C6, 0x00D8, 0x0041, 0x00A1]],
["COM:X54", "Life's Bet", [0x0115, 0x00CA, 0x0154, 0x00B8, 0x00C5, 0x0010, 0x00C7, 0x00B1, 0x00CB, 0x00DC, 0x00DC, 0x00C5, 0x0154, 0x0232, 0x00B8, 0x0010, 0x00B1, 0x00CB, 0x025E, 0x00CA, 0x00C6, 0x00B8, 0x0154, 0x00C7, 0x00B1, 0x00CB, 0x0010, 0x00CA, 0x025E, 0x0232, 0x00C6]],
["COM:X55", "Good Bet", [0x0115, 0x00F6, 0x0232, 0x00D9, 0x0010, 0x00CB, 0x025E, 0x00CA, 0x0230, 0x025E, 0x00D9, 0x0154, 0x0154, 0x00B8, 0x0010, 0x00B8, 0x0010, 0x00B1, 0x00CA, 0x00B1, 0x0230, 0x00D9, 0x0154, 0x00CB, 0x00B8, 0x00CB, 0x00B1, 0x00CA, 0x025E, 0x00F6, 0x0232]],
["COM:X56", "Lucky Dice", [0x0115, 0x0294, 0x00F7, 0x00CB, 0x0045, 0x00DC, 0x01A4, 0x00DC, 0x01A5, 0x00F7, 0x01A5, 0x0294, 0x00DF, 0x0045, 0x00CA, 0x00DF, 0x0232, 0x00DC, 0x008F, 0x01A4, 0x00C6, 0x00D9, 0x0294, 0x0232, 0x00DF, 0x00CA, 0x0045, 0x00CA, 0x00F7, 0x008F, 0x00CB]],
["COM:X57", "BZZRZZR", [0x0005, 0x0171, 0x00C5, 0x0017, 0x00CC, 0x01A5, 0x0185, 0x0035, 0x00C6, 0x008E, 0x0017, 0x0171, 0x00CC, 0x01A5, 0x0185, 0x00C6, 0x008A, 0x0035, 0x008A, 0x00C5, 0x008E, 0x0017, 0x0171, 0x00CC, 0x00C6, 0x01A5, 0x0185, 0x008A, 0x00C5, 0x0035, 0x008E]],
["COM:X58", "BZTBZTBZT", [0x0005, 0x00CA, 0x000A, 0x0034, 0x0016, 0x00CB, 0x01A3, 0x0034, 0x00CC, 0x00C6, 0x0035, 0x000A, 0x0035, 0x00C5, 0x00C6, 0x0016, 0x01A3, 0x00CB, 0x00CC, 0x00CA, 0x00C5, 0x0035, 0x00C5, 0x000A, 0x0016, 0x0034, 0x00CB, 0x00CC, 0x00CA, 0x01A3, 0x00C6]],
["COM:X59", "BZZANG!", [0x0005, 0x00F6, 0x00C5, 0x01A5, 0x00C6, 0x008F, 0x00CB, 0x0181, 0x00CC, 0x00CB, 0x01A4, 0x00C5, 0x0017, 0x01A5, 0x0181, 0x00F6, 0x008F, 0x00C6, 0x0017, 0x01A4, 0x0181, 0x00CB, 0x00CC, 0x01A5, 0x00F6, 0x00C5, 0x0017, 0x01A4, 0x008F, 0x00C6, 0x00CC]],
["COM:X5A", "GZZUGGH!", [0x0005, 0x00CA, 0x0185, 0x01A5, 0x000A, 0x01A5, 0x000A, 0x008F, 0x000C, 0x00CA, 0x0017, 0x0185, 0x00CB, 0x01A5, 0x0232, 0x00CA, 0x008F, 0x00C6, 0x008F, 0x000C, 0x00C6, 0x0017, 0x0232, 0x0185, 0x0232, 0x000A, 0x00CB, 0x0017, 0x000C, 0x00CB, 0x00C6]],
["COM:X5B", "BZAHBZAH", [0x0005, 0x00F7, 0x0186, 0x01A5, 0x0249, 0x0232, 0x01A8, 0x0232, 0x00C5, 0x00F7, 0x01AC, 0x0186, 0x01AC, 0x0230, 0x01A5, 0x00CC, 0x00C6, 0x0249, 0x01A8, 0x00F7, 0x0298, 0x01AC, 0x0298, 0x0186, 0x0230, 0x0249, 0x0232, 0x00CC, 0x01A8, 0x00C6, 0x0298]],
["COM:X5C", "GZZ...GAAH!", [0x0005, 0x0143, 0x0283, 0x009A, 0x000A, 0x00CA, 0x000A, 0x0034, 0x0223, 0x0143, 0x017C, 0x00CB, 0x009A, 0x00CB, 0x0211, 0x000A, 0x0211, 0x0034, 0x00CC, 0x0034, 0x00C6, 0x009A, 0x00CB, 0x0283, 0x00CA, 0x0211, 0x00CA, 0x00CC, 0x0223, 0x00C6, 0x0283]],
["COM:X5D", "Blue Eyes", [0x0008, 0x00CC, 0x0061, 0x008A, 0x00C6, 0x0064, 0x00DE, 0x0064, 0x0078, 0x00D8, 0x0061, 0x005F, 0x00DE, 0x008A, 0x0064, 0x008C, 0x0078, 0x0093, 0x00CC, 0x00D8, 0x00DB, 0x005F, 0x00DB, 0x0061, 0x00DE, 0x0093, 0x00C5, 0x00D8, 0x00C6, 0x0078, 0x00DB]],
["COM:X5E", "Wolves", [0x0008, 0x00CA, 0x005A, 0x00CB, 0x005A, 0x00D8, 0x00CC, 0x00C5, 0x0059, 0x00CA, 0x005E, 0x0059, 0x005E, 0x00C6, 0x00D8, 0x00CB, 0x00CC, 0x0078, 0x00D8, 0x0078, 0x00C5, 0x005E, 0x00C6, 0x005A, 0x00C6, 0x0059, 0x00CB, 0x00CC, 0x00CA, 0x00C5, 0x0078]],
["COM:X5F", "Native Wolf", [0x0008, 0x0056, 0x00CC, 0x005F, 0x0059, 0x00C5, 0x0059, 0x005A, 0x00C6, 0x00D8, 0x005B, 0x0056, 0x005F, 0x021E, 0x020C, 0x0059, 0x020C, 0x005A, 0x00D8, 0x00CC, 0x00DB, 0x005B, 0x021E, 0x0056, 0x00C5, 0x020C, 0x00C6, 0x00D8, 0x00C6, 0x005A, 0x021E]],
["COM:X60", "Revert", [0x0008, 0x022B, 0x005B, 0x005C, 0x005F, 0x00C6, 0x0068, 0x00DE, 0x0068, 0x00C5, 0x01E0, 0x005B, 0x01E0, 0x00C5, 0x005C, 0x005F, 0x00DE, 0x00C6, 0x00D8, 0x022B, 0x00CB, 0x01E0, 0x00C5, 0x005B, 0x00D8, 0x005C, 0x00C6, 0x005F, 0x022B, 0x0068, 0x00CB]],
["COM:X61", "Darkness", [0x0008, 0x0141, 0x0074, 0x00C5, 0x020F, 0x007D, 0x007D, 0x01E0, 0x0221, 0x01E0, 0x0074, 0x0078, 0x0221, 0x007D, 0x020F, 0x0141, 0x01E0, 0x008A, 0x00D8, 0x008C, 0x00D8, 0x0078, 0x00C6, 0x0074, 0x00C5, 0x0078, 0x020F, 0x0221, 0x0141, 0x008A, 0x00C6]],
["COM:X62", "Mad Flight", [0x0008, 0x005B, 0x00C6, 0x005E, 0x00C5, 0x00B1, 0x0078, 0x00B8, 0x00CB, 0x0078, 0x01F1, 0x00CA, 0x005E, 0x00CB, 0x01F1, 0x00B1, 0x0078, 0x00CA, 0x00B8, 0x00C6, 0x005B, 0x00C5, 0x01F1, 0x00C6, 0x00B1, 0x005E, 0x00CB, 0x00B8, 0x00CA, 0x005B, 0x00C5]],
["COM:X63", "Full Beast", [0x0008, 0x01D2, 0x013F, 0x00D8, 0x00CA, 0x006C, 0x0090, 0x0061, 0x021F, 0x0068, 0x01D2, 0x0068, 0x00DE, 0x00D8, 0x021F, 0x013F, 0x0090, 0x006C, 0x0061, 0x020D, 0x020D, 0x0068, 0x00C5, 0x01D2, 0x00CA, 0x006C, 0x021F, 0x0090, 0x013F, 0x020D, 0x00C5]],
["COM:X64", "MechAttack", [0x0008, 0x01D7, 0x0140, 0x00CA, 0x01DA, 0x00D9, 0x01DA, 0x0070, 0x006F, 0x021A, 0x01D7, 0x00D9, 0x00CA, 0x006F, 0x020E, 0x01DA, 0x020E, 0x0220, 0x021A, 0x00C5, 0x00CB, 0x006F, 0x00CB, 0x01D7, 0x00CA, 0x020E, 0x0220, 0x0070, 0x0220, 0x0140, 0x00CB]],
["COM:X65", "Guardian", [0x0008, 0x00CA, 0x01D2, 0x00D8, 0x005A, 0x00CB, 0x005A, 0x0059, 0x00CB, 0x00CC, 0x0078, 0x01D2, 0x0078, 0x00C6, 0x00D8, 0x005A, 0x00C5, 0x0059, 0x00CA, 0x0059, 0x0078, 0x00C5, 0x01D2, 0x00C6, 0x00DB, 0x00C6, 0x00CC, 0x00CB, 0x00DB, 0x00CA, 0x00C5]],
["COM:X66", "Pierce", [0x0008, 0x00F7, 0x0068, 0x008B, 0x01F3, 0x00C6, 0x01C8, 0x01C8, 0x0234, 0x00CB, 0x0078, 0x00C5, 0x008B, 0x00C6, 0x008B, 0x01F3, 0x00D9, 0x01C8, 0x00CB, 0x0078, 0x0056, 0x00C5, 0x0068, 0x01F3, 0x00C6, 0x00DF, 0x0234, 0x00CB, 0x00F7, 0x00F7, 0x00C5]],
["COM:X67", "Equal Red", [0x0008, 0x00CB, 0x006C, 0x00D9, 0x005E, 0x00CF, 0x00DF, 0x00C5, 0x0090, 0x005D, 0x01C7, 0x006C, 0x00C6, 0x01C7, 0x00CF, 0x00D9, 0x005E, 0x00C5, 0x005D, 0x0090, 0x00C6, 0x01C7, 0x006C, 0x00CF, 0x005D, 0x00DF, 0x005E, 0x00C5, 0x0090, 0x00CB, 0x00C6]],
["COM:X68", "Last Battle", [0x0008, 0x013B, 0x01D6, 0x023E, 0x01C8, 0x00C6, 0x007A, 0x012E, 0x013B, 0x00CB, 0x0068, 0x01D6, 0x023E, 0x00CA, 0x00C6, 0x00F6, 0x00C6, 0x007A, 0x00C5, 0x013B, 0x00CB, 0x0068, 0x00CA, 0x01C8, 0x00F6, 0x01C8, 0x012E, 0x00C5, 0x007A, 0x0068, 0x00CA]],
["COM:X69", "Gimme EX!", [0x011D, 0x006F, 0x0213, 0x01DC, 0x0070, 0x00C5, 0x0070, 0x00C6, 0x01D1, 0x00D8, 0x01DC, 0x006F, 0x00C6, 0x00CC, 0x0213, 0x0070, 0x00D8, 0x00C5, 0x01D1, 0x00DB, 0x00DE, 0x01DC, 0x00DE, 0x006F, 0x00CC, 0x0213, 0x00C5, 0x00D8, 0x00C6, 0x01D1, 0x00DE]],
["COM:X6A", "Dice Obey", [0x011D, 0x00F6, 0x005A, 0x00CB, 0x0064, 0x00C5, 0x01DF, 0x01DF, 0x00A3, 0x00F6, 0x005F, 0x005A, 0x0068, 0x0064, 0x008F, 0x00CB, 0x00A3, 0x01DF, 0x00C5, 0x005F, 0x0207, 0x0068, 0x00CB, 0x008F, 0x0064, 0x008F, 0x00C5, 0x00A3, 0x00F6, 0x0207, 0x00C6]],
["COM:X6B", "My Servant", [0x011D, 0x0134, 0x006F, 0x00CB, 0x01D1, 0x01D1, 0x0093, 0x0068, 0x00CA, 0x0134, 0x01D2, 0x006F, 0x020D, 0x006F, 0x020D, 0x00CA, 0x0093, 0x0068, 0x00D8, 0x01D2, 0x00DE, 0x006C, 0x00C6, 0x020D, 0x00CB, 0x01D1, 0x00CA, 0x00D8, 0x0134, 0x00DE, 0x00C6]],
["COM:X6C", "Beg Me", [0x011D, 0x00F6, 0x01E1, 0x00D8, 0x00C5, 0x007C, 0x0221, 0x007D, 0x008F, 0x00F6, 0x0078, 0x01E1, 0x00D8, 0x00DE, 0x0221, 0x00C5, 0x00B7, 0x007C, 0x007D, 0x008F, 0x008F, 0x0078, 0x00C6, 0x01E1, 0x00B7, 0x007C, 0x0221, 0x00B7, 0x00F6, 0x007D, 0x00C6]],
["COM:X6D", "Die 3 Times", [0x011D, 0x006F, 0x00CA, 0x01E1, 0x01DA, 0x0236, 0x01DA, 0x00DC, 0x0068, 0x00F7, 0x0069, 0x01E1, 0x006F, 0x00CA, 0x00D8, 0x01DA, 0x00D9, 0x0236, 0x00DC, 0x0069, 0x008F, 0x008F, 0x01E1, 0x00CA, 0x00D8, 0x00D9, 0x0236, 0x006A, 0x00F7, 0x008F, 0x00F7]],
["COM:X6E", "Stinky Pigs", [0x011D, 0x00CA, 0x01E1, 0x00DF, 0x0061, 0x0236, 0x008F, 0x01DF, 0x00F7, 0x0234, 0x00D9, 0x01E1, 0x00D9, 0x00CA, 0x00DF, 0x0061, 0x0134, 0x008F, 0x01DF, 0x00F7, 0x0234, 0x00D9, 0x00CA, 0x01E1, 0x0236, 0x00DF, 0x0061, 0x0134, 0x008F, 0x01DF, 0x00F7]],
["COM:X6F", "Servant Bow", [0x011D, 0x00C5, 0x005C, 0x00B1, 0x01DD, 0x00CB, 0x00C1, 0x0068, 0x00CA, 0x00BA, 0x0078, 0x005C, 0x00B1, 0x00C6, 0x00B1, 0x01DD, 0x00CB, 0x0068, 0x00BA, 0x00CA, 0x00C5, 0x0078, 0x00C6, 0x005C, 0x00C6, 0x00C1, 0x01DD, 0x00CB, 0x00BA, 0x0068, 0x00CA]],
["COM:X70", "Spit Out", [0x011D, 0x00C5, 0x0066, 0x00D8, 0x006F, 0x0222, 0x006F, 0x0134, 0x01D1, 0x00C5, 0x01DA, 0x0066, 0x01DA, 0x0236, 0x00D8, 0x0235, 0x0222, 0x01D1, 0x00C6, 0x0133, 0x00C5, 0x01DA, 0x0236, 0x0066, 0x0235, 0x006F, 0x0235, 0x00C6, 0x0134, 0x01D1, 0x0133]],
["COM:X71", "Rampage", [0x0116, 0x00A3, 0x0078, 0x0093, 0x00A3, 0x009C, 0x005B, 0x00DB, 0x005B, 0x00D8, 0x008A, 0x0078, 0x0093, 0x0056, 0x0093, 0x0056, 0x008C, 0x005B, 0x008E, 0x008A, 0x008E, 0x008A, 0x00A3, 0x0078, 0x009C, 0x008C, 0x00DB, 0x008C, 0x00D8, 0x008E, 0x00D8]],
["COM:X72", "Ultimate #1", [0x0116, 0x00C5, 0x0068, 0x008E, 0x0061, 0x00C6, 0x008A, 0x0093, 0x008A, 0x00CC, 0x008C, 0x0068, 0x008E, 0x00C5, 0x008E, 0x0061, 0x00C6, 0x0093, 0x021F, 0x0213, 0x0213, 0x008C, 0x00C5, 0x0068, 0x00C6, 0x0093, 0x008A, 0x021F, 0x0213, 0x008C, 0x00F0]],
["COM:X73", "Ill Intent", [0x0116, 0x00C6, 0x0068, 0x008C, 0x01D2, 0x00CB, 0x01D2, 0x00CB, 0x01F1, 0x00C6, 0x0093, 0x0068, 0x0093, 0x00C5, 0x008C, 0x01D2, 0x00CC, 0x01F1, 0x00CC, 0x00CA, 0x00C6, 0x0093, 0x00C5, 0x0068, 0x00C5, 0x008C, 0x00CB, 0x00CC, 0x00CA, 0x01F1, 0x00CA]],
["COM:X74", "Flash", [0x0116, 0x00C6, 0x0068, 0x00C6, 0x007C, 0x021F, 0x008A, 0x0213, 0x008A, 0x0137, 0x008C, 0x00C6, 0x008E, 0x0068, 0x008E, 0x007C, 0x0213, 0x021F, 0x00CC, 0x00C5, 0x00C5, 0x008C, 0x0068, 0x008E, 0x021F, 0x0213, 0x008A, 0x00CC, 0x00C5, 0x008C, 0x0137]],
["COM:X75", "Firey", [0x0116, 0x00CC, 0x0064, 0x00A3, 0x0068, 0x00C5, 0x01DF, 0x00CB, 0x01DF, 0x00CC, 0x008F, 0x0064, 0x008F, 0x00CA, 0x00A3, 0x0068, 0x0207, 0x01DF, 0x0207, 0x00CB, 0x00CC, 0x008F, 0x00CA, 0x0068, 0x00CA, 0x00A3, 0x00C5, 0x0207, 0x00C5, 0x0095, 0x00CB]],
["COM:X76", "Great Joy", [0x0116, 0x00C5, 0x0068, 0x008C, 0x01C7, 0x00C6, 0x01C7, 0x00CA, 0x00A0, 0x00CC, 0x008A, 0x0068, 0x008C, 0x00CB, 0x008B, 0x01C7, 0x00CA, 0x00C6, 0x00CA, 0x00A0, 0x00C5, 0x008A, 0x00CB, 0x0068, 0x00CB, 0x008B, 0x00C6, 0x00A0, 0x00CC, 0x00C5, 0x00CC]],
["COM:X77", "Focused", [0x0116, 0x00C5, 0x0068, 0x008B, 0x01C7, 0x00C6, 0x01C7, 0x00CB, 0x01DE, 0x0234, 0x008E, 0x0068, 0x00C5, 0x008E, 0x00C6, 0x008B, 0x00A0, 0x00CB, 0x00A0, 0x01DE, 0x00C5, 0x008E, 0x0068, 0x01C7, 0x00C6, 0x008B, 0x01DE, 0x00A0, 0x00CB, 0x008F, 0x0234]],
["COM:X78", "Flower Riot", [0x0116, 0x00C5, 0x0057, 0x00DF, 0x01F3, 0x00C6, 0x01F3, 0x0225, 0x00C6, 0x008B, 0x005A, 0x0057, 0x00DF, 0x00C6, 0x0090, 0x01F3, 0x0090, 0x01D7, 0x008B, 0x00CB, 0x00C5, 0x005A, 0x00C5, 0x0057, 0x01D7, 0x0090, 0x0225, 0x008B, 0x00CB, 0x01D7, 0x00CB]],
["COM:X79", "Gentle Rain", [0x011A, 0x00DE, 0x0058, 0x00CB, 0x0056, 0x0074, 0x00C6, 0x00D8, 0x005C, 0x00CB, 0x005F, 0x0058, 0x0056, 0x00C5, 0x0056, 0x0074, 0x00C6, 0x00DB, 0x005C, 0x00CB, 0x00DE, 0x005F, 0x00C5, 0x0058, 0x00C5, 0x00D8, 0x0074, 0x00C6, 0x00DB, 0x00DE, 0x005C]],
["COM:X7A", "Dead Knot", [0x011A, 0x01E0, 0x006B, 0x009A, 0x0074, 0x00C6, 0x00C6, 0x0058, 0x0093, 0x01E0, 0x006C, 0x00C5, 0x009A, 0x00CB, 0x0074, 0x00C6, 0x0058, 0x008C, 0x0093, 0x00C5, 0x01E0, 0x006C, 0x00CB, 0x006B, 0x00CB, 0x009A, 0x008C, 0x0074, 0x00C5, 0x0058, 0x006B]],
["COM:X7B", "Native Soul", [0x011A, 0x00C5, 0x01E8, 0x020C, 0x005E, 0x00CB, 0x021E, 0x005B, 0x00CA, 0x00C5, 0x01E8, 0x0056, 0x00C6, 0x0056, 0x005E, 0x020C, 0x021E, 0x00CB, 0x00CA, 0x005B, 0x00C5, 0x0056, 0x00C6, 0x01E8, 0x00C6, 0x020C, 0x005E, 0x00CB, 0x021E, 0x005B, 0x00CA]],
["COM:X7C", "Sacrifice", [0x011A, 0x00CF, 0x005C, 0x01E0, 0x01DD, 0x00CA, 0x01DD, 0x00CB, 0x005B, 0x00B1, 0x00CB, 0x01DC, 0x00C5, 0x005C, 0x01DD, 0x00B8, 0x00CA, 0x00B8, 0x00CB, 0x00B1, 0x00CF, 0x01DC, 0x00C5, 0x005C, 0x00C5, 0x00B8, 0x00CA, 0x00B1, 0x01DC, 0x005B, 0x01E4]],
["COM:X7D", "Beauty", [0x011A, 0x00DC, 0x0068, 0x00B8, 0x0069, 0x00CB, 0x0069, 0x00CB, 0x006F, 0x00D9, 0x0070, 0x0070, 0x00CC, 0x00CA, 0x0069, 0x00B8, 0x006F, 0x00B2, 0x00CA, 0x00D9, 0x00DC, 0x0070, 0x00CC, 0x0068, 0x00CC, 0x00B8, 0x00CB, 0x00B2, 0x006A, 0x006F, 0x00CA]],
["COM:X7E", "Mince", [0x011A, 0x00C6, 0x006A, 0x008F, 0x00CB, 0x0069, 0x006F, 0x0070, 0x00C5, 0x00F7, 0x0070, 0x006A, 0x00C5, 0x00CB, 0x0069, 0x008F, 0x00CA, 0x00C5, 0x00F7, 0x006F, 0x00C6, 0x0070, 0x00CB, 0x006A, 0x008F, 0x00CA, 0x0069, 0x00CA, 0x006F, 0x00C6, 0x00F7]],
["COM:X7F", "Trust Lost", [0x011A, 0x0068, 0x0056, 0x00C5, 0x008F, 0x0222, 0x00A0, 0x008C, 0x01E1, 0x00C6, 0x0068, 0x0216, 0x00C5, 0x0090, 0x008F, 0x005F, 0x01E1, 0x0222, 0x00C6, 0x00A0, 0x009A, 0x0068, 0x0090, 0x00C5, 0x0216, 0x00A0, 0x00C6, 0x008C, 0x005F, 0x0222, 0x008F]],
["COM:X80", "Full Might", [0x011A, 0x00C5, 0x01D2, 0x0068, 0x00CB, 0x00DC, 0x0069, 0x00CA, 0x00DE, 0x0061, 0x006A, 0x01D2, 0x020D, 0x00CC, 0x00CB, 0x0069, 0x0061, 0x00DC, 0x021F, 0x00CA, 0x021F, 0x01D2, 0x0068, 0x00CC, 0x020D, 0x0069, 0x00CB, 0x00DE, 0x0061, 0x00CA, 0x00F7]],
["COM:X81", "Returning", [0x011A, 0x022B, 0x01E0, 0x01DD, 0x00CB, 0x00B8, 0x005C, 0x00C5, 0x00B1, 0x005B, 0x01E0, 0x01DD, 0x00C6, 0x01DC, 0x005C, 0x00C5, 0x00B8, 0x005B, 0x0125, 0x00B1, 0x022B, 0x01E0, 0x01DD, 0x00CB, 0x0079, 0x005C, 0x00C5, 0x00B1, 0x005B, 0x0125, 0x0125]],
["COM:X82", "Algol's Light", [0x0117, 0x0068, 0x00C5, 0x00F6, 0x008A, 0x00C6, 0x005F, 0x021E, 0x020C, 0x00CB, 0x0068, 0x008E, 0x008F, 0x021F, 0x0056, 0x020C, 0x005F, 0x00C6, 0x00CB, 0x008E, 0x00F6, 0x0068, 0x00C5, 0x021F, 0x020C, 0x0056, 0x00C6, 0x005F, 0x021E, 0x00CB, 0x008F]],
["COM:X83", "Kick It!", [0x0117, 0x00CF, 0x0056, 0x0063, 0x00CB, 0x00B4, 0x01D2, 0x00CA, 0x0216, 0x0078, 0x0056, 0x0063, 0x00CB, 0x00B7, 0x01D2, 0x00CA, 0x00B4, 0x0078, 0x00F7, 0x0216, 0x00CF, 0x0056, 0x0063, 0x00CB, 0x00B7, 0x01D2, 0x00CA, 0x0216, 0x0078, 0x00F7, 0x00F7]],
["COM:X84", "Always", [0x0117, 0x00C6, 0x0209, 0x0067, 0x00CA, 0x0208, 0x0061, 0x021F, 0x020D, 0x022C, 0x01D2, 0x0067, 0x00C6, 0x0209, 0x01D1, 0x021F, 0x0208, 0x0061, 0x022C, 0x01D2, 0x00C6, 0x0209, 0x0067, 0x00CA, 0x0208, 0x01D1, 0x021F, 0x020D, 0x01D2, 0x020D, 0x022C]],
["COM:X85", "Crazy Life", [0x0117, 0x00CF, 0x0209, 0x0067, 0x00C6, 0x021F, 0x020D, 0x0061, 0x022C, 0x01D2, 0x01D2, 0x0067, 0x021F, 0x0209, 0x01D1, 0x022C, 0x020D, 0x0061, 0x00CA, 0x00CA, 0x00CF, 0x0209, 0x0067, 0x021F, 0x020D, 0x01D1, 0x022C, 0x00C6, 0x01D2, 0x00C6, 0x00CA]],
["COM:X86", "Creature=0", [0x011B, 0x00CC, 0x0213, 0x008A, 0x00CB, 0x008F, 0x008C, 0x00C6, 0x009A, 0x008E, 0x008A, 0x0213, 0x008C, 0x00CB, 0x009A, 0x00C6, 0x00C6, 0x008F, 0x008E, 0x00C5, 0x00CC, 0x0213, 0x008A, 0x00CB, 0x008F, 0x008C, 0x008E, 0x009A, 0x00C5, 0x00CC, 0x00C5]],
["COM:X87", "Solo Ver.1", [0x011B, 0x00C5, 0x0068, 0x00CB, 0x00A0, 0x00F6, 0x00A0, 0x00C6, 0x00CC, 0x00C5, 0x0068, 0x008C, 0x00F6, 0x008B, 0x00C6, 0x0068, 0x00CA, 0x008A, 0x008C, 0x00CC, 0x00C5, 0x008B, 0x00CB, 0x00CB, 0x00F6, 0x00A0, 0x00C6, 0x00CA, 0x008A, 0x00CA, 0x00CC]],
["COM:X88", "Charge=ON", [0x011B, 0x008A, 0x00C5, 0x007C, 0x0093, 0x0099, 0x0068, 0x00C6, 0x0213, 0x008A, 0x00CB, 0x00F0, 0x0213, 0x00C5, 0x0099, 0x006B, 0x007C, 0x0068, 0x00C6, 0x006B, 0x0093, 0x008A, 0x00CB, 0x00C5, 0x007C, 0x0099, 0x00C6, 0x006B, 0x0213, 0x0068, 0x00CB]],
["COM:X89", "Hit=Direct", [0x011B, 0x022B, 0x00A0, 0x0068, 0x00CB, 0x0099, 0x01DE, 0x00CA, 0x00C5, 0x00F6, 0x0068, 0x009A, 0x00A0, 0x022B, 0x00CA, 0x01DE, 0x0095, 0x0213, 0x009A, 0x00F6, 0x00A0, 0x00C5, 0x00CB, 0x0068, 0x01DE, 0x0099, 0x0213, 0x00CA, 0x00C5, 0x0095, 0x00F6]],
["COM:X8A", "Solo Ver.2", [0x011B, 0x00C5, 0x008C, 0x0068, 0x00CB, 0x008B, 0x01C7, 0x00C6, 0x00CA, 0x00CC, 0x0068, 0x008A, 0x008C, 0x00CB, 0x00C6, 0x01C7, 0x00A0, 0x00CA, 0x00A0, 0x00CC, 0x008A, 0x00C5, 0x00CB, 0x0068, 0x01C7, 0x008B, 0x00CA, 0x00C6, 0x00C5, 0x00A0, 0x00CC]],
["COM:X8B", "Life Value", [0x011B, 0x00C5, 0x00A0, 0x01C8, 0x00CB, 0x008B, 0x01D7, 0x00C6, 0x008E, 0x00CA, 0x01C8, 0x00B8, 0x00A0, 0x00CB, 0x00C6, 0x01D7, 0x01C7, 0x008B, 0x00B8, 0x00CA, 0x00A0, 0x00C5, 0x00CB, 0x01C8, 0x01D7, 0x008B, 0x008E, 0x00C6, 0x00C5, 0x01C7, 0x00CA]],
["COM:X8C", "Diagnosis", [0x011B, 0x00C5, 0x00A0, 0x01C8, 0x01D7, 0x008B, 0x01D7, 0x00C6, 0x008E, 0x00CC, 0x00B8, 0x01C8, 0x00CB, 0x00A0, 0x00CB, 0x00C6, 0x008B, 0x01C7, 0x00CC, 0x00B8, 0x00C5, 0x00A0, 0x01C8, 0x00CB, 0x008B, 0x01D7, 0x00C6, 0x008E, 0x01C7, 0x00C5, 0x00CC]],
["COM:X8D", "Hawk Speed", [0x011E, 0x0094, 0x005E, 0x006C, 0x00C5, 0x00C6, 0x005C, 0x00C6, 0x0093, 0x00CB, 0x006C, 0x005B, 0x005E, 0x00C5, 0x01F6, 0x01F6, 0x005C, 0x008A, 0x0093, 0x00CB, 0x005B, 0x0094, 0x00C5, 0x006C, 0x01F6, 0x008A, 0x0093, 0x00C6, 0x0094, 0x005C, 0x00CB]],
["COM:X8E", "Thin Air", [0x011E, 0x00C6, 0x01F6, 0x005A, 0x0059, 0x0059, 0x005B, 0x00CB, 0x00CC, 0x00CA, 0x005A, 0x0068, 0x01F6, 0x00C5, 0x020C, 0x00C5, 0x00CC, 0x0059, 0x005B, 0x00CA, 0x0068, 0x00C6, 0x00C5, 0x005A, 0x00CB, 0x01F6, 0x020C, 0x00CB, 0x00C6, 0x005B, 0x00CA]],
["COM:X8F", "Like Wind", [0x011E, 0x00CC, 0x01E1, 0x006C, 0x00CA, 0x00B7, 0x01F6, 0x0068, 0x008F, 0x0068, 0x006C, 0x01E1, 0x00B7, 0x00CA, 0x00C5, 0x01F6, 0x00C5, 0x008F, 0x00CB, 0x00CB, 0x01E1, 0x00CC, 0x00CA, 0x006C, 0x01F6, 0x00B7, 0x008F, 0x00C5, 0x00CC, 0x0068, 0x00CB]],
["COM:X90", "Soul Wheel", [0x011E, 0x00C5, 0x005C, 0x005F, 0x00CA, 0x008E, 0x0068, 0x00CB, 0x00BC, 0x01E0, 0x005F, 0x005C, 0x008E, 0x00C5, 0x00CA, 0x0068, 0x01E0, 0x00BC, 0x0093, 0x00CB, 0x005C, 0x00C5, 0x00CA, 0x005F, 0x0068, 0x008E, 0x00BC, 0x00CB, 0x00F7, 0x01E0, 0x00F7]],
["COM:X91", "Molt", [0x011E, 0x00CF, 0x0078, 0x01D2, 0x00C5, 0x01C7, 0x0063, 0x00C5, 0x0063, 0x00AE, 0x0056, 0x01D2, 0x0237, 0x0078, 0x0237, 0x01C7, 0x0205, 0x021B, 0x0205, 0x00AE, 0x00CF, 0x0056, 0x01D2, 0x0237, 0x01C7, 0x00C5, 0x0205, 0x0063, 0x021B, 0x00AE, 0x021B]],
["COM:X92", "Last Words", [0x011E, 0x006C, 0x006F, 0x006C, 0x00CA, 0x00B1, 0x006B, 0x0134, 0x00CB, 0x0133, 0x006C, 0x006F, 0x00B1, 0x00CF, 0x0235, 0x006B, 0x005C, 0x00C5, 0x005C, 0x012E, 0x006F, 0x00CF, 0x00CA, 0x00CA, 0x006B, 0x00B1, 0x00CB, 0x0235, 0x00CF, 0x005C, 0x0133]],
["COM:X93", "Slow Death", [0x011C, 0x00C6, 0x005E, 0x0064, 0x006F, 0x00C5, 0x0078, 0x00C5, 0x008C, 0x005B, 0x0064, 0x005E, 0x006F, 0x00CB, 0x0078, 0x00CB, 0x005B, 0x008C, 0x00A3, 0x00F6, 0x005E, 0x00A3, 0x00CB, 0x0064, 0x006F, 0x0078, 0x00C5, 0x008C, 0x00F6, 0x005B, 0x00F6]],
["COM:X94", "Withering", [0x011C, 0x00AC, 0x009D, 0x00C5, 0x01D2, 0x00AE, 0x00D0, 0x007D, 0x0205, 0x006F, 0x01D2, 0x006F, 0x009D, 0x0210, 0x005B, 0x00D0, 0x007D, 0x00AE, 0x0205, 0x00C5, 0x009D, 0x00AC, 0x0210, 0x01D2, 0x00AE, 0x005B, 0x0205, 0x00D0, 0x00C5, 0x006F, 0x00C6]],
["COM:X95", "Small Fry", [0x011C, 0x01DF, 0x01D4, 0x0064, 0x00C6, 0x00CB, 0x00A3, 0x00A3, 0x021F, 0x0207, 0x020D, 0x00C5, 0x006A, 0x01D4, 0x00C5, 0x0064, 0x0207, 0x00F6, 0x0207, 0x00C6, 0x01DF, 0x021F, 0x00F6, 0x01DF, 0x0064, 0x00F6, 0x01D4, 0x00A3, 0x006A, 0x00C5, 0x00CB]],
["COM:X96", "Counter", [0x011C, 0x00C5, 0x0078, 0x0062, 0x00B1, 0x00CC, 0x006F, 0x00CC, 0x00B2, 0x005C, 0x0062, 0x0078, 0x00B1, 0x00C5, 0x006F, 0x00C6, 0x005C, 0x00B2, 0x00B8, 0x00CB, 0x0078, 0x00B8, 0x00C5, 0x0062, 0x00B1, 0x006F, 0x00B2, 0x00CC, 0x00CB, 0x005C, 0x00E6]],
["COM:X97", "Death Step", [0x011C, 0x00CC, 0x005C, 0x00CA, 0x0059, 0x0090, 0x005A, 0x01D2, 0x00CB, 0x00AE, 0x0059, 0x005C, 0x0090, 0x021E, 0x005A, 0x00CA, 0x022B, 0x022B, 0x00CB, 0x01D2, 0x005C, 0x00CC, 0x021E, 0x0059, 0x00CA, 0x005A, 0x00CB, 0x0090, 0x01D2, 0x022B, 0x00F7]],
["COM:X98", "Move & Die", [0x011C, 0x00F0, 0x007D, 0x005D, 0x00C5, 0x00CB, 0x005A, 0x00CB, 0x0234, 0x01D2, 0x005D, 0x007D, 0x00C5, 0x00CA, 0x005A, 0x00CA, 0x01D2, 0x0234, 0x00CC, 0x00F7, 0x007D, 0x00CC, 0x00CA, 0x005D, 0x00C5, 0x005A, 0x0234, 0x00CB, 0x00F7, 0x01D2, 0x00F7]],
["COM:X99", "Revenge", [0x011C, 0x00C6, 0x0078, 0x0062, 0x00B1, 0x00CB, 0x006F, 0x00CB, 0x00B2, 0x005C, 0x0062, 0x0078, 0x00B1, 0x00CA, 0x006F, 0x00CA, 0x005C, 0x00B2, 0x00C6, 0x0133, 0x00C6, 0x00CA, 0x0078, 0x0062, 0x00CB, 0x006F, 0x00B1, 0x0133, 0x00B2, 0x005C, 0x0133]],
["COM:X9A", "Love Dies", [0x011C, 0x00CF, 0x00CB, 0x0078, 0x006C, 0x00B1, 0x0134, 0x006B, 0x012E, 0x01F1, 0x006C, 0x0078, 0x00B1, 0x00CA, 0x006B, 0x00CA, 0x01F1, 0x00C5, 0x00CF, 0x012E, 0x00CF, 0x00CA, 0x0078, 0x006C, 0x0134, 0x006B, 0x00B1, 0x0133, 0x00CB, 0x01F1, 0x0133]],
["COM:X9B", "Outlaw", [0x011C, 0x00C5, 0x01C7, 0x0066, 0x00B1, 0x00C6, 0x01F1, 0x01F1, 0x0090, 0x0078, 0x006C, 0x0078, 0x01C7, 0x00CB, 0x0066, 0x00CB, 0x00C6, 0x0090, 0x00C5, 0x00CA, 0x00C5, 0x00CB, 0x0078, 0x006C, 0x00C6, 0x0066, 0x00B1, 0x00CA, 0x0090, 0x01F1, 0x00CA]],
["COM:X9C", "Hi Lilies", [0x0007, 0x0093, 0x0078, 0x005B, 0x0056, 0x008C, 0x0064, 0x008C, 0x00A3, 0x0059, 0x005B, 0x0078, 0x0056, 0x008A, 0x0064, 0x008A, 0x0059, 0x00A3, 0x0093, 0x008E, 0x0093, 0x008A, 0x0078, 0x005B, 0x008C, 0x0064, 0x0056, 0x008E, 0x00A3, 0x0059, 0x008E]],
["COM:X9D", "Rapid Hits", [0x0007, 0x008A, 0x0078, 0x0064, 0x00C5, 0x0078, 0x008C, 0x005C, 0x00A3, 0x0059, 0x0064, 0x005A, 0x0078, 0x008A, 0x005C, 0x008C, 0x0059, 0x0207, 0x00A3, 0x00C5, 0x00A3, 0x008A, 0x005A, 0x0064, 0x008C, 0x005C, 0x0207, 0x00C6, 0x0207, 0x0059, 0x00C6]],
["COM:X9E", "Supporter", [0x0007, 0x021E, 0x0056, 0x0059, 0x0093, 0x00C6, 0x005A, 0x00C6, 0x00CB, 0x020C, 0x0059, 0x0056, 0x0093, 0x021E, 0x005A, 0x00C5, 0x005C, 0x008C, 0x020C, 0x005C, 0x021E, 0x00C5, 0x0056, 0x0059, 0x00C6, 0x005A, 0x008C, 0x00CB, 0x020C, 0x005C, 0x00CB]],
["COM:X9F", "Canadines!", [0x0007, 0x0213, 0x01DC, 0x01DD, 0x020E, 0x00C5, 0x006F, 0x00C5, 0x00B8, 0x0070, 0x01DD, 0x01DC, 0x020E, 0x00C6, 0x006F, 0x00C6, 0x0070, 0x00B8, 0x0213, 0x0220, 0x0213, 0x00C6, 0x01DC, 0x01DD, 0x00C5, 0x006F, 0x020E, 0x0220, 0x00B8, 0x0070, 0x0220]],
["COM:XA0", "Aim&Shoot", [0x0007, 0x008F, 0x007D, 0x00C5, 0x005A, 0x00C5, 0x01D2, 0x01D2, 0x008E, 0x01D9, 0x005A, 0x007D, 0x008A, 0x008F, 0x008A, 0x00CB, 0x01D9, 0x008C, 0x00BC, 0x00C6, 0x00BC, 0x00CB, 0x007D, 0x005A, 0x01D2, 0x008A, 0x00C5, 0x00C6, 0x008E, 0x01D9, 0x00C6]],
["COM:XA1", "Two As One", [0x0007, 0x00CB, 0x006F, 0x01DE, 0x020E, 0x00CA, 0x01D9, 0x0220, 0x021A, 0x0070, 0x01DE, 0x006F, 0x020E, 0x00CB, 0x01D9, 0x00CA, 0x0073, 0x021A, 0x00C5, 0x0220, 0x00CB, 0x00CA, 0x006F, 0x01DE, 0x0220, 0x01D9, 0x020E, 0x0140, 0x021A, 0x0070, 0x0140]],
["COM:XA2", "For Break", [0x0007, 0x00CF, 0x005E, 0x006F, 0x0073, 0x00CB, 0x01DC, 0x00CA, 0x0078, 0x00B8, 0x006F, 0x005E, 0x0073, 0x00CF, 0x01DC, 0x00CB, 0x0078, 0x00B1, 0x00B8, 0x00CA, 0x00B8, 0x00CF, 0x005E, 0x006F, 0x00C5, 0x01DC, 0x00B1, 0x00CA, 0x00B1, 0x0078, 0x012A]],
["COM:XA3", "Assistant", [0x0007, 0x00C5, 0x01D1, 0x005A, 0x009A, 0x00CB, 0x005D, 0x00C6, 0x00CA, 0x005E, 0x005A, 0x01D1, 0x009A, 0x00C5, 0x005D, 0x00CB, 0x005E, 0x008D, 0x00CA, 0x00C6, 0x00CA, 0x00C5, 0x01D1, 0x005A, 0x00CB, 0x005D, 0x009A, 0x00C6, 0x008D, 0x005E, 0x00CC]],
["COM:XA4", "Love Power", [0x0007, 0x00C5, 0x01D6, 0x00CB, 0x00F6, 0x007A, 0x00C6, 0x00F6, 0x00CA, 0x01D7, 0x01C8, 0x01D7, 0x00C6, 0x00FB, 0x01D6, 0x00FB, 0x007A, 0x00CA, 0x00C5, 0x013B, 0x00C5, 0x01D7, 0x01D6, 0x01C8, 0x00FB, 0x013B, 0x00CB, 0x00F6, 0x00CA, 0x007A, 0x013B]],
["COM:XA5", "Clipped Wing", [0x0118, 0x008E, 0x0093, 0x007D, 0x00A3, 0x00C5, 0x0059, 0x00C6, 0x008C, 0x005A, 0x007D, 0x0078, 0x0093, 0x008E, 0x0059, 0x00C5, 0x005A, 0x008C, 0x008A, 0x00C6, 0x008A, 0x00C5, 0x0078, 0x007D, 0x00C6, 0x0059, 0x00A3, 0x00CC, 0x008C, 0x005A, 0x00CC]],
["COM:XA6", "Mine Set", [0x0118, 0x00C5, 0x005A, 0x007D, 0x008A, 0x00C6, 0x01D2, 0x00CC, 0x0093, 0x00CB, 0x005A, 0x00B8, 0x00B8, 0x00C5, 0x007D, 0x00C6, 0x01D2, 0x008C, 0x0093, 0x00CC, 0x00C5, 0x00C6, 0x00B8, 0x005A, 0x00CC, 0x007D, 0x008A, 0x0093, 0x008C, 0x01D2, 0x00CB]],
["COM:XA7", "Power Bomb", [0x0118, 0x008C, 0x0061, 0x006C, 0x009A, 0x00CA, 0x0068, 0x00CA, 0x020D, 0x01EB, 0x006C, 0x01EB, 0x009A, 0x008C, 0x006B, 0x00C5, 0x0068, 0x0090, 0x020D, 0x021F, 0x020D, 0x00C5, 0x0061, 0x006C, 0x00CA, 0x006B, 0x0090, 0x021F, 0x0090, 0x01EB, 0x021F]],
["COM:XA8", "Dear Break", [0x0118, 0x00CF, 0x0073, 0x005C, 0x00B2, 0x00C5, 0x0079, 0x00CA, 0x00B8, 0x0078, 0x005C, 0x0078, 0x00B2, 0x00CF, 0x0062, 0x00CB, 0x0079, 0x00B1, 0x00B8, 0x00CA, 0x00B8, 0x00CF, 0x0078, 0x005C, 0x00C5, 0x0062, 0x00B1, 0x00CA, 0x00B1, 0x0079, 0x012A]],
["COM:XA9", "Hungry", [0x0119, 0x00DE, 0x0205, 0x0058, 0x00D8, 0x00C6, 0x0074, 0x00C6, 0x00DB, 0x0064, 0x0058, 0x0064, 0x00D8, 0x00C5, 0x0074, 0x00C5, 0x0078, 0x00DB, 0x00DE, 0x00CB, 0x00DE, 0x00C5, 0x0205, 0x0058, 0x00C6, 0x0074, 0x00D8, 0x00CB, 0x00DB, 0x0078, 0x00CB]],
["COM:XAA", "Gelatin", [0x0119, 0x00DE, 0x009A, 0x006B, 0x008C, 0x00CB, 0x008C, 0x0074, 0x00AC, 0x0058, 0x006B, 0x006C, 0x009A, 0x00D8, 0x00CB, 0x00D8, 0x0058, 0x008C, 0x00DE, 0x00C5, 0x00DE, 0x00D8, 0x006C, 0x006B, 0x00CB, 0x0074, 0x0074, 0x00C5, 0x00AC, 0x0058, 0x00C5]],
["COM:XAB", "Buffet", [0x0119, 0x00C6, 0x006F, 0x0078, 0x00D8, 0x00CA, 0x0068, 0x00CA, 0x00CC, 0x006C, 0x01F6, 0x007D, 0x005C, 0x00C5, 0x005F, 0x00C5, 0x01D2, 0x00DB, 0x00C6, 0x00CB, 0x00C6, 0x00C5, 0x01F1, 0x005A, 0x00CA, 0x0056, 0x00D8, 0x00CB, 0x00DB, 0x01D9, 0x00CB]],
["COM:XAC", "Snacks?", [0x0119, 0x0220, 0x00D8, 0x006F, 0x020E, 0x00CA, 0x020E, 0x01D1, 0x0140, 0x0070, 0x006F, 0x01D9, 0x00D8, 0x0220, 0x01D1, 0x013F, 0x0070, 0x00CB, 0x00CA, 0x0134, 0x00CA, 0x0220, 0x01D9, 0x006F, 0x0140, 0x01D1, 0x013F, 0x0134, 0x00CB, 0x0070, 0x0134]],
["COM:XAD", "Bad Food", [0x0119, 0x00C6, 0x01F1, 0x0078, 0x00B8, 0x00CA, 0x005B, 0x00CA, 0x00CC, 0x01D2, 0x0078, 0x01F1, 0x00B8, 0x00C5, 0x005B, 0x00C5, 0x01D2, 0x00CC, 0x00C6, 0x00CB, 0x00C6, 0x00C5, 0x01F1, 0x0078, 0x00CA, 0x005B, 0x00B8, 0x00CB, 0x00CC, 0x01D2, 0x00CB]],
["COM:XAE", "Leftovers", [0x0119, 0x00C5, 0x005A, 0x01E0, 0x005A, 0x00CB, 0x005C, 0x00CB, 0x00C6, 0x01DD, 0x01E4, 0x01DD, 0x005A, 0x00CA, 0x01E0, 0x00CA, 0x005C, 0x00B1, 0x00C5, 0x0125, 0x00C5, 0x00CA, 0x01DD, 0x01E4, 0x00CB, 0x01E0, 0x00B1, 0x0125, 0x00B1, 0x005C, 0x0125]],
["COM:XAF", "Breakfast", [0x0119, 0x00A1, 0x005C, 0x00C6, 0x00D9, 0x00C5, 0x01DD, 0x00C5, 0x00DE, 0x01E8, 0x0061, 0x01E8, 0x00D9, 0x00CC, 0x01DD, 0x00CC, 0x01DF, 0x00D8, 0x00DE, 0x013D, 0x00C6, 0x00CC, 0x00A1, 0x0061, 0x00C5, 0x005C, 0x00D9, 0x013D, 0x00D8, 0x01DF, 0x013D]],
["COM:XB0", "All Two", [0x0119, 0x00CC, 0x00A1, 0x00CB, 0x0061, 0x00D8, 0x0133, 0x007C, 0x0103, 0x01D4, 0x0056, 0x0075, 0x00D8, 0x00C5, 0x005C, 0x00C5, 0x005D, 0x00DE, 0x00CB, 0x0103, 0x00CC, 0x00C5, 0x0062, 0x0059, 0x0133, 0x01DC, 0x00DE, 0x013C, 0x00CB, 0x01C6, 0x013C]],
["COM:XB1", "1-2 Punch", [0x0119, 0x00C6, 0x005C, 0x01F3, 0x00C1, 0x00CB, 0x01C8, 0x00CA, 0x00C5, 0x01C7, 0x01F3, 0x005C, 0x00C1, 0x00C6, 0x01C8, 0x00CB, 0x01C7, 0x00BA, 0x00C5, 0x00CA, 0x00C6, 0x00CB, 0x005C, 0x01F3, 0x00CA, 0x01C8, 0x00BA, 0x00F7, 0x00BA, 0x01C7, 0x00F7]],
["COM:XB2", "Make Peace", [0x011F, 0x00CC, 0x00C7, 0x0068, 0x00C6, 0x014A, 0x00A3, 0x014A, 0x00C5, 0x021F, 0x0068, 0x021F, 0x00C6, 0x00CB, 0x008C, 0x00CB, 0x00A3, 0x00C5, 0x00CC, 0x00F6, 0x00CC, 0x00CB, 0x00C7, 0x0068, 0x014A, 0x008C, 0x00C6, 0x00F6, 0x00C5, 0x021F, 0x00F6]],
["COM:XB3", "Friendly", [0x011F, 0x00C6, 0x01DF, 0x0064, 0x00B8, 0x00B8, 0x01DC, 0x00C5, 0x0213, 0x006F, 0x005B, 0x006F, 0x00B8, 0x00D0, 0x00C5, 0x00D0, 0x01DC, 0x008C, 0x00C6, 0x00CB, 0x00C6, 0x00D0, 0x01DF, 0x005B, 0x00C5, 0x0064, 0x01DC, 0x00CB, 0x0213, 0x006F, 0x00CB]],
["COM:XB4", "Love&Peace", [0x011F, 0x00C7, 0x01D2, 0x0068, 0x008A, 0x00C6, 0x006B, 0x00C6, 0x00D0, 0x01E4, 0x0068, 0x005E, 0x006C, 0x00C5, 0x006B, 0x00C5, 0x01E4, 0x021F, 0x00D0, 0x00CB, 0x00C7, 0x00C5, 0x01D2, 0x0068, 0x00C6, 0x006B, 0x008A, 0x00CB, 0x021F, 0x01E4, 0x00CB]],
["COM:XB5", "Bloodless", [0x011F, 0x00CC, 0x01D7, 0x0068, 0x0093, 0x00C5, 0x01C7, 0x00C6, 0x008F, 0x01DE, 0x0068, 0x01D7, 0x0093, 0x00CC, 0x01C7, 0x00C5, 0x01DE, 0x0090, 0x021A, 0x00C6, 0x00CC, 0x00C5, 0x01D7, 0x0068, 0x00C6, 0x01C7, 0x0090, 0x021B, 0x008F, 0x01DE, 0x021B]],
["COM:XB6", "Steel Wall", [0x011F, 0x00C5, 0x0209, 0x01D7, 0x00C7, 0x00CB, 0x01D9, 0x00CB, 0x00CC, 0x0060, 0x01C7, 0x0060, 0x00C7, 0x00C6, 0x01D9, 0x021B, 0x005D, 0x021F, 0x00CC, 0x00CA, 0x00C5, 0x00C6, 0x0209, 0x01C7, 0x00CB, 0x01D7, 0x021F, 0x00CA, 0x00CC, 0x005D, 0x00CA]],
]

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

+1
View File
@@ -0,0 +1 @@
../../quests/e901-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e903-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e904-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e905-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e906-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e907-gc3.mnm
+1
View File
@@ -0,0 +1 @@
../../quests/e908-gc3.mnm

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