From 9f2f0ccc14ba7c82d5b5884b14d586667f0910d5 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 9 Nov 2022 22:48:27 -0800 Subject: [PATCH] implement episode 3 battles --- CMakeLists.txt | 11 +- README.md | 7 +- src/CommandFormats.hh | 2679 +++++++------- src/Episode3.cc | 936 ----- src/Episode3.hh | 374 -- src/Episode3/AssistServer.cc | 286 ++ src/Episode3/AssistServer.hh | 60 + src/Episode3/Card.cc | 1235 +++++++ src/Episode3/Card.hh | 131 + src/Episode3/CardSpecial.cc | 4515 +++++++++++++++++++++++ src/Episode3/CardSpecial.hh | 347 ++ src/Episode3/DataIndex.cc | 1403 +++++++ src/Episode3/DataIndex.hh | 815 ++++ src/Episode3/DeckState.cc | 286 ++ src/Episode3/DeckState.hh | 117 + src/Episode3/MapState.cc | 95 + src/Episode3/MapState.hh | 63 + src/Episode3/PlayerState.cc | 1835 +++++++++ src/Episode3/PlayerState.hh | 193 + src/Episode3/PlayerStateSubordinates.cc | 547 +++ src/Episode3/PlayerStateSubordinates.hh | 265 ++ src/Episode3/RulerServer.cc | 2681 ++++++++++++++ src/Episode3/RulerServer.hh | 232 ++ src/Episode3/Server.cc | 2427 ++++++++++++ src/Episode3/Server.hh | 280 ++ src/Lobby.hh | 2 + src/Main.cc | 88 +- src/PSOEncryption.cc | 13 +- src/PSOEncryption.hh | 4 + src/Player.hh | 6 +- src/ProxyCommands.cc | 6 +- src/Quest.cc | 19 +- src/ReceiveCommands.cc | 100 +- src/ReceiveSubcommands.cc | 4 +- src/SendCommands.cc | 30 +- src/SendCommands.hh | 4 - src/Server.cc | 12 +- src/ServerState.cc | 2 + src/ServerState.hh | 4 +- src/Text.hh | 67 +- system/config.example.json | 10 + 41 files changed, 19300 insertions(+), 2891 deletions(-) delete mode 100644 src/Episode3.cc delete mode 100644 src/Episode3.hh create mode 100644 src/Episode3/AssistServer.cc create mode 100644 src/Episode3/AssistServer.hh create mode 100644 src/Episode3/Card.cc create mode 100644 src/Episode3/Card.hh create mode 100644 src/Episode3/CardSpecial.cc create mode 100644 src/Episode3/CardSpecial.hh create mode 100644 src/Episode3/DataIndex.cc create mode 100644 src/Episode3/DataIndex.hh create mode 100644 src/Episode3/DeckState.cc create mode 100644 src/Episode3/DeckState.hh create mode 100644 src/Episode3/MapState.cc create mode 100644 src/Episode3/MapState.hh create mode 100644 src/Episode3/PlayerState.cc create mode 100644 src/Episode3/PlayerState.hh create mode 100644 src/Episode3/PlayerStateSubordinates.cc create mode 100644 src/Episode3/PlayerStateSubordinates.hh create mode 100644 src/Episode3/RulerServer.cc create mode 100644 src/Episode3/RulerServer.hh create mode 100644 src/Episode3/Server.cc create mode 100644 src/Episode3/Server.hh diff --git a/CMakeLists.txt b/CMakeLists.txt index 39ef031e..4f7da08a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,7 +48,16 @@ add_executable(newserv src/Client.cc src/Compression.cc src/DNSServer.cc - src/Episode3.cc + src/Episode3/AssistServer.cc + src/Episode3/Card.cc + src/Episode3/CardSpecial.cc + src/Episode3/DataIndex.cc + src/Episode3/DeckState.cc + src/Episode3/MapState.cc + src/Episode3/PlayerState.cc + src/Episode3/PlayerStateSubordinates.cc + src/Episode3/RulerServer.cc + src/Episode3/Server.cc src/FileContentsCache.cc src/FunctionCompiler.cc src/GSLArchive.cc diff --git a/README.md b/README.md index 653f1c5c..050c09b3 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ With that said, I offer no guarantees on how or when this project will advance. 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 and tournaments aren't implemented. (Some reverse-engineering has already been done here though - see Episode3.hh/cc) - - Card auctions could be supported without too much effort, though; only the EF command needs to be implemented. +- Episode 3 battles are implemented but not well-tested. +- Card auctions could be supported without too much effort, though; only the EF command needs to be implemented. - 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). @@ -42,6 +42,7 @@ Current known issues / missing features / things to do: - 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). ## Compatibility @@ -63,7 +64,7 @@ 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, and trade cards, but CARD battles are not implemented yet. Tournaments are also not supported.* +3. *Episode 3 players can download quests, join lobbies, create and join games, and trade cards. CARD battles are implemented but not well-tested. Tournaments and View Battle are not implemented.* 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.* diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 46bca49b..ea7b9951 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -8,13 +8,12 @@ #include "PSOProtocol.hh" #include "Text.hh" #include "Player.hh" +#include "Episode3/DataIndex.hh" +#include "Episode3/DeckState.hh" +#include "Episode3/MapState.hh" +#include "Episode3/PlayerStateSubordinates.hh" - - -#pragma pack(push) -#pragma pack(1) - - +#define __packed__ __attribute__((packed)) // This file is newserv's canonical reference of the PSO client/server protocol. @@ -76,14 +75,14 @@ struct ClientConfig { uint32_t proxy_destination_address; uint16_t proxy_destination_port; parray unused; -}; +} __packed__; struct ClientConfigBB { ClientConfig cfg; uint8_t bb_game_state; uint8_t bb_player_index; parray unused; -}; +} __packed__; @@ -142,11 +141,11 @@ struct ClientConfigBB { struct S_ServerInit_Patch_02 { ptext copyright; - le_uint32_t server_key; // Key for commands sent by server - le_uint32_t client_key; // Key for commands sent by client + le_uint32_t server_key = 0; // Key for commands sent by server + le_uint32_t client_key = 0; // Key for commands sent by client // The client rejects the command if it's larger than this size, so we can't // add the after_message like we do in the other server init commands. -}; +} __packed__; // 02 (C->S): Encryption started // No arguments @@ -166,7 +165,7 @@ struct C_Login_Patch_04 { ptext username; ptext password; ptext email; -}; +} __packed__; // 05 (S->C): Unknown // This is probably the disconnect command, like on the game server. It seems @@ -176,10 +175,10 @@ struct C_Login_Patch_04 { // 06 (S->C): Open file for writing struct S_OpenFile_Patch_06 { - le_uint32_t unknown; // Seems to always be zero - le_uint32_t size; + le_uint32_t unknown = 0; + le_uint32_t size = 0; ptext filename; -}; +} __packed__; // 07 (S->C): Write file // The client's handler table says this command's maximum size is 0x6010 @@ -190,11 +189,11 @@ struct S_OpenFile_Patch_06 { // appended to the end. struct S_WriteFileHeader_Patch_07 { - le_uint32_t chunk_index; - le_uint32_t chunk_checksum; // CRC32 of the following chunk data - le_uint32_t chunk_size; + le_uint32_t chunk_index = 0; + le_uint32_t chunk_checksum = 0; // CRC32 of the following chunk data + le_uint32_t chunk_size = 0; // The chunk data immediately follows here -}; +} __packed__; // 08 (S->C): Close current file // The unused field is optional. It's not clear whether this field was ever @@ -202,14 +201,14 @@ struct S_WriteFileHeader_Patch_07 { // simply set the maximum size of this command incorrectly. struct S_CloseCurrentFile_Patch_08 { - le_uint32_t unused; -}; + le_uint32_t unused = 0; +} __packed__; // 09 (S->C): Enter directory struct S_EnterDirectory_Patch_09 { ptext name; -}; +} __packed__; // 0A (S->C): Exit directory // No arguments @@ -220,9 +219,9 @@ struct S_EnterDirectory_Patch_09 { // 0C (S->C): File checksum request struct S_FileChecksumRequest_Patch_0C { - le_uint32_t request_id; + le_uint32_t request_id = 0; ptext filename; -}; +} __packed__; // 0D (S->C): End of file checksum requests // No arguments @@ -232,10 +231,10 @@ struct S_FileChecksumRequest_Patch_0C { // 0F (C->S): File information struct C_FileInformation_Patch_0F { - le_uint32_t request_id; // Matches a request ID from an earlier 0C command - le_uint32_t checksum; // CRC32 of the file's data - le_uint32_t size; -}; + le_uint32_t request_id = 0; // Matches request_id from an earlier 0C command + le_uint32_t checksum = 0; // CRC32 of the file's data + le_uint32_t size = 0; +} __packed__; // 10 (C->S): End of file information command list // No arguments @@ -243,9 +242,9 @@ struct C_FileInformation_Patch_0F { // 11 (S->C): Start file downloads struct S_StartFileDownloads_Patch_11 { - le_uint32_t total_bytes; - le_uint32_t num_files; -}; + le_uint32_t total_bytes = 0; + le_uint32_t num_files = 0; +} __packed__; // 12 (S->C): End patch session successfully // No arguments @@ -265,12 +264,12 @@ struct S_StartFileDownloads_Patch_11 { template struct S_Reconnect { - be_uint32_t address; - PortT port; - le_uint16_t unused; -}; + be_uint32_t address = 0; + PortT port = 0; + le_uint16_t unused = 0; +} __packed__; -struct S_Reconnect_Patch_14 : S_Reconnect { }; +struct S_Reconnect_Patch_14 : S_Reconnect { } __packed__; // 15 (S->C): Login failure // No arguments @@ -292,10 +291,10 @@ struct S_Reconnect_Patch_14 : S_Reconnect { }; // the guild_card_number field is unused and should be 0. struct SC_TextHeader_01_06_11_B0_EE { - le_uint32_t unused; - le_uint32_t guild_card_number; + le_uint32_t unused = 0; + le_uint32_t guild_card_number = 0; // Text immediately follows here (char[] on DC/V3, char16_t[] on PC/BB) -}; +} __packed__; // 02 (S->C): Start encryption (except on BB) // This command should be used for non-initial sessions (after the client has @@ -315,9 +314,9 @@ struct SC_TextHeader_01_06_11_B0_EE { struct S_ServerInitDefault_DC_PC_V3_02_17_91_9B { ptext copyright; - le_uint32_t server_key; // Key for data sent by server - le_uint32_t client_key; // Key for data sent by client -}; + le_uint32_t server_key = 0; // Key for data sent by server + le_uint32_t client_key = 0; // Key for data sent by client +} __packed__; template struct S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B { @@ -325,24 +324,24 @@ struct S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B { // This field is not part of SEGA's implementation; the client ignores it. // newserv sends a message here disavowing the preceding copyright notice. ptext after_message; -}; +} __packed__; // 03 (C->S): Legacy login (non-BB) // TODO: Check if this command exists on DC v1/v2. struct C_LegacyLogin_PC_V3_03 { - le_uint64_t unused; // Same as unused field in 9D/9E - le_uint32_t sub_version; - uint8_t unknown_a1; - uint8_t language; // Same as 9D/9E - le_uint16_t unknown_a2; + le_uint64_t unused = 0; // Same as unused field in 9D/9E + le_uint32_t sub_version = 0; + uint8_t unknown_a1 = 0; + uint8_t language = 0; // Same as 9D/9E + le_uint16_t unknown_a2 = 0; // Note: These are suffixed with 2 since they come from the same source data // as the corresponding fields in 9D/9E. (Even though serial_number and // serial_number2 have the same contents in 9E, they do not come from the same // field on the client's connection context object.) ptext serial_number2; ptext access_key2; -}; +} __packed__; // 03 (S->C): Legacy password check result (non-BB) // header.flag specifies if the password was correct. If header.flag is 0, the @@ -362,14 +361,14 @@ struct S_ServerInitDefault_BB_03_9B { ptext copyright; parray server_key; parray client_key; -}; +} __packed__; template struct S_ServerInitWithAfterMessage_BB_03_9B { S_ServerInitDefault_BB_03_9B basic_cmd; // As in 02, this field is not part of SEGA's implementation. ptext after_message; -}; +} __packed__; // 04 (C->S): Legacy login // See comments on non-BB 03 (S->C). This is likely a relic of an older, @@ -378,20 +377,20 @@ struct S_ServerInitWithAfterMessage_BB_03_9B { // header.flag is nonzero, but it's not clear what it's used for. struct C_LegacyLogin_PC_V3_04 { - le_uint64_t unused1; // Same as unused field in 9D/9E - le_uint32_t sub_version; - uint8_t unknown_a1; - uint8_t language; // Same as 9D/9E - le_uint16_t unknown_a2; + le_uint64_t unused1 = 0; // Same as unused field in 9D/9E + le_uint32_t sub_version = 0; + uint8_t unknown_a1 = 0; + uint8_t language = 0; // Same as 9D/9E + le_uint16_t unknown_a2 = 0; ptext serial_number; ptext access_key; -}; +} __packed__; struct C_LegacyLogin_BB_04 { parray unknown_a1; ptext username; ptext password; -}; +} __packed__; // 04 (S->C): Set guild card number and update client config ("security data") // header.flag specifies an error code; the format described below is only used @@ -429,17 +428,17 @@ struct S_UpdateClientConfig { // data structures). For historical and simplicity reasons, newserv combines // these three fields into one, which takes on the value 0x00010000 when a // player is present and zero when none is present. - le_uint32_t player_tag; - le_uint32_t guild_card_number; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; // The ClientConfig structure describes how newserv uses this command; other // servers do not use the same format for the following 0x20 or 0x28 bytes (or // may not use it at all). The cfg field is opaque to the client; it will send // back the contents verbatim in its next 9E command (or on request via 9F). ClientConfigT cfg; -}; +} __packed__; -struct S_UpdateClientConfig_DC_PC_V3_04 : S_UpdateClientConfig { }; -struct S_UpdateClientConfig_BB_04 : S_UpdateClientConfig { }; +struct S_UpdateClientConfig_DC_PC_V3_04 : S_UpdateClientConfig { } __packed__; +struct S_UpdateClientConfig_BB_04 : S_UpdateClientConfig { } __packed__; // 05: Disconnect // No arguments @@ -468,8 +467,8 @@ struct C_Chat_06 { union { char dcv3[0]; char16_t pcbb[0]; - } text; -}; + } __packed__ text; +} __packed__; // 07 (S->C): Ship select menu @@ -478,13 +477,13 @@ struct C_Chat_06 { // the first entry becomes the ship name when the client joins a lobby. template struct S_MenuEntry { - le_uint32_t menu_id; - le_uint32_t item_id; - le_uint16_t flags; // should be 0x0F04 + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + le_uint16_t flags = 0x0F04; // Should be this value, apparently ptext text; -}; -struct S_MenuEntry_PC_BB_07_1F : S_MenuEntry { }; -struct S_MenuEntry_DC_V3_07_1F : S_MenuEntry { }; +} __packed__; +struct S_MenuEntry_PC_BB_07_1F : S_MenuEntry { } __packed__; +struct S_MenuEntry_DC_V3_07_1F : S_MenuEntry { } __packed__; // 08 (C->S): Request game list // No arguments @@ -496,30 +495,30 @@ struct S_MenuEntry_DC_V3_07_1F : S_MenuEntry { }; // is not included in the count and does not appear on the client. template struct S_GameMenuEntry { - le_uint32_t menu_id; - le_uint32_t game_id; - uint8_t difficulty_tag; // 0x0A = Ep3; else difficulty + 0x22 (so 0x25 = Ult) - uint8_t num_players; + le_uint32_t menu_id = 0; + le_uint32_t game_id = 0; + uint8_t difficulty_tag = 0; // 0x0A = Ep3; else difficulty + 0x22 (so 0x25 = Ult) + uint8_t num_players = 0; ptext name; // The episode field is used differently by different versions: // - On DCv1, PC, and GC Episode 3, the value is ignored. // - On DCv2, 1 means v1 players can't join the game, and 0 means they can. // - On GC Ep1&2, 0x40 means Episode 1, and 0x41 means Episode 2. // - On BB, 0x40/0x41 mean Episodes 1/2 as on GC, and 0x43 means Episode 4. - uint8_t episode; - uint8_t flags; // 02 = locked, 04 = disabled (BB), 10 = battle, 20 = challenge -}; -struct S_GameMenuEntry_PC_BB_08 : S_GameMenuEntry { }; -struct S_GameMenuEntry_DC_V3_08_Ep3_E6 : S_GameMenuEntry { }; + uint8_t episode = 0; + uint8_t flags = 0; // 02 = locked, 04 = disabled (BB), 10 = battle, 20 = challenge +} __packed__; +struct S_GameMenuEntry_PC_BB_08 : S_GameMenuEntry { } __packed__; +struct S_GameMenuEntry_DC_V3_08_Ep3_E6 : S_GameMenuEntry { } __packed__; // 09 (C->S): Menu item info request // Server will respond with an 11 command, or an A3 or A5 if the specified menu // is the quest menu. struct C_MenuItemInfoRequest_09 { - le_uint32_t menu_id; - le_uint32_t item_id; -}; + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; +} __packed__; // 0B: Invalid command @@ -539,24 +538,24 @@ struct S_Unknown_PC_0E { parray unknown_a1; parray unknown_a2[4]; parray unknown_a3; -}; +} __packed__; struct S_Unknown_GC_0E { PlayerLobbyDataDCGC lobby_data[4]; // This type is a guess struct UnknownA0 { - uint8_t unknown_a1[2]; - le_uint16_t unknown_a2; - le_uint32_t unknown_a3; - }; + parray unknown_a1; + le_uint16_t unknown_a2 = 0; + le_uint32_t unknown_a3 = 0; + } __packed__; UnknownA0 unknown_a0[8]; - le_uint32_t unknown_a1; + le_uint32_t unknown_a1 = 0; parray unknown_a2; - uint8_t unknown_a3[4]; -}; + parray unknown_a3; +} __packed__; struct S_Unknown_XB_0E { parray unknown_a1; -}; +} __packed__; // 0F: Invalid command @@ -570,34 +569,34 @@ struct S_Unknown_XB_0E { // used! struct C_MenuSelection_10_Flag00 { - le_uint32_t menu_id; - le_uint32_t item_id; -}; + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; +} __packed__; template struct C_MenuSelection_10_Flag01 { C_MenuSelection_10_Flag00 basic_cmd; ptext unknown_a1; -}; -struct C_MenuSelection_DC_V3_10_Flag01 : C_MenuSelection_10_Flag01 { }; -struct C_MenuSelection_PC_BB_10_Flag01 : C_MenuSelection_10_Flag01 { }; +} __packed__; +struct C_MenuSelection_DC_V3_10_Flag01 : C_MenuSelection_10_Flag01 { } __packed__; +struct C_MenuSelection_PC_BB_10_Flag01 : C_MenuSelection_10_Flag01 { } __packed__; template struct C_MenuSelection_10_Flag02 { C_MenuSelection_10_Flag00 basic_cmd; ptext password; -}; -struct C_MenuSelection_DC_V3_10_Flag02 : C_MenuSelection_10_Flag02 { }; -struct C_MenuSelection_PC_BB_10_Flag02 : C_MenuSelection_10_Flag02 { }; +} __packed__; +struct C_MenuSelection_DC_V3_10_Flag02 : C_MenuSelection_10_Flag02 { } __packed__; +struct C_MenuSelection_PC_BB_10_Flag02 : C_MenuSelection_10_Flag02 { } __packed__; template struct C_MenuSelection_10_Flag03 { C_MenuSelection_10_Flag00 basic_cmd; ptext unknown_a1; ptext password; -}; -struct C_MenuSelection_DC_V3_10_Flag03 : C_MenuSelection_10_Flag03 { }; -struct C_MenuSelection_PC_BB_10_Flag03 : C_MenuSelection_10_Flag03 { }; +} __packed__; +struct C_MenuSelection_DC_V3_10_Flag03 : C_MenuSelection_10_Flag03 { } __packed__; +struct C_MenuSelection_PC_BB_10_Flag03 : C_MenuSelection_10_Flag03 { } __packed__; // 11 (S->C): Ship info // Same format as 01 command. @@ -615,9 +614,9 @@ struct C_MenuSelection_PC_BB_10_Flag03 : C_MenuSelection_10_Flag03 { } // header.flag = file chunk index (start offset / 0x400) struct S_WriteFile_13_A7 { ptext filename; - uint8_t data[0x400]; - le_uint32_t data_size; -}; + parray data; + le_uint32_t data_size = 0; +} __packed__; // 13 (C->S): Confirm file write (V3/BB) // Client sends this in response to each 13 sent by the server. It appears these @@ -627,7 +626,7 @@ struct S_WriteFile_13_A7 { // header.flag = file chunk index (same as in the 13/A7 sent by the server) struct C_WriteFileConfirmation_V3_BB_13_A7 { ptext filename; -}; +} __packed__; // 14 (S->C): Valid but ignored (PC/V3/BB) // TODO: Check if this command exists on DC v1/v2. @@ -656,7 +655,7 @@ struct C_WriteFileConfirmation_V3_BB_13_A7 { // Note: PSO XB seems to ignore the address field, which makes sense given its // networking architecture. -struct S_Reconnect_19 : S_Reconnect { }; +struct S_Reconnect_19 : S_Reconnect { } __packed__; // Because PSO PC and some versions of PSO DC/GC use the same port but different // protocols, we use a specially-crafted 19 command to send them to two @@ -664,16 +663,16 @@ struct S_Reconnect_19 : S_Reconnect { }; // used by Schthack; I don't know if it was his original creation. struct S_ReconnectSplit_19 { - be_uint32_t pc_address; - le_uint16_t pc_port; + be_uint32_t pc_address = 0; + le_uint16_t pc_port = 0; parray unused1; - uint8_t gc_command; - uint8_t gc_flag; - le_uint16_t gc_size; - be_uint32_t gc_address; - le_uint16_t gc_port; + uint8_t gc_command = 0x19; + uint8_t gc_flag = 0; + le_uint16_t gc_size = 0x97; + be_uint32_t gc_address = 0; + le_uint16_t gc_port = 0; parray unused2; -}; +} __packed__; // 1A (S->C): Large message box // On V3, client will sometimes respond with a D6 command (see D6 for more @@ -725,7 +724,7 @@ struct S_ReconnectSplit_19 { struct SC_GameCardCheck_BB_0022 { parray data; -}; +} __packed__; // Command 0122 uses a 4-byte challenge sent in the header.flag field instead. // This version of the command has no other arguments. @@ -736,21 +735,21 @@ struct SC_GameCardCheck_BB_0022 { // 24 (S->C): Unknown (BB) struct S_Unknown_BB_24 { - le_uint16_t unknown_a1; - le_uint16_t unknown_a2; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; parray values; -}; +} __packed__; // 25 (S->C): Unknown (BB) struct S_Unknown_BB_25 { - le_uint16_t unknown_a1; - uint8_t offset1; - uint8_t value1; - uint8_t offset2; - uint8_t value2; - le_uint16_t unused; -}; + le_uint16_t unknown_a1 = 0; + uint8_t offset1 = 0; + uint8_t value1 = 0; + uint8_t offset2 = 0; + uint8_t value2 = 0; + le_uint16_t unused = 0; +} __packed__; // 26: Invalid command // 27: Invalid command @@ -784,26 +783,26 @@ struct S_Unknown_BB_25 { // target is not online, the server doesn't respond at all. struct C_GuildCardSearch_40 { - le_uint32_t player_tag; - le_uint32_t searcher_guild_card_number; - le_uint32_t target_guild_card_number; -}; + le_uint32_t player_tag = 0x00010000; + le_uint32_t searcher_guild_card_number = 0; + le_uint32_t target_guild_card_number = 0; +} __packed__; // 41 (S->C): Guild card search result template struct SC_MeetUserExtension { - le_uint32_t menu_id; - le_uint32_t lobby_id; + le_uint32_t menu_id = 0; + le_uint32_t lobby_id = 0; parray unknown_a1; ptext player_name; -}; +} __packed__; template struct S_GuildCardSearchResult { - le_uint32_t player_tag; - le_uint32_t searcher_guild_card_number; - le_uint32_t result_guild_card_number; + le_uint32_t player_tag = 0x00010000; + le_uint32_t searcher_guild_card_number = 0; + le_uint32_t result_guild_card_number = 0; HeaderT reconnect_command_header; // Ignored by the client S_Reconnect_19 reconnect_command; // The format of this string is "GAME-NAME,BLOCK##,SERVER-NAME". If the result @@ -816,13 +815,13 @@ struct S_GuildCardSearchResult { // reconnect_command. When processing the 9D/9E, newserv uses only the // lobby_id field within, but it fills in all fields when sengind a 41. SC_MeetUserExtension extension; -}; +} __packed__; struct S_GuildCardSearchResult_PC_41 - : S_GuildCardSearchResult { }; + : S_GuildCardSearchResult { } __packed__; struct S_GuildCardSearchResult_DC_V3_41 - : S_GuildCardSearchResult { }; + : S_GuildCardSearchResult { } __packed__; struct S_GuildCardSearchResult_BB_41 - : S_GuildCardSearchResult { }; + : S_GuildCardSearchResult { } __packed__; // 42: Invalid command // 43: Invalid command @@ -836,33 +835,33 @@ struct S_GuildCardSearchResult_BB_41 struct S_OpenFile_DC_44_A6 { ptext name; // Should begin with "PSO/" parray unused; - uint8_t flags; + uint8_t flags = 0; ptext filename; - le_uint32_t file_size; -}; + le_uint32_t file_size = 0; +} __packed__; struct S_OpenFile_PC_V3_44_A6 { ptext name; // Should begin with "PSO/" parray unused; - le_uint16_t flags; // 0 = download quest, 2 = online quest, 3 = Episode 3 + le_uint16_t flags = 0; // 0 = download quest, 2 = online quest, 3 = Episode 3 ptext filename; - le_uint32_t file_size; -}; + le_uint32_t file_size = 0; +} __packed__; // Curiously, PSO XB expects an extra 0x18 bytes at the end of this command, but // those extra bytes are unused, and the client does not fail if they're // omitted. struct S_OpenFile_XB_44_A6 : S_OpenFile_PC_V3_44_A6 { parray unused2; -}; +} __packed__; struct S_OpenFile_BB_44_A6 { parray unused; - le_uint16_t flags; + le_uint16_t flags = 0; ptext filename; - le_uint32_t file_size; + le_uint32_t file_size = 0; ptext name; -}; +} __packed__; // 44 (C->S): Confirm open file // Client sends this in response to each 44 sent by the server. @@ -874,7 +873,7 @@ struct S_OpenFile_BB_44_A6 { // > 0xFF so the flag is essentially meaningless) struct C_OpenFileConfirmation_44_A6 { ptext filename; -}; +} __packed__; // 45: Invalid command // 46: Invalid command @@ -952,27 +951,27 @@ struct S_JoinGame { // Unlike lobby join commands, these are filled in in their slot positions. // That is, if there's only one player in a game with ID 2, then the first two // of these are blank and the player's data is in the third entry here. - LobbyDataT lobby_data[4]; - uint8_t client_id; - uint8_t leader_id; - uint8_t disable_udp; - uint8_t difficulty; - uint8_t battle_mode; - uint8_t event; - uint8_t section_id; - uint8_t challenge_mode; - le_uint32_t rare_seed; + parray lobby_data; + uint8_t client_id = 0; + uint8_t leader_id = 0; + uint8_t disable_udp = 1; + uint8_t difficulty = 0; + uint8_t battle_mode = 0; + uint8_t event = 0; + uint8_t section_id = 0; + uint8_t challenge_mode = 0; + le_uint32_t rare_seed = 0; // Note: The 64 command for PSO DC ends here (the next 4 fields are ignored). // newserv sends them anyway for code simplicity reasons. - uint8_t episode; + uint8_t episode = 0; // Similarly, PSO GC ignores the values in the following fields. - uint8_t unused2; // Should be 1 for PSO PC? - uint8_t solo_mode; - uint8_t unused3; -}; + uint8_t unused2 = 1; // Should be 1 for PSO PC? + uint8_t solo_mode = 0; + uint8_t unused3 = 0; +} __packed__; -struct S_JoinGame_PC_64 : S_JoinGame { }; -struct S_JoinGame_DC_GC_64 : S_JoinGame { }; +struct S_JoinGame_PC_64 : S_JoinGame { } __packed__; +struct S_JoinGame_DC_GC_64 : S_JoinGame { } __packed__; struct S_JoinGame_GC_Ep3_64 : S_JoinGame_DC_GC_64 { // This field is only present if the game (and client) is Episode 3. Similarly @@ -981,14 +980,14 @@ struct S_JoinGame_GC_Ep3_64 : S_JoinGame_DC_GC_64 { struct { PlayerInventory inventory; PlayerDispDataDCPCV3 disp; - } players_ep3[4]; -}; + } __packed__ players_ep3[4]; +} __packed__; struct S_JoinGame_XB_64 : S_JoinGame { parray unknown_a1; -}; +} __packed__; -struct S_JoinGame_BB_64 : S_JoinGame { }; +struct S_JoinGame_BB_64 : S_JoinGame { } __packed__; // 65 (S->C): Add player to game // When a player joins an existing game, the joining player receives a 64 @@ -998,73 +997,73 @@ struct S_JoinGame_BB_64 : S_JoinGame { }; // Header flag = entry count (always 1 for 65 and 68; up to 0x0C for 67) template struct S_JoinLobby { - uint8_t client_id; - uint8_t leader_id; - uint8_t disable_udp; - uint8_t lobby_number; - uint8_t block_number; - uint8_t unknown_a1; - uint8_t event; - uint8_t unknown_a2; - le_uint32_t unused; + uint8_t client_id = 0; + uint8_t leader_id = 0; + uint8_t disable_udp = 1; + uint8_t lobby_number = 0; + uint8_t block_number = 0; + uint8_t unknown_a1 = 0; + uint8_t event = 0; + uint8_t unknown_a2 = 0; + le_uint32_t unused = 0; struct Entry { LobbyDataT lobby_data; PlayerInventory inventory; DispDataT disp; - }; + } __packed__; // Note: not all of these will be filled in and sent if the lobby isn't full // (the command size will be shorter than this struct's size) - Entry entries[0x0C]; + parray entries; static inline size_t size(size_t used_entries) { return offsetof(S_JoinLobby, entries) + used_entries * sizeof(Entry); } -}; +} __packed__; struct S_JoinLobby_PC_65_67_68 - : S_JoinLobby { }; + : S_JoinLobby { } __packed__; struct S_JoinLobby_DC_GC_65_67_68_Ep3_EB - : S_JoinLobby { }; + : S_JoinLobby { } __packed__; struct S_JoinLobby_BB_65_67_68 - : S_JoinLobby { }; + : S_JoinLobby { } __packed__; struct S_JoinLobby_XB_65_67_68 { - uint8_t client_id; - uint8_t leader_id; - uint8_t disable_udp; - uint8_t lobby_number; - uint8_t block_number; - uint8_t unknown_a1; - uint8_t event; - uint8_t unknown_a2; + uint8_t client_id = 0; + uint8_t leader_id = 0; + uint8_t disable_udp = 1; + uint8_t lobby_number = 0; + uint8_t block_number = 0; + uint8_t unknown_a1 = 0; + uint8_t event = 0; + uint8_t unknown_a2 = 0; parray unknown_a3; parray unknown_a4; struct Entry { PlayerLobbyDataXB lobby_data; PlayerInventory inventory; PlayerDispDataDCPCV3 disp; - }; + } __packed__; // Note: not all of these will be filled in and sent if the lobby isn't full // (the command size will be shorter than this struct's size) - Entry entries[0x0C]; + parray entries; static inline size_t size(size_t used_entries) { return offsetof(S_JoinLobby_XB_65_67_68, entries) + used_entries * sizeof(Entry); } -}; +} __packed__; // 66 (S->C): Remove player from game // This is sent to all players in a game except the leaving player. // Header flag = leaving player ID (same as client_id); struct S_LeaveLobby_66_69_Ep3_E9 { - uint8_t client_id; - uint8_t leader_id; + uint8_t client_id = 0; + uint8_t leader_id = 0; // Note: disable_udp only has an effect for games; it is unused for lobbies // and spectator teams. - uint8_t disable_udp; - uint8_t unused; -}; + uint8_t disable_udp = 1; + uint8_t unused = 0; +} __packed__; // 67 (S->C): Join lobby // This is sent to the joining player; the other players receive a 68 instead. @@ -1114,10 +1113,10 @@ struct S_LeaveLobby_66_69_Ep3_E9 { // TODO: Check if this command exists on DC v1/v2. struct S_Unknown_PC_V3_80 { - le_uint32_t which; // Expected to be in the range 00-0B... maybe client ID? - le_uint32_t unknown_a1; // Could be player_tag - le_uint32_t unknown_a2; // Could be guild_card_number -}; + le_uint32_t which = 0; // Expected to be in the range 00-0B... maybe client ID? + le_uint32_t unknown_a1 = 0; // Could be player_tag + le_uint32_t unknown_a2 = 0; // Could be guild_card_number +} __packed__; // 81: Simple mail // Format is the same in both directions. The server should forward the command @@ -1130,24 +1129,24 @@ struct S_Unknown_PC_V3_80 { template struct SC_SimpleMail_81 { - le_uint32_t player_tag; - le_uint32_t from_guild_card_number; + le_uint32_t player_tag = 0x00010000; + le_uint32_t from_guild_card_number = 0; ptext from_name; - le_uint32_t to_guild_card_number; + le_uint32_t to_guild_card_number = 0; ptext text; -}; +} __packed__; -struct SC_SimpleMail_PC_81 : SC_SimpleMail_81 { }; -struct SC_SimpleMail_DC_V3_81 : SC_SimpleMail_81 { }; +struct SC_SimpleMail_PC_81 : SC_SimpleMail_81 { } __packed__; +struct SC_SimpleMail_DC_V3_81 : SC_SimpleMail_81 { } __packed__; struct SC_SimpleMail_BB_81 { - le_uint32_t player_tag; - le_uint32_t from_guild_card_number; + le_uint32_t player_tag = 0x00010000; + le_uint32_t from_guild_card_number = 0; ptext from_name; - le_uint32_t to_guild_card_number; + le_uint32_t to_guild_card_number = 0; ptext received_date; ptext text; -}; +} __packed__; // 82: Invalid command @@ -1163,17 +1162,17 @@ struct SC_SimpleMail_BB_81 { // Command is a list of these; header.flag is the entry count (15 or 20) struct S_LobbyListEntry_83 { - le_uint32_t menu_id; - le_uint32_t item_id; - le_uint32_t unused; -}; + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; + le_uint32_t unused = 0; +} __packed__; // 84 (C->S): Choose lobby struct C_LobbySelection_84 { - le_uint32_t menu_id; - le_uint32_t item_id; -}; + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; +} __packed__; // 85: Invalid command // 86: Invalid command @@ -1185,7 +1184,7 @@ struct C_LobbySelection_84 { struct C_Login_DCNTE_88 { ptext serial_number; ptext access_key; -}; +} __packed__; // 88 (S->C): License check result (DC NTE only) // No arguemnts except header.flag. @@ -1202,10 +1201,10 @@ struct C_Login_DCNTE_88 { // be an update for every player in the lobby in this command, even if their // arrow color isn't being changed. struct S_ArrowUpdateEntry_88 { - le_uint32_t player_tag; - le_uint32_t guild_card_number; - le_uint32_t arrow_color; -}; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + le_uint32_t arrow_color = 0; +} __packed__; // The arrow color values are: // any number not specified below (including 00) - none @@ -1231,12 +1230,12 @@ struct S_ArrowUpdateEntry_88 { struct C_ConnectionInfo_DCNTE_8A { ptext hardware_id; - le_uint32_t sub_version; // 0x20 - le_uint32_t unknown_a1; + le_uint32_t sub_version = 0x20; // 0x20 + le_uint32_t unknown_a1 = 0; ptext username; ptext password; ptext email_address; // From Sylverant documentation -}; +} __packed__; // 8A (S->C): Connection information result (DC NTE only) // header.flag is a success flag. If 0 is sent, the client shows an error @@ -1255,12 +1254,12 @@ struct C_ConnectionInfo_DCNTE_8A { // 8B: Log in (DC NTE only) struct C_Login_DCNTE_8B { - le_uint32_t player_tag; - le_uint32_t guild_card_number; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; parray hardware_id; - le_uint32_t sub_version; - uint8_t is_extended; - uint8_t language; + le_uint32_t sub_version = 0x20; + uint8_t is_extended = 0; + uint8_t language = 0; parray unused1; ptext serial_number; ptext access_key; @@ -1268,11 +1267,11 @@ struct C_Login_DCNTE_8B { ptext password; ptext name; parray unused; -}; +} __packed__; struct C_LoginExtended_DCNTE_8B : C_Login_DCNTE_8B { SC_MeetUserExtension extension; -}; +} __packed__; // 8C: Invalid command @@ -1293,7 +1292,7 @@ struct C_LoginV1_DC_PC_V3_90 { ptext serial_number; ptext access_key; parray unused; -}; +} __packed__; // 90 (S->C): License verification result (V3) // Behaves exactly the same as 9A (S->C). No arguments except header.flag. @@ -1307,14 +1306,14 @@ struct C_LoginV1_DC_PC_V3_90 { struct C_RegisterV1_DC_92 { parray unknown_a1; - uint8_t unknown_a2; - uint8_t language; // TODO: This is a guess; verify it - uint8_t unknown_a3[2]; + uint8_t unknown_a2 = 0; + uint8_t language = 0; // TODO: This is a guess; verify it + parray unknown_a3; ptext hardware_id; parray unused1; ptext email; // According to Sylverant documentation parray unused2; -}; +} __packed__; // 92 (S->C): Register result (non-BB) // Same format and usage as 9C (S->C) command. @@ -1322,13 +1321,13 @@ struct C_RegisterV1_DC_92 { // 93 (C->S): Log in (DCv1) struct C_LoginV1_DC_93 { - le_uint32_t player_tag; - le_uint32_t guild_card_number; - le_uint32_t unknown_a1; - le_uint32_t unknown_a2; - le_uint32_t sub_version; - uint8_t is_extended; - uint8_t language; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + le_uint32_t unknown_a1 = 0; + le_uint32_t unknown_a2 = 0; + le_uint32_t sub_version = 0; + uint8_t is_extended = 0; + uint8_t language = 0; parray unused1; ptext serial_number; ptext access_key; @@ -1337,27 +1336,27 @@ struct C_LoginV1_DC_93 { ptext hardware_id; ptext name; parray unused2; -}; +} __packed__; struct C_LoginExtendedV1_DC_93 : C_LoginV1_DC_93 { SC_MeetUserExtension extension; -}; +} __packed__; // 93 (C->S): Log in (BB) struct C_Login_BB_93 { - le_uint32_t player_tag; - le_uint32_t guild_card_number; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; ptext unused; - le_uint32_t team_id; + le_uint32_t team_id = 0; ptext username; ptext password; // These fields map to the same fields in SC_MeetUserExtension. There is no // equivalent of the name field from that structure on BB (though newserv // doesn't use it anyway). - le_uint32_t menu_id; - le_uint32_t preferred_lobby_id; + le_uint32_t menu_id = 0; + le_uint32_t preferred_lobby_id = 0; // Note: Unlike other versions, BB puts the version string in the client // config at connect time. So the first time the server gets this command, it @@ -1369,16 +1368,16 @@ struct C_Login_BB_93 { union ClientConfigFields { ClientConfigBB cfg; ptext version_string; - le_uint32_t as_u32[10]; - }; + parray as_u32; + } __packed__; ClientConfigFields old_clients_cfg; struct NewFormat { - le_uint32_t hardware_info[2]; + parray hardware_info; ClientConfigFields cfg; - } new_clients; - } var; -}; + } __packed__ new_clients; + } __packed__ var; +} __packed__; // 94: Invalid command @@ -1397,15 +1396,15 @@ struct C_Login_BB_93 { struct C_CharSaveInfo_V3_BB_96 { // This field appears to be a checksum or random stamp of some sort; it seems // to be unique and constant per character. - le_uint32_t unknown_a1; + le_uint32_t unknown_a1 = 0; // This field counts certain events on a per-character basis. One of the // relevant events is the act of sending a 96 command; another is the act of // receiving a 97 command (to which the client responds with a B1 command). // Presumably Sega's original implementation could keep track of this value // for each character and could therefore tell if a character had connected to // an unofficial server between connections to Sega's servers. - le_uint32_t event_counter; -}; + le_uint32_t event_counter = 0; +} __packed__; // 97 (S->C): Save to memory card // No arguments @@ -1430,13 +1429,13 @@ struct C_Login_DC_PC_V3_9A { ptext v1_access_key; ptext serial_number; ptext access_key; - le_uint32_t player_tag; - le_uint32_t guild_card_number; - le_uint32_t sub_version; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + le_uint32_t sub_version = 0; ptext serial_number2; // On DCv2, this is the hardware ID ptext access_key2; ptext email_address; -}; +} __packed__; // 9A (S->C): License verification result // The result code is sent in the header.flag field. Result codes: @@ -1478,25 +1477,25 @@ struct C_Login_DC_PC_V3_9A { // It appears PSO GC sends uninitialized data in the header.flag field here. struct C_Register_DC_PC_V3_9C { - le_uint64_t unused; - le_uint32_t sub_version; - uint8_t unused1; - uint8_t language; - uint8_t unused2[2]; + le_uint64_t unused = 0; + le_uint32_t sub_version = 0; + uint8_t unused1 = 0; + uint8_t language = 0; + parray unused2; ptext serial_number; // On XB, this is the XBL gamertag ptext access_key; // On XB, this is the XBL user ID ptext password; // On XB, this contains "xbox-pso" -}; +} __packed__; struct C_Register_BB_9C { - le_uint32_t sub_version; - uint8_t unused1; - uint8_t language; - uint8_t unused2[2]; + le_uint32_t sub_version = 0; + uint8_t unused1 = 0; + uint8_t language = 0; + parray unused2; ptext username; ptext password; ptext game_tag; // "psopc2" on BB -}; +} __packed__; // 9C (S->C): Register result // On GC, the only possible error here seems to be wrong password (127) which is @@ -1514,14 +1513,14 @@ struct C_Register_BB_9C { // by its menu ID and item ID. struct C_Login_DC_PC_GC_9D { - le_uint32_t player_tag; // 0x00010000 if guild card is set (via 04) - le_uint32_t guild_card_number; // 0xFFFFFFFF if not set - le_uint32_t unused1; - le_uint32_t unused2; - le_uint32_t sub_version; - uint8_t is_extended; // If 1, structure has extended format - uint8_t language; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES - parray unused3; // Always zeroes? + le_uint32_t player_tag = 0x00010000; // 0x00010000 if guild card is set (via 04) + le_uint32_t guild_card_number = 0; // 0xFFFFFFFF if not set + le_uint32_t unused1 = 0; + le_uint32_t unused2 = 0; + le_uint32_t sub_version = 0; + uint8_t is_extended = 0; // If 1, structure has extended format + uint8_t language = 0; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES + parray unused3; // Always zeroes ptext v1_serial_number; ptext v1_access_key; ptext serial_number; // On XB, this is the XBL gamertag @@ -1529,13 +1528,13 @@ struct C_Login_DC_PC_GC_9D { ptext serial_number2; // On XB, this is the XBL gamertag ptext access_key2; // On XB, this is the XBL user ID ptext name; -}; +} __packed__; struct C_LoginExtended_DC_GC_9D : C_Login_DC_PC_GC_9D { SC_MeetUserExtension extension; -}; +} __packed__; struct C_LoginExtended_PC_9D : C_Login_DC_PC_GC_9D { SC_MeetUserExtension extension; -}; +} __packed__; // 9E (C->S): Log in with client config (V3/BB) // Not used on GC Episodes 1&2 Trial Edition. @@ -1548,26 +1547,26 @@ struct C_Login_GC_9E : C_Login_DC_PC_GC_9D { ClientConfig cfg; parray data; ClientConfigFields() : data() { } - } client_config; -}; + } __packed__ client_config; +} __packed__; struct C_LoginExtended_GC_9E : C_Login_GC_9E { SC_MeetUserExtension extension; -}; +} __packed__; struct C_Login_XB_9E : C_Login_GC_9E { XBNetworkLocation netloc; parray unknown_a1; -}; +} __packed__; struct C_LoginExtended_XB_9E : C_Login_XB_9E { SC_MeetUserExtension extension; -}; +} __packed__; struct C_LoginExtended_BB_9E { - le_uint32_t player_tag; - le_uint32_t guild_card_number; // == serial_number when on newserv - le_uint32_t sub_version; - le_uint32_t unknown_a1; - le_uint32_t unknown_a2; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; // == serial_number when on newserv + le_uint32_t sub_version = 0; + le_uint32_t unknown_a1 = 0; + le_uint32_t unknown_a2 = 0; ptext unknown_a3; // Always blank? ptext unknown_a4; // == "?" ptext unknown_a5; // Always blank? @@ -1577,7 +1576,7 @@ struct C_LoginExtended_BB_9E { ptext guild_card_number_str; parray unknown_a7; SC_MeetUserExtension extension; -}; +} __packed__; // 9F (S->C): Request client config / security data (V3/BB) // This command is not valid on PSO GC Episodes 1&2 Trial Edition, nor any @@ -1597,10 +1596,10 @@ struct C_LoginExtended_BB_9E { // same arguments on DC/PC. struct C_ChangeShipOrBlock_A0_A1 { - le_uint32_t player_tag; - le_uint32_t guild_card_number; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; parray unused; -}; +} __packed__; // A0 (S->C): Ship select menu // Same as 07 command. @@ -1622,15 +1621,15 @@ struct C_ChangeShipOrBlock_A0_A1 { template struct S_QuestMenuEntry { - le_uint32_t menu_id; - le_uint32_t item_id; + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; ptext name; ptext short_description; -}; -struct S_QuestMenuEntry_PC_A2_A4 : S_QuestMenuEntry { }; -struct S_QuestMenuEntry_DC_GC_A2_A4 : S_QuestMenuEntry { }; -struct S_QuestMenuEntry_XB_A2_A4 : S_QuestMenuEntry { }; -struct S_QuestMenuEntry_BB_A2_A4 : S_QuestMenuEntry { }; +} __packed__; +struct S_QuestMenuEntry_PC_A2_A4 : S_QuestMenuEntry { } __packed__; +struct S_QuestMenuEntry_DC_GC_A2_A4 : S_QuestMenuEntry { } __packed__; +struct S_QuestMenuEntry_XB_A2_A4 : S_QuestMenuEntry { } __packed__; +struct S_QuestMenuEntry_BB_A2_A4 : S_QuestMenuEntry { } __packed__; // A3 (S->C): Quest information // Same format as 1A/D5 command (plain text) @@ -1686,15 +1685,15 @@ struct S_QuestMenuEntry_BB_A2_A4 : S_QuestMenuEntry { }; // because the following command (AB) is definitely not valid on that version. struct C_UpdateQuestStatistics_V3_BB_AA { - le_uint16_t quest_internal_id; - le_uint16_t unused; - le_uint16_t request_token; - le_uint16_t unknown_a1; - le_uint32_t unknown_a2; - le_uint32_t kill_count; - le_uint32_t time_taken; // in seconds + le_uint16_t quest_internal_id = 0; + le_uint16_t unused = 0; + le_uint16_t request_token = 0; + le_uint16_t unknown_a1 = 0; + le_uint32_t unknown_a2 = 0; + le_uint32_t kill_count = 0; + le_uint32_t time_taken = 0; // in seconds parray unknown_a3; -}; +} __packed__; // AB (S->C): Confirm update quest statistics (V3/BB) // This command is not valid on PSO GC Episodes 1&2 Trial Edition. @@ -1702,11 +1701,11 @@ struct C_UpdateQuestStatistics_V3_BB_AA { // all there, or is the handler an undeleted vestige from Episodes 1&2? struct S_ConfirmUpdateQuestStatistics_V3_BB_AB { - le_uint16_t unknown_a1; // 0 - be_uint16_t unknown_a2; // Probably actually unused - le_uint16_t request_token; // Should match token sent in AA command - le_uint16_t unknown_a3; // Schtserv always sends 0xBFFF here -}; + le_uint16_t unknown_a1 = 0; // 0 + be_uint16_t unknown_a2 = 0; // Probably actually unused + le_uint16_t request_token = 0; // Should match token sent in AA command + le_uint16_t unknown_a3 = 0; // Schtserv always sends 0xBFFF here +} __packed__; // AC: Quest barrier (V3/BB) // No arguments; header.flag must be 0 (or else the client disconnects) @@ -1761,11 +1760,11 @@ struct S_ExecuteCode_B2 { // If code_size == 0, no code is executed, but checksumming may still occur. // In that case, this structure is the entire body of the command (no footer // is sent). - le_uint32_t code_size; // Size of code (following this struct) and footer - le_uint32_t checksum_start; // May be null if size is zero - le_uint32_t checksum_size; // If zero, no checksum is computed + le_uint32_t code_size = 0; // Size of code (following this struct) and footer + le_uint32_t checksum_start = 0; // May be null if size is zero + le_uint32_t checksum_size = 0; // If zero, no checksum is computed // The code immediately follows, ending with an S_ExecuteCode_Footer_B2 -}; +} __packed__; template struct S_ExecuteCode_Footer_B2 { @@ -1787,19 +1786,19 @@ struct S_ExecuteCode_Footer_B2 { // relocations_offset points there, so those 12 bytes may also be omitted from // the command entirely (without changing code_size - so code_size would // technically extend beyond the end of the B2 command). - LongT relocations_offset; // Relative to code base (after checksum_size) - LongT num_relocations; + LongT relocations_offset = 0; // Relative to code base (after checksum_size) + LongT num_relocations = 0; parray unused1; // entrypoint_offset is doubly indirect - it points to a pointer to a 32-bit // value that itself is the actual entrypoint. This is presumably done so the // entrypoint can be optionally relocated. - LongT entrypoint_addr_offset; // Relative to code base (after checksum_size). + LongT entrypoint_addr_offset = 0; // Relative to code base (after checksum_size). parray unused2; -}; +} __packed__; -struct S_ExecuteCode_Footer_GC_B2 : S_ExecuteCode_Footer_B2 { }; +struct S_ExecuteCode_Footer_GC_B2 : S_ExecuteCode_Footer_B2 { } __packed__; struct S_ExecuteCode_Footer_DC_PC_XB_BB_B2 - : S_ExecuteCode_Footer_B2 { }; + : S_ExecuteCode_Footer_B2 { } __packed__; // B3 (C->S): Execute code and/or checksum memory result // Not used on versions that don't support the B2 command (see above). @@ -1810,9 +1809,9 @@ struct C_ExecuteCodeResult_B3 { // On GC, return_value has the value in r3 when the function returns. // On XB and BB, return_value has the value in eax when the function returns. // If code_size was 0 in the B2 command, return_value is always 0. - le_uint32_t return_value; - le_uint32_t checksum; // 0 if no checksum was computed -}; + le_uint32_t return_value = 0; + le_uint32_t checksum = 0; // 0 if no checksum was computed +} __packed__; // B4: Invalid command // B5: Invalid command @@ -1821,12 +1820,12 @@ struct C_ExecuteCodeResult_B3 { // B7 (S->C): Rank update (Episode 3) struct S_RankUpdate_GC_Ep3_B7 { - le_uint32_t rank; + le_uint32_t rank = 0; ptext rank_text; - le_uint32_t meseta; - le_uint32_t max_meseta; - le_uint32_t jukebox_songs_unlocked; -}; + le_uint32_t meseta = 0; + le_uint32_t max_meseta = 0; + le_uint32_t jukebox_songs_unlocked = 0; +} __packed__; // B7 (C->S): Confirm rank update (Episode 3) // No arguments @@ -1859,7 +1858,7 @@ struct S_UpdateMediaHeader_GC_Ep3_B9 { // 'NMDM', and 'NSSM' are found in some of the game's existing BML files, but // the others don't seem to be anywhere on the disc. 'NJBM' is found in // psohistory_e.sfd, but not in any other files. - le_uint32_t type; + le_uint32_t type = 0; // Valid values for the type field (at least, when type is 1): // 0: Unknown // 1: Set lobby banner 1 (in front of where player 0 enters) @@ -1869,15 +1868,15 @@ struct S_UpdateMediaHeader_GC_Ep3_B9 { // 5: Unknown // 6: Unknown // Any other value: entire command is ignored - le_uint32_t which; - le_uint16_t size; - le_uint16_t unused; + le_uint32_t which = 0; + le_uint16_t size = 0; + le_uint16_t unused = 0; // The PRS-compressed data immediately follows this header. The maximum size // of the compressed data is 0x3800 bytes, and the data must decompress to // fewer than 0x37000 bytes of output. The size field above contains the // compressed size of this data (the decompressed size is not included // anywhere in the command). -}; +} __packed__; // B9 (C->S): Confirm received B9 (Episode 3) // No arguments @@ -1887,16 +1886,16 @@ struct S_UpdateMediaHeader_GC_Ep3_B9 { // This command is not valid on Episode 3 Trial Edition. struct C_Meseta_GC_Ep3_BA { - le_uint32_t transaction_num; - le_uint32_t value; - le_uint32_t request_token; -}; + le_uint32_t transaction_num = 0; + le_uint32_t value = 0; + le_uint32_t request_token = 0; +} __packed__; struct S_Meseta_GC_Ep3_BA { - le_uint32_t remaining_meseta; - le_uint32_t total_meseta_awarded; - le_uint32_t request_token; // Should match the token sent by the client -}; + le_uint32_t remaining_meseta = 0; + le_uint32_t total_meseta_awarded = 0; + le_uint32_t request_token = 0; // Should match the token sent by the client +} __packed__; // BB (S->C): Unknown (Episode 3) // header.flag is used, but it's not clear for what. It may be the number of @@ -1905,14 +1904,14 @@ struct S_Meseta_GC_Ep3_BA { struct S_Unknown_GC_Ep3_BB { struct Entry { - uint8_t unknown_a1[0x20]; - le_uint16_t unknown_a2; - le_uint16_t unknown_a3; - }; + parray unknown_a1; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + } __packed__; // The first entry here is probably fake, like for ship select menus (07) - Entry entries[0x21]; - uint8_t unknown_a3[0x900]; -}; + parray entries; + parray unknown_a3; +} __packed__; // BC: Invalid command // BD: Invalid command @@ -1930,13 +1929,13 @@ template struct S_ChoiceSearchEntry { // Category IDs are nonzero; if the high byte of the ID is nonzero then the // category can be set by the user at any time; otherwise it can't. - ItemIDT parent_category_id; // 0 for top-level categories - ItemIDT category_id; + ItemIDT parent_category_id = 0; // 0 for top-level categories + ItemIDT category_id = 0; ptext text; -}; -struct S_ChoiceSearchEntry_DC_C0 : S_ChoiceSearchEntry { }; -struct S_ChoiceSearchEntry_V3_C0 : S_ChoiceSearchEntry { }; -struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry { }; +} __packed__; +struct S_ChoiceSearchEntry_DC_C0 : S_ChoiceSearchEntry { } __packed__; +struct S_ChoiceSearchEntry_V3_C0 : S_ChoiceSearchEntry { } __packed__; +struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry { } __packed__; // Top-level categories are things like "Level", "Class", etc. // Choices for each top-level category immediately follow the category, so @@ -1958,40 +1957,40 @@ struct C_CreateGame { parray unused; ptext name; ptext password; - uint8_t difficulty; // 0-3 (always 0 on Episode 3) - uint8_t battle_mode; // 0 or 1 (always 0 on Episode 3) + uint8_t difficulty = 0; // 0-3 (always 0 on Episode 3) + uint8_t battle_mode = 0; // 0 or 1 (always 0 on Episode 3) // Note: Episode 3 uses the challenge mode flag for view battle permissions. // 0 = view battle allowed; 1 = not allowed - uint8_t challenge_mode; // 0 or 1 + uint8_t challenge_mode = 0; // 0 or 1 // Note: According to the Sylverant wiki, in v2-land, the episode field has a // different meaning: if set to 0, the game can be joined by v1 and v2 // players; if set to 1, it's v2-only. - uint8_t episode; // 1-4 on V3+ (3 on Episode 3); unused on DC/PC -}; -struct C_CreateGame_DC_V3_0C_C1_Ep3_EC : C_CreateGame { }; -struct C_CreateGame_PC_C1 : C_CreateGame { }; + uint8_t episode = 0; // 1-4 on V3+ (3 on Episode 3); unused on DC/PC +} __packed__; +struct C_CreateGame_DC_V3_0C_C1_Ep3_EC : C_CreateGame { } __packed__; +struct C_CreateGame_PC_C1 : C_CreateGame { } __packed__; struct C_CreateGame_BB_C1 : C_CreateGame { - uint8_t solo_mode; - uint8_t unused2[3]; -}; + uint8_t solo_mode = 0; + parray unused2; +} __packed__; // C2 (C->S): Set choice search parameters // Server does not respond. template struct C_ChoiceSearchSelections_C2_C3 { - le_uint16_t disabled; // 0 = enabled, 1 = disabled. Unused for command C3 - le_uint16_t unused; + le_uint16_t disabled = 0; // 0 = enabled, 1 = disabled. Unused for command C3 + le_uint16_t unused = 0; struct Entry { - ItemIDT parent_category_id; - ItemIDT category_id; - }; + ItemIDT parent_category_id = 0; + ItemIDT category_id = 0; + } __packed__; Entry entries[0]; -}; +} __packed__; -struct C_ChoiceSearchSelections_DC_C2_C3 : C_ChoiceSearchSelections_C2_C3 { }; -struct C_ChoiceSearchSelections_PC_V3_BB_C2_C3 : C_ChoiceSearchSelections_C2_C3 { }; +struct C_ChoiceSearchSelections_DC_C2_C3 : C_ChoiceSearchSelections_C2_C3 { } __packed__; +struct C_ChoiceSearchSelections_PC_V3_BB_C2_C3 : C_ChoiceSearchSelections_C2_C3 { } __packed__; // C3 (C->S): Execute choice search // Same format as C2. The disabled field is unused. @@ -2001,7 +2000,7 @@ struct C_ChoiceSearchSelections_PC_V3_BB_C2_C3 : C_ChoiceSearchSelections_C2_C3< // Command is a list of these; header.flag is the entry count struct S_ChoiceSearchResultEntry_V3_C4 { - le_uint32_t guild_card_number; + le_uint32_t guild_card_number = 0; ptext name; // No language marker, as usual on V3 ptext info_string; // Usually something like " Lvl " // Format is stricter here; this is "LOBBYNAME,BLOCKNUM,SHIPNAME" @@ -2009,14 +2008,14 @@ struct S_ChoiceSearchResultEntry_V3_C4 { // If target is in lobby, for example, "BLOCK01-1,BLOCK01,Alexandria" ptext locator_string; // Server IP and port for "meet user" option - le_uint32_t server_ip; - le_uint16_t server_port; - le_uint16_t unused1; - le_uint32_t menu_id; - le_uint32_t lobby_id; // These two are guesses - le_uint32_t game_id; // Zero if target is in a lobby rather than a game + le_uint32_t server_ip = 0; + le_uint16_t server_port = 0; + le_uint16_t unused1 = 0; + le_uint32_t menu_id = 0; + le_uint32_t lobby_id = 0; // These two are guesses + le_uint32_t game_id = 0; // Zero if target is in a lobby rather than a game parray unused2; -}; +} __packed__; // C5 (S->C): Challenge rank update (V3/BB) // header.flag = entry count @@ -2036,10 +2035,10 @@ struct S_ChoiceSearchResultEntry_V3_C4 { template struct C_SetBlockedSenders_C6 { parray blocked_senders; -}; +} __packed__; -struct C_SetBlockedSenders_V3_C6 : C_SetBlockedSenders_C6<30> { }; -struct C_SetBlockedSenders_BB_C6 : C_SetBlockedSenders_C6<28> { }; +struct C_SetBlockedSenders_V3_C6 : C_SetBlockedSenders_C6<30> { } __packed__; +struct C_SetBlockedSenders_BB_C6 : C_SetBlockedSenders_C6<28> { } __packed__; // C7 (C->S): Enable simple mail auto-reply (V3/BB) // Same format as 1A/D5 command (plain text). @@ -2059,8 +2058,8 @@ struct C_SetBlockedSenders_BB_C6 : C_SetBlockedSenders_C6<28> { }; // The CA command format is the same as that of the 6xB3 commands, and the // subsubcommands formats are shared as well. Unlike the 6x commands, the server // is expected to respond to the command appropriately instead of forwarding it. -// However, not all 6xB3 subsubcommands are also CA subcommands - those that are -// shared are noted in the structure names. (Search for "CAx" to find them.) +// Because the formats are shared, the 6xB3 commands are also referred to as CAx +// commands in the comments and structure names. // CB: Broadcast command (Episode 3) // Same as 60, but only send to Episode 3 clients. @@ -2081,12 +2080,12 @@ struct S_ConfirmTournamentEntry_GC_Ep3_CC { ptext server_name; ptext start_time; // e.g. "15:09:30" or "13:03 PST" struct Entry { - le_uint16_t unknown_a1; - le_uint16_t present; // 1 if team present, 0 otherwise + le_uint16_t unknown_a1 = 0; + le_uint16_t present = 0; // 1 if team present, 0 otherwise ptext team_name; - }; - Entry entries[0x20]; -}; + } __packed__; + parray entries; +} __packed__; // CD: Invalid command // CE: Invalid command @@ -2115,13 +2114,13 @@ struct S_ConfirmTournamentEntry_GC_Ep3_CC { // disconnects during the sequence. struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server - le_uint16_t target_client_id; - le_uint16_t item_count; + le_uint16_t target_client_id = 0; + le_uint16_t item_count = 0; // Note: PSO GC sends uninitialized data in the unused entries of this // command. newserv parses and regenerates the item data when sending D3, // which effectively erases the uninitialized data. - ItemData items[0x20]; -}; + parray items; +} __packed__; // D1 (S->C): Advance trade state (V3/BB) // No arguments @@ -2161,7 +2160,7 @@ struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server struct C_GBAGameRequest_V3_D7 { ptext filename; -}; +} __packed__; // D7 (S->C): Unknown (V3/BB) // No arguments @@ -2185,9 +2184,9 @@ template struct S_InfoBoardEntry_D8 { ptext name; ptext message; -}; -struct S_InfoBoardEntry_BB_D8 : S_InfoBoardEntry_D8 { }; -struct S_InfoBoardEntry_V3_D8 : S_InfoBoardEntry_D8 { }; +} __packed__; +struct S_InfoBoardEntry_BB_D8 : S_InfoBoardEntry_D8 { } __packed__; +struct S_InfoBoardEntry_V3_D8 : S_InfoBoardEntry_D8 { } __packed__; // D9 (C->S): Write info board (V3/BB) // Contents are plain text, like 1A/D5. @@ -2205,11 +2204,11 @@ struct C_VerifyLicense_V3_DB { ptext serial_number; // On XB, this is the XBL gamertag ptext access_key; // On XB, this is the XBL user ID ptext unused2; - le_uint32_t sub_version; + le_uint32_t sub_version = 0; ptext serial_number2; // On XB, this is the XBL gamertag ptext access_key2; // On XB, this is the XBL user ID ptext password; // On XB, this contains "xbox-pso" -}; +} __packed__; // Note: This login pathway generally isn't used on BB (and isn't supported at // all during the data server phase). All current servers use 03/93 instead. @@ -2219,11 +2218,11 @@ struct C_VerifyLicense_BB_DB { ptext unknown_a4; // == "?" ptext unknown_a5; // Always blank? ptext unknown_a6; // Always blank? - le_uint32_t sub_version; + le_uint32_t sub_version = 0; ptext username; ptext password; ptext game_tag; // "psopc2" -}; +} __packed__; // DC: Player menu state (Episode 3) // No arguments. It seems the client expects the server to respond with another @@ -2236,22 +2235,22 @@ struct C_VerifyLicense_BB_DB { // DC: Guild card data (BB) struct S_GuildCardHeader_BB_01DC { - le_uint32_t unknown; // should be 1 - le_uint32_t filesize; // 0x0000D590 - le_uint32_t checksum; // CRC32 of entire guild card file (0xD590 bytes) -}; + le_uint32_t unknown = 1; + le_uint32_t filesize = 0x0000D590; + le_uint32_t checksum = 0; // CRC32 of entire guild card file (0xD590 bytes) +} __packed__; struct S_GuildCardFileChunk_02DC { - le_uint32_t unknown; // 0 - le_uint32_t chunk_index; + le_uint32_t unknown = 0; // 0 + le_uint32_t chunk_index = 0; uint8_t data[0x6800]; // Command may be shorter if this is the last chunk -}; +} __packed__; struct C_GuildCardDataRequest_BB_03DC { - le_uint32_t unknown; - le_uint32_t chunk_index; - le_uint32_t cont; -}; + le_uint32_t unknown = 0; + le_uint32_t chunk_index = 0; + le_uint32_t cont = 0; +} __packed__; // DD (S->C): Send quest state to joining player (BB) // When a player joins a game with a quest already in progress, the server @@ -2264,42 +2263,42 @@ struct C_GuildCardDataRequest_BB_03DC { struct S_RareMonsterList_BB_DE { // Unused entries are set to FFFF - le_uint16_t enemy_ids[16]; -}; + parray enemy_ids; +} __packed__; // DF (C->S): Unknown (BB) // This command has many subcommands. It's not clear what any of them do. struct C_Unknown_BB_01DF { - le_uint32_t unknown_a1; -}; + le_uint32_t unknown_a1 = 0; +} __packed__; struct C_Unknown_BB_02DF { - le_uint32_t unknown_a1; -}; + le_uint32_t unknown_a1 = 0; +} __packed__; struct C_Unknown_BB_03DF { - le_uint32_t unknown_a1; -}; + le_uint32_t unknown_a1 = 0; +} __packed__; struct C_Unknown_BB_04DF { - le_uint32_t unknown_a1; -}; + le_uint32_t unknown_a1 = 0; +} __packed__; struct C_Unknown_BB_05DF { - le_uint32_t unknown_a1; + le_uint32_t unknown_a1 = 0; ptext unknown_a2; -}; +} __packed__; struct C_Unknown_BB_06DF { parray unknown_a1; -}; +} __packed__; struct C_Unknown_BB_07DF { - le_uint32_t unused1; // Always 0xFFFFFFFF - le_uint32_t unused2; // Always 0 + le_uint32_t unused1 = 0xFFFFFFFF; + le_uint32_t unused2 = 0; // ALways 0 parray unknown_a1; -}; +} __packed__; // E0 (S->C): Tournament list (Episode 3) // The client will send 09 and 10 commands to inspect or enter a tournament. The @@ -2309,17 +2308,17 @@ struct C_Unknown_BB_07DF { // header.flag is the count of filled-in entries. struct S_TournamentList_GC_Ep3_E0 { struct Entry { - le_uint32_t menu_id; - le_uint32_t item_id; + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; parray unknown_a1; - le_uint32_t start_time; // In seconds since Unix epoch + le_uint32_t start_time = 0; // In seconds since Unix epoch ptext name; - le_uint16_t num_teams; - le_uint16_t max_teams; + le_uint16_t num_teams = 0; + le_uint16_t max_teams = 0; parray unknown_a3; - }; - Entry entries[0x20]; -}; + } __packed__; + parray entries; +} __packed__; // E0 (C->S): Request team and key config (BB) @@ -2330,12 +2329,12 @@ struct S_Unknown_GC_Ep3_E1 { struct Entry { ptext name; ptext description; - }; - /* 0024 */ Entry entries[4]; + } __packed__; + /* 0024 */ parray entries; /* 00E4 */ parray unknown_a3; /* 0104 */ parray unknown_a4; /* 0118 */ parray unknown_a5; -}; +} __packed__; // E2 (C->S): Tournament control (Episode 3) // No arguments (in any of its forms) except header.flag, which determines ths @@ -2358,16 +2357,16 @@ struct S_Unknown_GC_Ep3_E1 { // command. struct S_TournamentEntryList_GC_Ep3_E2 { - le_uint16_t unknown_a1; - le_uint16_t unknown_a2; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; struct Entry { - le_uint32_t menu_id; - le_uint32_t item_id; + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; parray unknown_a1; ptext team_name; - }; - Entry entries[0x20]; -}; + } __packed__; + parray entries; +} __packed__; // E2 (S->C): Team and key config (BB) // See KeyAndTeamConfigBB in Player.hh for format @@ -2376,28 +2375,28 @@ struct S_TournamentEntryList_GC_Ep3_E2 { struct S_TournamentInfo_GC_Ep3_E3 { struct Entry { - le_uint16_t unknown_a1; - le_uint16_t unknown_a2; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; ptext team_name; - }; + } __packed__; ptext name; ptext map_name; - Ep3BattleRules rules; - Entry entries[0x20]; + Episode3::Rules rules; + parray entries; parray unknown_a2; - le_uint16_t max_entries; - le_uint16_t unknown_a3; - le_uint16_t unknown_a4; - le_uint16_t unknown_a5; + le_uint16_t max_entries = 0; + le_uint16_t unknown_a3 = 0; + le_uint16_t unknown_a4 = 0; + le_uint16_t unknown_a5 = 0; parray unknown_a6; -}; +} __packed__; // E3 (C->S): Player preview request (BB) struct C_PlayerPreviewRequest_BB_E3 { - le_uint32_t player_index; - le_uint32_t unused; -}; + le_uint32_t player_index = 0; + le_uint32_t unused = 0; +} __packed__; // E4: CARD lobby battle table state (Episode 3) // When client sends an E4, server should respond with another E4 (but these @@ -2405,47 +2404,47 @@ struct C_PlayerPreviewRequest_BB_E3 { // Header flag = seated state (1 = present, 0 = leaving) struct C_CardBattleTableState_GC_Ep3_E4 { - le_uint16_t table_number; - le_uint16_t seat_number; -}; + le_uint16_t table_number = 0; + le_uint16_t seat_number = 0; +} __packed__; // Header flag = table number struct S_CardBattleTableState_GC_Ep3_E4 { struct Entry { - le_uint16_t present; // 1 = player present, 0 = no player - le_uint16_t unknown_a1; - le_uint32_t guild_card_number; - }; - Entry entries[4]; -}; + le_uint16_t present = 0; // 1 = player present, 0 = no player + le_uint16_t unknown_a1 = 0; + le_uint32_t guild_card_number = 0; + } __packed__; + parray entries; +} __packed__; // E4 (S->C): Player choice or no player present (BB) struct S_ApprovePlayerChoice_BB_00E4 { - le_uint32_t player_index; - le_uint32_t result; // 1 = approved -}; + le_uint32_t player_index = 0; + le_uint32_t result = 0; // 1 = approved +} __packed__; struct S_PlayerPreview_NoPlayer_BB_00E4 { - le_uint32_t player_index; - le_uint32_t error; // 2 = no player present -}; + le_uint32_t player_index = 0; + le_uint32_t error = 0; // 2 = no player present +} __packed__; // E5 (C->S): Confirm CARD lobby battle table choice (Episode 3) // header.flag specifies whether the client answered "Yes" (1) or "No" (0). struct S_CardBattleTableConfirmation_GC_Ep3_E5 { - le_uint16_t table_number; - le_uint16_t seat_number; -}; + le_uint16_t table_number = 0; + le_uint16_t seat_number = 0; +} __packed__; // E5 (S->C): Player preview (BB) // E5 (C->S): Create character (BB) struct SC_PlayerPreview_CreateCharacter_BB_00E5 { - le_uint32_t player_index; + le_uint32_t player_index = 0; PlayerDispDataBBPreview preview; -}; +} __packed__; // E6 (C->S): Spectator team control (Episode 3) @@ -2457,9 +2456,9 @@ struct SC_PlayerPreview_CreateCharacter_BB_00E5 { // form: struct C_JoinSpectatorTeam_GC_Ep3_E6_Flag01 { - le_uint32_t menu_id; - le_uint32_t item_id; -}; + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; +} __packed__; // E6 (S->C): Spectator team list (Episode 3) // Same format as 08 command. @@ -2470,23 +2469,23 @@ struct C_JoinSpectatorTeam_GC_Ep3_E6_Flag01 { // set by the 04 command (and returned in the 9E and 9F commands). struct S_ClientInit_BB_00E6 { - le_uint32_t error; - le_uint32_t player_tag; - le_uint32_t guild_card_number; - le_uint32_t team_id; + le_uint32_t error = 0; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + le_uint32_t team_id = 0; ClientConfigBB cfg; - le_uint32_t caps; // should be 0x00000102 -}; + le_uint32_t caps = 0x00000102; +} __packed__; // E7 (C->S): Create spectator team (Episode 3) struct C_CreateSpectatorTeam_GC_Ep3_E7 { - le_uint32_t menu_id; - le_uint32_t item_id; + le_uint32_t menu_id = 0; + le_uint32_t item_id = 0; ptext name; ptext password; - le_uint32_t unused; -}; + le_uint32_t unused = 0; +} __packed__; // E7 (S->C): Unknown (Episode 3) // Same format as E2 command. @@ -2504,29 +2503,29 @@ struct S_JoinSpectatorTeam_GC_Ep3_E8 { PlayerLobbyDataDCGC lobby_data; // 0x20 bytes PlayerInventory inventory; // 0x34C bytes PlayerDispDataDCPCV3 disp; // 0xD0 bytes - }; // 0x43C bytes - PlayerEntry players[4]; // 84-1174 + } __packed__; // 0x43C bytes + parray players; // 84-1174 parray unknown_a2; // 1174-117C - le_uint32_t unknown_a3; // 117C-1180 + le_uint32_t unknown_a3 = 0; // 117C-1180 parray unknown_a4; // 1180-1184 struct SpectatorEntry { - le_uint32_t player_tag; - le_uint32_t guild_card_number; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; ptext name; - uint8_t unknown_a3[2]; - le_uint16_t unknown_a4; + parray unknown_a3; + le_uint16_t unknown_a4 = 0; parray unknown_a5; parray unknown_a6; - }; // 0x38 bytes + } __packed__; // 0x38 bytes // Somewhat misleadingly, this array also includes the players actually in the // battle - they appear in the first positions. Presumably the first 4 are // always for battlers, and the last 8 are always for spectators. - SpectatorEntry entries[12]; // 1184-1424 + parray entries; // 1184-1424 ptext spectator_team_name; // This field doesn't appear to be actually used by the game, but some servers // send it anyway (and the game presumably ignores it) - PlayerEntry spectator_players[8]; -}; + parray spectator_players; +} __packed__; // E8 (C->S): Guild card commands (BB) @@ -2535,9 +2534,9 @@ struct S_JoinSpectatorTeam_GC_Ep3_E8 { // This struct is for documentation purposes only; newserv ignores the contents // of this command. struct C_GuildCardChecksum_01E8 { - le_uint32_t checksum; - le_uint32_t unused; -}; + le_uint32_t checksum = 0; + le_uint32_t unused = 0; +} __packed__; // 02E8 (S->C): Accept/decline guild card file checksum // If needs_update is nonzero, the client will request the guild card file by @@ -2546,9 +2545,9 @@ struct C_GuildCardChecksum_01E8 { // stream file) instead. struct S_GuildCardChecksumResponse_BB_02E8 { - le_uint32_t needs_update; - le_uint32_t unused; -}; + le_uint32_t needs_update = 0; + le_uint32_t unused = 0; +} __packed__; // 03E8 (C->S): Request guild card file // No arguments @@ -2560,8 +2559,8 @@ struct S_GuildCardChecksumResponse_BB_02E8 { // 05E8 (C->S): Delete guild card struct C_DeleteGuildCard_BB_05E8_08E8 { - le_uint32_t guild_card_number; -}; + le_uint32_t guild_card_number = 0; +} __packed__; // 06E8 (C->S): Update (overwrite) guild card // Note: This command is also sent when the player writes a comment on their own @@ -2577,16 +2576,16 @@ struct C_DeleteGuildCard_BB_05E8_08E8 { // 09E8 (C->S): Write comment struct C_WriteGuildCardComment_BB_09E8 { - le_uint32_t guild_card_number; + le_uint32_t guild_card_number = 0; ptext comment; -}; +} __packed__; // 0AE8 (C->S): Set guild card position in list struct C_MoveGuildCard_BB_0AE8 { - le_uint32_t guild_card_number; - le_uint32_t position; -}; + le_uint32_t guild_card_number = 0; + le_uint32_t position = 0; +} __packed__; // E9 (S->C): Remove player from spectator team (Episode 3) // Same format as 66/69 commands. Like 69 (and unlike 66), the disable_udp field @@ -2602,9 +2601,9 @@ struct C_MoveGuildCard_BB_0AE8 { // differences though. struct S_TimedMessageBoxHeader_GC_Ep3_EA { - le_uint32_t duration; // In frames; 30 frames = 1 second + le_uint32_t duration = 0; // In frames; 30 frames = 1 second // Message data follows here (up to 0x1000 chars) -}; +} __packed__; // EA: Team control (BB) @@ -2612,13 +2611,13 @@ struct S_TimedMessageBoxHeader_GC_Ep3_EA { struct C_CreateTeam_BB_01EA { ptext name; -}; +} __packed__; // 03EA (C->S): Add team member struct C_AddOrRemoveTeamMember_BB_03EA_05EA { - le_uint32_t guild_card_number; -}; + le_uint32_t guild_card_number = 0; +} __packed__; // 05EA (C->S): Remove team member // Same format as 03EA. @@ -2637,7 +2636,7 @@ struct C_AddOrRemoveTeamMember_BB_03EA_05EA { struct C_SetTeamFlag_BB_0FEA { parray data; -}; +} __packed__; // 10EA: Delete team // No arguments @@ -2646,8 +2645,8 @@ struct C_SetTeamFlag_BB_0FEA { // TODO: header.flag is used for this command. Figure out what it's for. struct C_PromoteTeamMember_BB_11EA { - le_uint32_t unknown_a1; -}; + le_uint32_t unknown_a1 = 0; +} __packed__; // 12EA (S->C): Unknown @@ -2681,7 +2680,7 @@ struct C_PromoteTeamMember_BB_11EA { struct C_Unknown_BB_1EEA { ptext unknown_a1; -}; +} __packed__; // 20EA (C->S): Unknown // header.flag is used, but no other arguments @@ -2699,18 +2698,18 @@ struct C_Unknown_BB_1EEA { // Command is a list of these; header.flag is the entry count. struct S_StreamFileIndexEntry_BB_01EB { - le_uint32_t size; - le_uint32_t checksum; // CRC32 of file data - le_uint32_t offset; // offset in stream (== sum of all previous files' sizes) + le_uint32_t size = 0; + le_uint32_t checksum = 0; // CRC32 of file data + le_uint32_t offset = 0; // offset in stream (== sum of all previous files' sizes) ptext filename; -}; +} __packed__; // 02EB (S->C): Send stream file chunk (BB) struct S_StreamFileChunk_BB_02EB { - le_uint32_t chunk_index; + le_uint32_t chunk_index = 0; uint8_t data[0x6800]; -}; +} __packed__; // 03EB (C->S): Request a specific stream file chunk // header.flag is the chunk index. Server should respond with a 02EB command. @@ -2729,8 +2728,8 @@ struct C_LeaveCharacterSelect_BB_00EC { // 0 = canceled // 1 = recreate character // 2 = dressing room - le_uint32_t reason; -}; + le_uint32_t reason = 0; +} __packed__; // ED (S->C): Force leave lobby/game (Episode 3) // No arguments @@ -2747,7 +2746,7 @@ struct C_LeaveCharacterSelect_BB_00EC { // TODO: Actually define these structures and don't just treat them as raw data union C_UpdateAccountData_BB_ED { - le_uint32_t option; // 01ED + le_uint32_t option = 0; // 01ED parray symbol_chats; // 02ED parray chat_shortcuts; // 03ED parray key_config; // 04ED @@ -2755,7 +2754,7 @@ union C_UpdateAccountData_BB_ED { parray tech_menu; // 06ED parray customize; // 07ED parray challenge_battle_config; // 08ED -}; +} __packed__; // EE: Trade cards (Episode 3) // This command has different forms depending on the header.flag value; the flag @@ -2765,19 +2764,19 @@ union C_UpdateAccountData_BB_ED { // EE D0 (C->S): Begin trade struct SC_TradeCards_GC_Ep3_EE_FlagD0_FlagD3 { - le_uint16_t target_client_id; - le_uint16_t entry_count; + le_uint16_t target_client_id = 0; + le_uint16_t entry_count = 0; struct Entry { - le_uint32_t card_type; - le_uint32_t count; - }; - Entry entries[4]; -}; + le_uint32_t card_type = 0; + le_uint32_t count = 0; + } __packed__; + parray entries; +} __packed__; // EE D1 (S->C): Advance trade state struct S_AdvanceCardTradeState_GC_Ep3_EE_FlagD1 { - le_uint32_t unused; -}; + le_uint32_t unused = 0; +} __packed__; // EE D2 (C->S): Trade can proceed // No arguments @@ -2789,8 +2788,8 @@ struct S_AdvanceCardTradeState_GC_Ep3_EE_FlagD1 { // EE D4 (S->C): Trade complete struct S_CardTradeComplete_GC_Ep3_EE_FlagD4 { - le_uint32_t success; // 0 = failed, 1 = success, anything else = invalid -}; + le_uint32_t success = 0; // 0 = failed, 1 = success, anything else = invalid +} __packed__; // EE (S->C): Scrolling message (BB) // Same format as 01. The message appears at the top of the screen and slowly @@ -2802,14 +2801,14 @@ struct S_CardTradeComplete_GC_Ep3_EE_FlagD4 { // EF (S->C): Start card auction (Episode 3) struct S_StartCardAuction_GC_Ep3_EF { - le_uint16_t points_available; - le_uint16_t unused; + le_uint16_t points_available = 0; + le_uint16_t unused = 0; struct Entry { - le_uint16_t card_id; // Must be < 0x02F1 - le_uint16_t price; // Must be > 0 and < 100 - }; - Entry entries[0x14]; -}; + le_uint16_t card_id = 0xFFFF; // Must be < 0x02F1 + le_uint16_t min_price = 0; // Must be > 0 and < 100 + } __packed__; + parray entries; +} __packed__; // EF (S->C): Unknown (BB) // Has an unknown number of subcommands (00EF, 01EF, etc.) @@ -2818,11 +2817,11 @@ struct S_StartCardAuction_GC_Ep3_EF { // F0 (S->C): Unknown (BB) struct S_Unknown_BB_F0 { - le_uint32_t unknown_a1[7]; - le_uint32_t which; // Must be < 12 + parray unknown_a1; + le_uint32_t which = 0; // Must be < 12 ptext unknown_a2; - le_uint32_t unknown_a3; -}; + le_uint32_t unknown_a3 = 0; +} __packed__; // F1: Invalid command // F2: Invalid command @@ -2869,31 +2868,31 @@ struct S_Unknown_BB_F0 { // These common structures are used my many subcommands. struct G_ClientIDHeader { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; // <= 12 -}; + uint8_t subcommand = 0; + uint8_t size = 0; + le_uint16_t client_id = 0; // <= 12 +} __packed__; struct G_EnemyIDHeader { - uint8_t subcommand; - uint8_t size; - le_uint16_t enemy_id; // In range [0x1000, 0x4000) -}; + uint8_t subcommand = 0; + uint8_t size = 0; + le_uint16_t enemy_id = 0; // In range [0x1000, 0x4000) +} __packed__; struct G_ObjectIDHeader { - uint8_t subcommand; - uint8_t size; - le_uint16_t object_id; // >= 0x4000, != 0xFFFF -}; + uint8_t subcommand = 0; + uint8_t size = 0; + le_uint16_t object_id = 0; // >= 0x4000, != 0xFFFF +} __packed__; struct G_UnusedHeader { - uint8_t subcommand; - uint8_t size; - le_uint16_t unused; -}; + uint8_t subcommand = 0; + uint8_t size = 0; + le_uint16_t unused = 0; +} __packed__; template struct G_ExtendedHeader { HeaderT basic_header; - le_uint32_t size; -}; + le_uint32_t size = 0; +} __packed__; @@ -2912,7 +2911,7 @@ struct G_Unknown_6x04 { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unused; -}; +} __packed__; // 6x05: Switch state changed // Some things that don't look like switches are implemented as switches using @@ -2927,7 +2926,7 @@ struct G_SwitchStateChanged_6x05 { parray unknown_a3; uint8_t area; uint8_t flags; // Bit field, with 2 lowest bits having meaning -}; +} __packed__; // 6x06: Send guild card @@ -2943,13 +2942,13 @@ struct G_SendGuildCard_DC_PC_V3 { uint8_t present2; uint8_t section_id; uint8_t char_class; -}; +} __packed__; struct G_SendGuildCard_DC_6x06 : G_SendGuildCard_DC_PC_V3 { parray unused3; -}; -struct G_SendGuildCard_PC_6x06 : G_SendGuildCard_DC_PC_V3 { }; -struct G_SendGuildCard_V3_6x06 : G_SendGuildCard_DC_PC_V3 { }; +} __packed__; +struct G_SendGuildCard_PC_6x06 : G_SendGuildCard_DC_PC_V3 { } __packed__; +struct G_SendGuildCard_V3_6x06 : G_SendGuildCard_DC_PC_V3 { } __packed__; struct G_SendGuildCard_BB_6x06 { G_UnusedHeader header; @@ -2961,7 +2960,7 @@ struct G_SendGuildCard_BB_6x06 { uint8_t present2; uint8_t section_id; uint8_t char_class; -}; +} __packed__; // 6x07: Symbol chat @@ -2980,7 +2979,7 @@ struct G_SymbolChat_6x07 { uint8_t type; // FF = no object in this slot // Bits: 000VHCCC (V = reverse vertical, H = reverse horizontal, C = color) uint8_t flags_color; - }; + } __packed__; CornerObject corner_objects[4]; // In reading order (top-left is first) struct FacePart { uint8_t type; // FF = no part in this slot @@ -2988,9 +2987,9 @@ struct G_SymbolChat_6x07 { uint8_t y; // Bits: 000000VH (V = reverse vertical, H = reverse horizontal) uint8_t flags; - }; + } __packed__; FacePart face_parts[12]; -}; +} __packed__; // 6x08: Invalid subcommand @@ -2998,7 +2997,7 @@ struct G_SymbolChat_6x07 { struct G_Unknown_6x09 { G_EnemyIDHeader header; -}; +} __packed__; // 6x0A: Enemy hit @@ -3008,7 +3007,7 @@ struct G_EnemyHitByPlayer_6x0A { le_uint16_t enemy_id2; le_uint16_t damage; be_uint32_t flags; -}; +} __packed__; // 6x0B: Box destroyed @@ -3016,7 +3015,7 @@ struct G_BoxDestroyed_6x0B { G_ClientIDHeader header; le_uint32_t unknown_a2; le_uint32_t unknown_a3; -}; +} __packed__; // 6x0C: Add condition (poison/slow/etc.) @@ -3024,7 +3023,7 @@ struct G_AddOrRemoveCondition_6x0C_6x0D { G_ClientIDHeader header; le_uint32_t unknown_a1; // Probably condition type le_uint32_t unknown_a2; -}; +} __packed__; // 6x0D: Remove condition (poison/slow/etc.) // Same format as 6x0C @@ -3033,7 +3032,7 @@ struct G_AddOrRemoveCondition_6x0C_6x0D { struct G_Unknown_6x0E { G_ClientIDHeader header; -}; +} __packed__; // 6x0F: Invalid subcommand @@ -3044,7 +3043,7 @@ struct G_Unknown_6x10_6x11_6x12_6x14 { le_uint16_t unknown_a2; le_uint16_t unknown_a3; le_uint32_t unknown_a4; -}; +} __packed__; // 6x11: Unknown (not valid on Episode 3) // Same format as 6x10 @@ -3058,7 +3057,7 @@ struct G_DeRolLeBossActions_6x13 { G_EnemyIDHeader header; le_uint16_t unknown_a2; le_uint16_t unknown_a3; -}; +} __packed__; // 6x14: Unknown (supported; game only; not valid on Episode 3) // Same format as 6x10 @@ -3071,7 +3070,7 @@ struct G_VolOptBossActions_6x15 { le_uint16_t unknown_a3; le_uint16_t unknown_a4; le_uint16_t unknown_a5; -}; +} __packed__; // 6x16: Vol Opt boss actions (not valid on Episode 3) @@ -3079,7 +3078,7 @@ struct G_VolOptBossActions_6x16 { G_UnusedHeader header; parray unknown_a2; le_uint16_t unknown_a3; -}; +} __packed__; // 6x17: Unknown (supported; game only; not valid on Episode 3) @@ -3089,14 +3088,14 @@ struct G_Unknown_6x17 { le_float unknown_a3; le_float unknown_a4; le_uint32_t unknown_a5; -}; +} __packed__; // 6x18: Unknown (supported; game only; not valid on Episode 3) struct G_Unknown_6x18 { G_ClientIDHeader header; parray unknown_a2; -}; +} __packed__; // 6x19: Dark Falz boss actions (not valid on Episode 3) @@ -3106,7 +3105,7 @@ struct G_DarkFalzActions_6x19 { le_uint16_t unknown_a3; le_uint32_t unknown_a4; le_uint32_t unused; -}; +} __packed__; // 6x1A: Invalid subcommand @@ -3114,13 +3113,13 @@ struct G_DarkFalzActions_6x19 { struct G_Unknown_6x1B { G_ClientIDHeader header; -}; +} __packed__; // 6x1C: Unknown (supported; game only; not valid on Episode 3) struct G_Unknown_6x1C { G_ClientIDHeader header; -}; +} __packed__; // 6x1D: Invalid subcommand // 6x1E: Invalid subcommand @@ -3130,7 +3129,7 @@ struct G_Unknown_6x1C { struct G_Unknown_6x1F { G_ClientIDHeader header; le_uint32_t area; -}; +} __packed__; // 6x20: Set position (existing clients send when a new client joins a lobby/game) @@ -3141,21 +3140,21 @@ struct G_Unknown_6x20 { le_float y; le_float z; le_uint32_t unknown_a1; -}; +} __packed__; // 6x21: Inter-level warp struct G_InterLevelWarp_6x21 { G_ClientIDHeader header; le_uint32_t area; -}; +} __packed__; // 6x22: Set player invisible // 6x23: Set player visible struct G_SetPlayerVisibility_6x22_6x23 { G_ClientIDHeader header; -}; +} __packed__; // 6x24: Unknown (supported; game only) @@ -3165,7 +3164,7 @@ struct G_Unknown_6x24 { le_float x; le_float y; le_float z; -}; +} __packed__; // 6x25: Equip item @@ -3173,7 +3172,7 @@ struct G_EquipOrUnequipItem_6x25_6x26 { G_ClientIDHeader header; le_uint32_t item_id; le_uint32_t equip_slot; // Unused for 6x26 (unequip item) -}; +} __packed__; // 6x26: Unequip item // Same format as 6x25 @@ -3183,7 +3182,7 @@ struct G_EquipOrUnequipItem_6x25_6x26 { struct G_UseItem_6x27 { G_ClientIDHeader header; le_uint32_t item_id; -}; +} __packed__; // 6x28: Feed MAG @@ -3191,7 +3190,7 @@ struct G_FeedMAG_6x28 { G_ClientIDHeader header; le_uint32_t mag_item_id; le_uint32_t fed_item_id; -}; +} __packed__; // 6x29: Delete inventory item (via bank deposit / sale / feeding MAG) // This subcommand is also used for reducing the size of stacks - if amount is @@ -3201,7 +3200,7 @@ struct G_DeleteInventoryItem_6x29 { G_ClientIDHeader header; le_uint32_t item_id; le_uint32_t amount; -}; +} __packed__; // 6x2A: Drop item @@ -3213,21 +3212,21 @@ struct G_DropItem_6x2A { le_float x; le_float y; le_float z; -}; +} __packed__; // 6x2B: Create item in inventory (e.g. via tekker or bank withdraw) struct G_CreateInventoryItem_DC_6x2B { G_ClientIDHeader header; ItemData item; -}; +} __packed__; struct G_CreateInventoryItem_PC_V3_BB_6x2B { G_CreateInventoryItem_DC_6x2B basic_cmd; uint8_t unused1; uint8_t unknown_a2; le_uint16_t unused2; -}; +} __packed__; // 6x2C: Talk to NPC @@ -3238,13 +3237,13 @@ struct G_TalkToNPC_6x2C { le_float unknown_a3; le_float unknown_a4; le_float unknown_a5; -}; +} __packed__; // 6x2D: Done talking to NPC struct G_EndTalkToNPC_6x2D { G_ClientIDHeader header; -}; +} __packed__; // 6x2E: Set and/or clear player flags @@ -3252,7 +3251,7 @@ struct G_SetOrClearPlayerFlags_6x2E { G_ClientIDHeader header; le_uint32_t and_mask; le_uint32_t or_mask; -}; +} __packed__; // 6x2F: Hit by enemy @@ -3261,7 +3260,7 @@ struct G_HitByEnemy_6x2F { le_uint32_t hit_type; // 0 = set HP, 1 = add/subtract HP, 2 = add/sub fixed HP le_uint16_t damage; le_uint16_t client_id; -}; +} __packed__; // 6x30: Level up @@ -3275,19 +3274,19 @@ struct G_LevelUp_6x30 { le_uint16_t ata; le_uint16_t level; le_uint16_t unknown_a1; // Must be 0 or 1 -}; +} __packed__; // 6x31: Medical center struct G_UseMedicalCenter_6x31 { G_ClientIDHeader header; -}; +} __packed__; // 6x32: Unknown (occurs when using Medical Center) struct G_Unknown_6x32 { G_UnusedHeader header; -}; +} __packed__; // 6x33: Revive player (e.g. with moon atomizer) @@ -3295,7 +3294,7 @@ struct G_RevivePlayer_6x33 { G_ClientIDHeader header; le_uint16_t client_id2; le_uint16_t unused; -}; +} __packed__; // 6x34: Unknown // This subcommand is completely ignored (at least, by PSO GC). @@ -3311,7 +3310,7 @@ struct G_PhotonBlast_6x37 { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unused; -}; +} __packed__; // 6x38: Unknown @@ -3319,25 +3318,25 @@ struct G_Unknown_6x38 { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unused; -}; +} __packed__; // 6x39: Photon blast ready struct G_PhotonBlastReady_6x38 { G_ClientIDHeader header; -}; +} __packed__; // 6x3A: Unknown (supported; game only) struct G_Unknown_6x3A { G_ClientIDHeader header; -}; +} __packed__; // 6x3B: Unknown (supported; lobby & game) struct G_Unknown_6x3B { G_ClientIDHeader header; -}; +} __packed__; // 6x3C: Invalid subcommand // 6x3D: Invalid subcommand @@ -3353,7 +3352,7 @@ struct G_StopAtPosition_6x3E { le_float x; le_float y; le_float z; -}; +} __packed__; // 6x3F: Set position @@ -3366,7 +3365,7 @@ struct G_SetPosition_6x3F { le_float x; le_float y; le_float z; -}; +} __packed__; // 6x40: Walk @@ -3375,7 +3374,7 @@ struct G_WalkToPosition_6x40 { le_float x; le_float z; le_uint32_t unknown_a1; -}; +} __packed__; // 6x41: Unknown // This subcommand is completely ignored (at least, by PSO GC). @@ -3386,7 +3385,7 @@ struct G_RunToPosition_6x42 { G_ClientIDHeader header; le_float x; le_float z; -}; +} __packed__; // 6x43: First attack @@ -3394,7 +3393,7 @@ struct G_Attack_6x43_6x44_6x45 { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unknown_a2; -}; +} __packed__; // 6x44: Second attack // Same format as 6x43 @@ -3410,9 +3409,9 @@ struct G_AttackFinished_6x46 { struct Entry { le_uint16_t unknown_a1; le_uint16_t unknown_a2; - }; + } __packed__; Entry entries[11]; -}; +} __packed__; // 6x47: Cast technique @@ -3430,9 +3429,9 @@ struct G_CastTechnique_6x47 { struct TargetEntry { le_uint16_t client_id; le_uint16_t unknown_a2; - }; + } __packed__; TargetEntry targets[10]; -}; +} __packed__; // 6x48: Cast technique complete @@ -3442,7 +3441,7 @@ struct G_CastTechniqueComplete_6x48 { // This level matches the level sent in the 6x47 command, even if that level // was overridden by a preceding 6x8D command. le_uint16_t level; -}; +} __packed__; // 6x49: Subtract PB energy @@ -3456,15 +3455,15 @@ struct G_SubtractPBEnergy_6x49 { struct Entry { le_uint16_t unknown_a1; le_uint16_t unknown_a2; - }; + } __packed__; Entry entries[14]; -}; +} __packed__; // 6x4A: Fully shield attack struct G_ShieldAttack_6x4A { G_ClientIDHeader header; -}; +} __packed__; // 6x4B: Hit by enemy @@ -3474,7 +3473,7 @@ struct G_HitByEnemy_6x4B_6x4C { le_uint16_t damage; le_float unknown_a2; le_float unknown_a3; -}; +} __packed__; // 6x4C: Hit by enemy // Same format as 6x4B @@ -3484,26 +3483,26 @@ struct G_HitByEnemy_6x4B_6x4C { struct G_Unknown_6x4D { G_ClientIDHeader header; le_uint32_t unknown_a1; -}; +} __packed__; // 6x4E: Unknown (supported; lobby & game) struct G_Unknown_6x4E { G_ClientIDHeader header; -}; +} __packed__; // 6x4F: Unknown (supported; lobby & game) struct G_Unknown_6x4F { G_ClientIDHeader header; -}; +} __packed__; // 6x50: Unknown (supported; lobby & game) struct G_Unknown_6x50 { G_ClientIDHeader header; le_uint32_t unknown_a1; -}; +} __packed__; // 6x51: Invalid subcommand @@ -3514,13 +3513,13 @@ struct G_Unknown_6x52 { le_uint16_t unknown_a1; le_uint16_t unknown_a2; le_uint32_t unknown_a3; -}; +} __packed__; // 6x53: Unknown (supported; game only) struct G_Unknown_6x53 { G_ClientIDHeader header; -}; +} __packed__; // 6x54: Unknown // This subcommand is completely ignored (at least, by PSO GC). @@ -3536,7 +3535,7 @@ struct G_IntraMapWarp_6x55 { le_float x2; le_float y2; le_float z2; -}; +} __packed__; // 6x56: Unknown (supported; lobby & game) @@ -3546,13 +3545,13 @@ struct G_Unknown_6x56 { le_float x; le_float y; le_float z; -}; +} __packed__; // 6x57: Unknown (supported; lobby & game) struct G_Unknown_6x57 { G_ClientIDHeader header; -}; +} __packed__; // 6x58: Unknown (supported; game only) @@ -3560,7 +3559,7 @@ struct G_Unknown_6x58 { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unused; -}; +} __packed__; // 6x59: Pick up item @@ -3569,7 +3568,7 @@ struct G_PickUpItem_6x59 { le_uint16_t client_id2; le_uint16_t area; le_uint32_t item_id; -}; +} __packed__; // 6x5A: Request to pick up item @@ -3578,7 +3577,7 @@ struct G_PickUpItemRequest_6x5A { le_uint32_t item_id; le_uint16_t area; le_uint16_t unused; -}; +} __packed__; // 6x5B: Invalid subcommand @@ -3588,7 +3587,7 @@ struct G_Unknown_6x5C { G_UnusedHeader header; le_uint32_t unknown_a1; le_uint32_t unknown_a2; -}; +} __packed__; // 6x5D: Drop meseta or stacked item @@ -3599,19 +3598,19 @@ struct G_DropStackedItem_DC_6x5D { le_float x; le_float z; ItemData data; -}; +} __packed__; struct G_DropStackedItem_PC_V3_BB_6x5D { G_DropStackedItem_DC_6x5D basic_cmd; le_uint32_t unused3; -}; +} __packed__; // 6x5E: Buy item at shop struct G_BuyShopItem_6x5E { G_ClientIDHeader header; ItemData item; -}; +} __packed__; // 6x5F: Drop item from box/enemy @@ -3625,12 +3624,12 @@ struct G_DropItem_DC_6x5F { le_uint16_t unknown_a1; le_uint16_t unknown_a2; ItemData data; -}; +} __packed__; struct G_DropItem_PC_V3_BB_6x5F { G_DropItem_DC_6x5F basic_cmd; le_uint32_t unused3; -}; +} __packed__; // 6x60: Request for item drop (handled by the server on BB) @@ -3643,12 +3642,12 @@ struct G_EnemyDropItemRequest_DC_6x60 { le_float z; le_uint16_t unknown_a1; le_uint16_t unknown_a2; -}; +} __packed__; struct G_EnemyDropItemRequest_PC_V3_BB_6x60 { G_EnemyDropItemRequest_DC_6x60 basic_cmd; le_uint32_t unknown_a2; -}; +} __packed__; // 6x61: Feed MAG @@ -3656,7 +3655,7 @@ struct G_FeedMAG_6x61 { G_UnusedHeader header; le_uint32_t mag_item_id; le_uint32_t fed_item_id; -}; +} __packed__; // 6x62: Unknown // This subcommand is completely ignored (at least, by PSO GC). @@ -3667,7 +3666,7 @@ struct G_DestroyGroundItem_6x63 { G_UnusedHeader header; le_uint32_t item_id; le_uint32_t area; -}; +} __packed__; // 6x64: Unknown (not valid on Episode 3) // This subcommand is completely ignored (at least, by PSO GC). @@ -3680,7 +3679,7 @@ struct G_DestroyGroundItem_6x63 { struct G_UseStarAtomizer_6x66 { G_UnusedHeader header; parray target_client_ids; -}; +} __packed__; // 6x67: Create enemy set @@ -3691,7 +3690,7 @@ struct G_CreateEnemySet_6x67 { le_uint32_t unused1; le_uint32_t unknown_a1; le_uint32_t unused2; -}; +} __packed__; // 6x68: Telepipe/Ryuker @@ -3705,7 +3704,7 @@ struct G_CreateTelepipe_6x68 { le_float y; le_float z; le_uint32_t unknown_a4; -}; +} __packed__; // 6x69: Unknown (supported; game only) @@ -3715,7 +3714,7 @@ struct G_Unknown_6x69 { le_uint16_t unknown_a1; le_uint16_t what; // 0-3; logic is very different for each value le_uint16_t unknown_a2; -}; +} __packed__; // 6x6A: Unknown (supported; game only; not valid on Episode 3) @@ -3723,7 +3722,7 @@ struct G_Unknown_6x6A { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unused; -}; +} __packed__; // 6x6B: Sync enemy state (used while loading into game; same header format as 6E) // 6x6C: Sync object state (used while loading into game; same header format as 6E) @@ -3736,14 +3735,14 @@ struct G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E { le_uint32_t decompressed_size; le_uint32_t compressed_size; // Must be <= subcommand_size - 0x10 // BC0-compressed data follows here (use bc0_decompress from Compression.hh) -}; +} __packed__; // 6x6F: Unknown (used while loading into game) struct G_Unknown_6x6F { G_UnusedHeader header; parray unknown_a1; -}; +} __packed__; // 6x70: Sync player disp data and inventory (used while loading into game) // Annoyingly, they didn't use the same format as the 65/67/68 commands here, @@ -3767,7 +3766,7 @@ struct G_Unknown_6x70 { /* 0084 */ struct { parray unknown_a1; parray unknown_a2; - } unknown_a7; + } __packed__ unknown_a7; /* 00A0 */ le_uint32_t unknown_a8; /* 00A4 */ parray unknown_a9; /* 00B8 */ le_uint32_t unknown_a10; @@ -3795,40 +3794,40 @@ struct G_Unknown_6x70 { le_uint16_t hair_b; le_uint32_t proportion_x; le_uint32_t proportion_y; - } disp_part2; + } __packed__ disp_part2; /* 0124 */ struct { PlayerStats stats; parray unknown_a1; le_uint32_t level; le_uint32_t experience; le_uint32_t meseta; - } disp_part1; + } __packed__ disp_part1; /* 0148 */ struct { le_uint32_t num_items; // Entries >= num_items in this array contain uninitialized data (usually // the contents of a previous sync command) parray items; - } inventory; + } __packed__ inventory; /* 0494 */ le_uint32_t unknown_a15; -}; +} __packed__; // 6x71: Unknown (used while loading into game) struct G_Unknown_6x71 { G_UnusedHeader header; -}; +} __packed__; // 6x72: Unknown (used while loading into game) struct G_Unknown_6x72 { G_UnusedHeader header; -}; +} __packed__; // 6x73: Unknown struct G_Unknown_6x73 { G_UnusedHeader header; -}; +} __packed__; // 6x74: Word select @@ -3839,7 +3838,7 @@ struct G_WordSelect_6x74 { parray entries; le_uint32_t unknown_a3; le_uint32_t unknown_a4; -}; +} __packed__; // 6x75: Phase setup (supported; game only) @@ -3848,13 +3847,13 @@ struct G_PhaseSetup_DC_PC_6x75 { G_UnusedHeader header; le_uint16_t phase; // Must be < 0x400 le_uint16_t unknown_a1; // Must be 0 or 1 -}; +} __packed__; struct G_PhaseSetup_V3_BB_6x75 { G_PhaseSetup_DC_PC_6x75 basic_cmd; le_uint16_t difficulty; le_uint16_t unused; -}; +} __packed__; // 6x76: Enemy killed @@ -3862,7 +3861,7 @@ struct G_EnemyKilled_6x76 { G_EnemyIDHeader header; le_uint16_t unknown_a1; le_uint16_t unknown_a2; // Flags of some sort -}; +} __packed__; // 6x77: Sync quest data @@ -3871,7 +3870,7 @@ struct G_SyncQuestData_6x77 { le_uint16_t register_number; // Must be < 0x100 le_uint16_t unused; le_uint32_t value; -}; +} __packed__; // 6x78: Unknown @@ -3880,7 +3879,7 @@ struct G_Unknown_6x78 { le_uint16_t client_id; // Must be < 12 le_uint16_t unused1; le_uint32_t unused2; -}; +} __packed__; // 6x79: Lobby 14/15 gogo ball (soccer game) @@ -3892,19 +3891,19 @@ struct G_GogoBall_6x79 { le_float unknown_a4; uint8_t unknown_a5; parray unused; -}; +} __packed__; // 6x7A: Unknown struct G_Unknown_6x7A { G_ClientIDHeader header; -}; +} __packed__; // 6x7B: Unknown struct G_Unknown_6x7B { G_ClientIDHeader header; -}; +} __packed__; // 6x7C: Unknown (supported; game only; not valid on Episode 3) @@ -3927,9 +3926,9 @@ struct G_Unknown_6x7C { struct Entry { le_uint32_t unknown_a1; le_uint32_t unknown_a2; - }; + } __packed__; Entry entries[3]; -}; +} __packed__; // 6x7D: Unknown (supported; game only; not valid on Episode 3) @@ -3938,7 +3937,7 @@ struct G_Unknown_6x7D { uint8_t unknown_a1; // Must be < 7; used in jump table parray unused; parray unknown_a2; -}; +} __packed__; // 6x7E: Unknown (not valid on Episode 3) // This subcommand is completely ignored (at least, by PSO GC). @@ -3948,7 +3947,7 @@ struct G_Unknown_6x7D { struct G_Unknown_6x7F { G_UnusedHeader header; parray unknown_a1; -}; +} __packed__; // 6x80: Trigger trap (not valid on Episode 3) @@ -3956,19 +3955,19 @@ struct G_TriggerTrap_6x80 { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unknown_a2; -}; +} __packed__; // 6x81: Unknown struct G_Unknown_6x81 { G_ClientIDHeader header; -}; +} __packed__; // 6x82: Unknown struct G_Unknown_6x82 { G_ClientIDHeader header; -}; +} __packed__; // 6x83: Place trap @@ -3976,7 +3975,7 @@ struct G_PlaceTrap_6x83 { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unknown_a2; -}; +} __packed__; // 6x84: Unknown (supported; game only; not valid on Episode 3) @@ -3986,7 +3985,7 @@ struct G_Unknown_6x84 { le_uint16_t unknown_a2; le_uint16_t unknown_a3; le_uint16_t unused; -}; +} __packed__; // 6x85: Unknown (supported; game only; not valid on Episode 3) @@ -3994,7 +3993,7 @@ struct G_Unknown_6x85 { G_UnusedHeader header; le_uint16_t unknown_a1; // Command is ignored unless this is 0 parray unknown_a2; // Only the first 3 appear to be used -}; +} __packed__; // 6x86: Hit destructible object (not valid on Episode 3) @@ -4004,20 +4003,20 @@ struct G_HitDestructibleObject_6x86 { le_uint32_t unknown_a2; le_uint16_t unknown_a3; le_uint16_t unknown_a4; -}; +} __packed__; // 6x87: Unknown struct G_Unknown_6x87 { G_ClientIDHeader header; le_float unknown_a1; -}; +} __packed__; // 6x88: Unknown (supported; game only) struct G_Unknown_6x88 { G_ClientIDHeader header; -}; +} __packed__; // 6x89: Unknown (supported; game only) @@ -4025,14 +4024,14 @@ struct G_Unknown_6x89 { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unused; -}; +} __packed__; // 6x8A: Unknown (not valid on Episode 3) struct G_Unknown_6x8A { G_ClientIDHeader header; le_uint32_t unknown_a1; // Must be < 0x11 -}; +} __packed__; // 6x8B: Unknown (not valid on Episode 3) // This subcommand is completely ignored (at least, by PSO GC). @@ -4047,7 +4046,7 @@ struct G_SetTechniqueLevelOverride_6x8D { uint8_t level_upgrade; uint8_t unused1; le_uint16_t unused2; -}; +} __packed__; // 6x8E: Unknown (not valid on Episode 3) // This subcommand is completely ignored (at least, by PSO GC). @@ -4058,14 +4057,14 @@ struct G_Unknown_6x8F { G_ClientIDHeader header; le_uint16_t client_id2; le_uint16_t unknown_a1; -}; +} __packed__; // 6x90: Unknown (not valid on Episode 3) struct G_Unknown_6x90 { G_ClientIDHeader header; le_uint32_t unknown_a1; -}; +} __packed__; // 6x91: Unknown (supported; game only) @@ -4077,7 +4076,7 @@ struct G_Unknown_6x91 { le_uint16_t unknown_a4; le_uint16_t unknown_a5; parray unknown_a6; -}; +} __packed__; // 6x92: Unknown (not valid on Episode 3) @@ -4085,7 +4084,7 @@ struct G_Unknown_6x92 { G_UnusedHeader header; le_uint32_t unknown_a1; le_float unknown_a2; -}; +} __packed__; // 6x93: Timed switch activated (not valid on Episode 3) @@ -4095,7 +4094,7 @@ struct G_TimedSwitchActivated_6x93 { le_uint16_t switch_id; uint8_t unknown_a1; // Logic is different if this is 1 vs. any other value parray unused; -}; +} __packed__; // 6x94: Warp (not valid on Episode 3) @@ -4103,7 +4102,7 @@ struct G_InterLevelWarp_6x94 { G_UnusedHeader header; le_uint16_t area; parray unused; -}; +} __packed__; // 6x95: Unknown (not valid on Episode 3) @@ -4113,7 +4112,7 @@ struct G_Unknown_6x95 { le_uint32_t unknown_a1; le_uint32_t unknown_a2; le_uint32_t unknown_a3; -}; +} __packed__; // 6x96: Unknown (not valid on Episode 3) // This subcommand is completely ignored (at least, by PSO GC). @@ -4126,7 +4125,7 @@ struct G_Unknown_6x97 { le_uint32_t unknown_a1; // Must be 0 or 1 le_uint32_t unused2; le_uint32_t unused3; -}; +} __packed__; // 6x98: Unknown // This subcommand is completely ignored (at least, by PSO GC). @@ -4147,7 +4146,7 @@ struct G_UpdatePlayerStat_6x9A { // 4 = add TP uint8_t what; uint8_t amount; -}; +} __packed__; // 6x9B: Unknown @@ -4155,21 +4154,21 @@ struct G_Unknown_6x9B { G_UnusedHeader header; uint8_t unknown_a1; parray unused; -}; +} __packed__; // 6x9C: Unknown (supported; game only; not valid on Episode 3) struct G_Unknown_6x9C { G_EnemyIDHeader header; le_uint32_t unknown_a1; -}; +} __packed__; // 6x9D: Unknown (not valid on Episode 3) struct G_Unknown_6x9D { G_UnusedHeader header; le_uint32_t client_id2; -}; +} __packed__; // 6x9E: Unknown (not valid on Episode 3) // This subcommand is completely ignored (at least, by PSO GC). @@ -4181,7 +4180,7 @@ struct G_GalGryphonActions_6x9F { le_uint32_t unknown_a1; le_float unknown_a2; le_float unknown_a3; -}; +} __packed__; // 6xA0: Gal Gryphon actions (not valid on PC or Episode 3) @@ -4194,13 +4193,13 @@ struct G_GalGryphonActions_6xA0 { le_uint16_t unknown_a2; le_uint16_t unknown_a3; parray unknown_a4; -}; +} __packed__; // 6xA1: Unknown (not valid on PC) struct G_Unknown_6xA1 { G_ClientIDHeader header; -}; +} __packed__; // 6xA2: Request for item drop from box (not valid on PC; handled by server on BB) @@ -4218,7 +4217,7 @@ struct G_BoxItemDropRequest_6xA2 { le_uint32_t unknown_a6; le_uint32_t unknown_a7; le_uint32_t unknown_a8; -}; +} __packed__; // 6xA3: Episode 2 boss actions (not valid on PC or Episode 3) @@ -4227,7 +4226,7 @@ struct G_Episode2BossActions_6xA3 { uint8_t unknown_a1; uint8_t unknown_a2; parray unknown_a3; -}; +} __packed__; // 6xA4: Olga Flow phase 1 actions (not valid on PC or Episode 3) @@ -4235,7 +4234,7 @@ struct G_OlgaFlowPhase1Actions_6xA4 { G_EnemyIDHeader header; uint8_t what; parray unknown_a3; -}; +} __packed__; // 6xA5: Olga Flow phase 2 actions (not valid on PC or Episode 3) @@ -4243,7 +4242,7 @@ struct G_OlgaFlowPhase2Actions_6xA5 { G_EnemyIDHeader header; uint8_t what; parray unknown_a3; -}; +} __packed__; // 6xA6: Modify trade proposal (not valid on PC) @@ -4254,7 +4253,7 @@ struct G_ModifyTradeProposal_6xA6 { parray unknown_a3; le_uint32_t unknown_a4; le_uint32_t unknown_a5; -}; +} __packed__; // 6xA7: Unknown (not valid on PC) // This subcommand is completely ignored (at least, by PSO GC). @@ -4266,7 +4265,7 @@ struct G_GolDragonActions_6xA8 { le_uint16_t unknown_a1; le_uint16_t unknown_a2; le_uint32_t unknown_a3; -}; +} __packed__; // 6xA9: Barba Ray actions (not valid on PC or Episode 3) @@ -4274,7 +4273,7 @@ struct G_BarbaRayActions_6xA9 { G_EnemyIDHeader header; le_uint16_t unknown_a1; le_uint16_t unknown_a2; -}; +} __packed__; // 6xAA: Episode 2 boss actions (not valid on PC or Episode 3) @@ -4283,7 +4282,7 @@ struct G_Episode2BossActions_6xAA { le_uint16_t unknown_a1; le_uint16_t unknown_a2; le_uint32_t unknown_a3; -}; +} __packed__; // 6xAB: Create lobby chair (not valid on PC) @@ -4291,7 +4290,7 @@ struct G_CreateLobbyChair_6xAB { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unknown_a2; -}; +} __packed__; // 6xAC: Unknown (not valid on PC) @@ -4299,7 +4298,7 @@ struct G_Unknown_6xAC { G_ClientIDHeader header; le_uint32_t num_items; parray item_ids; -}; +} __packed__; // 6xAD: Unknown (not valid on PC, Episode 3, or GC Trial Edition) @@ -4307,7 +4306,7 @@ struct G_Unknown_6xAD { G_UnusedHeader header; // The first byte in this array seems to have a special meaning parray unknown_a1; -}; +} __packed__; // 6xAE: Set lobby chair state (sent by existing clients at join time) // This subcommand is not valid on DC, PC, or GC Trial Edition. @@ -4318,21 +4317,21 @@ struct G_SetLobbyChairState_6xAE { le_uint16_t unknown_a2; le_uint32_t unknown_a3; le_uint32_t unknown_a4; -}; +} __packed__; // 6xAF: Turn lobby chair (not valid on PC or GC Trial Edition) struct G_TurnLobbyChair_6xAF { G_ClientIDHeader header; le_uint32_t angle; // In range [0x0000, 0xFFFF] -}; +} __packed__; // 6xB0: Move lobby chair (not valid on PC or GC Trial Edition) struct G_MoveLobbyChair_6xB0 { G_ClientIDHeader header; le_uint32_t unknown_a1; -}; +} __packed__; // 6xB1: Unknown (not valid on PC or GC Trial Edition) // This subcommand is completely ignored (at least, by PSO GC). @@ -4344,7 +4343,7 @@ struct G_Unknown_6xB2 { parray unknown_a1; le_uint16_t unknown_a2; le_uint32_t unknown_a3; -}; +} __packed__; // 6xB3: Unknown (XBOX) @@ -4352,16 +4351,18 @@ struct G_Unknown_6xB2 { // These commands have multiple subcommands; see the Episode 3 subsubcommand // table after this table. The common format is: -struct G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - G_UnusedHeader basic_header; - uint8_t subsubcommand; // See 6xBx subcommand table (after this table) - uint8_t unknown_a1; - // If mask_key is nonzero, the remainder of the data (after header_b2 in this +struct G_CardBattleCommandHeader { + uint8_t subcommand = 0x00; + uint8_t size = 0x00; + le_uint16_t unused1 = 0x00; + uint8_t subsubcommand = 0x00; // See 6xBx subcommand table (after this table) + uint8_t sender_client_id = 0x00; + // If mask_key is nonzero, the remainder of the data (after unused2 in this // struct) is encrypted using a simple algorithm, which is implemented in // set_mask_for_ep3_game_command in SendCommands.cc. - uint8_t mask_key; - uint8_t unused; -}; + uint8_t mask_key = 0x00; + uint8_t unused2 = 0x00; +} __packed__; // 6xB4: Unknown (XBOX) // 6xB4: CARD battle command (Episode 3) - see 6xB3 (above) @@ -4371,7 +4372,7 @@ struct G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { struct G_ShopContentsRequest_BB_6xB5 { G_UnusedHeader header; le_uint32_t shop_type; -}; +} __packed__; // 6xB6: Episode 3 map list and map contents (server->client only) // Unlike 6xB3-6xB5, these commands cannot be masked. Also unlike 6xB3-6xB5, @@ -4383,14 +4384,14 @@ struct G_MapSubsubcommand_GC_Ep3_6xB6 { G_ExtendedHeader header; uint8_t subsubcommand; // 0x40 or 0x41 parray unused; -}; +} __packed__; struct G_MapList_GC_Ep3_6xB6x40 { G_MapSubsubcommand_GC_Ep3_6xB6 header; le_uint16_t compressed_data_size; le_uint16_t unused; // PRS-compressed map list follows (see Ep3DataIndex::get_compressed_map_list) -}; +} __packed__; struct G_MapData_GC_Ep3_6xB6x41 { G_MapSubsubcommand_GC_Ep3_6xB6 header; @@ -4398,7 +4399,7 @@ struct G_MapData_GC_Ep3_6xB6x41 { le_uint16_t compressed_data_size; le_uint16_t unused; // PRS-compressed map data follows (which decompresses to an Ep3Map) -}; +} __packed__; // 6xB6: BB shop contents (server->client only) @@ -4409,7 +4410,7 @@ struct G_ShopContents_BB_6xB6 { le_uint16_t unused; // Note: data2d of these entries should be the price ItemData entries[20]; -}; +} __packed__; // 6xB7: Unknown (Episode 3 Trial Edition) // 6xB7: BB buy shop item (handled by the server) @@ -4421,7 +4422,7 @@ struct G_BuyShopItem_BB_6xB7 { uint8_t item_index; uint8_t amount; uint8_t unknown_a1; // TODO: Probably actually unused; verify this -}; +} __packed__; // 6xB8: Unknown (Episode 3 Trial Edition) // 6xB8: BB accept tekker result (handled by the server) @@ -4429,7 +4430,7 @@ struct G_BuyShopItem_BB_6xB7 { struct G_AcceptItemIdentification_BB_6xB8 { G_UnusedHeader header; le_uint32_t item_id; -}; +} __packed__; // 6xB9: Unknown (Episode 3 Trial Edition) // 6xB9: BB provisional tekker result @@ -4437,7 +4438,7 @@ struct G_AcceptItemIdentification_BB_6xB8 { struct G_IdentifyResult_BB_6xB9 { G_ClientIDHeader header; ItemData item; -}; +} __packed__; // 6xBA: Unknown (Episode 3) @@ -4447,14 +4448,14 @@ struct G_Unknown_GC_Ep3_6xBA { le_uint16_t unknown_a2; le_uint32_t unknown_a3; le_uint32_t unknown_a4; -}; +} __packed__; // 6xBA: BB accept tekker result (handled by the server) struct G_AcceptItemIdentification_BB_6xBA { G_UnusedHeader header; le_uint32_t item_id; -}; +} __packed__; // 6xBB: Unknown (Episode 3) @@ -4463,7 +4464,7 @@ struct G_Unknown_GC_Ep3_6xBB { le_uint16_t unknown_a1; // Low byte must be < 5 le_uint16_t unknown_a2; parray unknown_a3; -}; +} __packed__; // 6xBB: BB bank request (handled by the server) @@ -4475,7 +4476,7 @@ struct G_Unknown_GC_Ep3_6xBC { // The length of this array strongly implies one flag or value per card type. parray unknown_a1; parray unused2; -}; +} __packed__; // 6xBC: BB bank contents (server->client only) @@ -4485,20 +4486,20 @@ struct G_BankContentsHeader_BB_6xBC { le_uint32_t numItems; le_uint32_t meseta; // Item data follows -}; +} __packed__; -// 6xBD: Unknown (Episode 3; not Trial Edition) +// 6xBD: Word select during battle (Episode 3; not Trial Edition) // Note: This structure does not have a normal header - the client ID field is // big-endian! -struct G_Unknown_GC_Ep3_6xBD { +struct G_WordSelectDuringBattle_GC_Ep3_6xBD { G_ClientIDHeader header; le_uint16_t unknown_a1; le_uint16_t unknown_a2; - parray unknown_a3; + parray entries; le_uint32_t unknown_a4; le_uint32_t unknown_a5; -}; +} __packed__; // 6xBD: BB bank action (take/deposit meseta/item) (handled by the server) @@ -4509,7 +4510,7 @@ struct G_BankAction_BB_6xBD { uint8_t action; // 0 = deposit, 1 = take uint8_t item_amount; le_uint16_t unused2; -}; +} __packed__; // 6xBE: Sound chat (Episode 3; not Trial Edition) @@ -4517,7 +4518,7 @@ struct G_SoundChat_GC_Ep3_6xBE { G_UnusedHeader header; le_uint32_t sound_id; // Must be < 0x27 be_uint32_t unknown_a1; -}; +} __packed__; // 6xBE: BB create inventory item (server->client only) @@ -4525,21 +4526,21 @@ struct G_CreateInventoryItem_BB_6xBE { G_ClientIDHeader header; ItemData item; le_uint32_t unused; -}; +} __packed__; // 6xBF: Change lobby music (Episode 3; not Trial Edition) struct G_ChangeLobbyMusic_GC_Ep3_6xBF { G_UnusedHeader header; le_uint32_t song_number; // Must be < 0x34 -}; +} __packed__; // 6xBF: Give EXP (BB) (server->client only) struct G_GiveExperience_BB_6xBF { G_ClientIDHeader header; le_uint32_t amount; -}; +} __packed__; // 6xC0: BB sell item at shop @@ -4547,7 +4548,7 @@ struct G_SellItemAtShop_BB_6xC0 { G_UnusedHeader header; le_uint32_t item_id; le_uint32_t amount; -}; +} __packed__; // 6xC1: Unknown // 6xC2: Unknown @@ -4564,14 +4565,14 @@ struct G_SplitStackedItem_6xC3 { le_float z; le_uint32_t item_id; le_uint32_t amount; -}; +} __packed__; // 6xC4: Sort inventory (handled by the server on BB) struct G_SortInventory_6xC4 { G_UnusedHeader header; le_uint32_t item_ids[30]; -}; +} __packed__; // 6xC5: Medical center used // 6xC6: Invalid subcommand @@ -4584,7 +4585,7 @@ struct G_EnemyKilled_6xC8 { le_uint16_t enemy_id2; le_uint16_t killer_client_id; le_uint32_t unused; -}; +} __packed__; // 6xC9: Invalid subcommand // 6xCA: Invalid subcommand @@ -4651,952 +4652,834 @@ struct G_EnemyKilled_6xC8 { // subcommand. Unlike the above listings, invalid commands are not listed here, // since this table is known to be complete. -struct DeckCardRef { // Note: The game treats these as le_uint16_ts - uint8_t deck_index; - uint8_t client_id; -}; - // 6xB4x02: Update hands and equips struct G_UpdateHand_GC_Ep3_6xB4x02 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - le_uint16_t client_id; - le_uint16_t unused3; - parray unknown_a1; - uint8_t atk_die; - uint8_t def_die; - uint8_t atk_die2; - parray unknown_a2; - uint8_t client_id2; - le_uint32_t unknown_a3; - // Empty slots in all of these arrays should be set to FFFF - parray cards_in_hand; - le_uint16_t unknown_a4; - parray cards_equipped; - // Note: The order of entries in this field matches the order of entries in - // 6xB4x04's entries list (and the card refs should match exactly). The first - // entry here is always the SC card (ref with deck_index=0). - parray unknown_a5; - parray unknown_a6; // {0, 0xFFFF} always? - parray unknown_a7; // {0, 0, 0, 0, 0x08, 0x0D} always? -}; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateHand_GC_Ep3_6xB4x02) / 4, 0, 0x02, 0, 0, 0}; + le_uint16_t client_id = 0; + le_uint16_t unused = 0; + Episode3::HandAndEquipState state; +} __packed__; // 6xB4x03: Set state flags struct G_SetStateFlags_GC_Ep3_6xB4x03 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - le_uint16_t battle_round; - uint8_t battle_phase; - uint8_t unknown_a2; - uint8_t unknown_a3; - uint8_t unknown_a4; - uint8_t setup_phase; - uint8_t unknown_a5; - parray team_exp; - parray team_dice_boost; - uint8_t unknown_a6; - // If tournament_flag is 1, the player will start at the counter instead of in - // the default position the next time they join a game, and they will be - // unable to leave the counter menu. - uint8_t tournament_flag; - be_uint32_t unknown_a7; -}; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetStateFlags_GC_Ep3_6xB4x03) / 4, 0, 0x03, 0, 0, 0}; + Episode3::StateFlags state; +} __packed__; -// 6xB4x04: Update SC/FC stats +// 6xB4x04: Update SC/FC short statuses struct G_UpdateStats_GC_Ep3_6xB4x04 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - le_uint16_t client_id; - le_uint16_t unused; - struct Entry { - DeckCardRef card; // FFFF if this entry is unused (and all other fields are 0) - le_uint16_t hp; - le_uint32_t unknown_a3; - uint8_t card_hp; - uint8_t card_tp; - uint8_t card_ap; - uint8_t card_mv; - le_uint16_t unknown_a5; - le_uint16_t hp2; // Seems unused by the client, but Sega duplicated HP here - }; - Entry entries[0x10]; -}; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateStats_GC_Ep3_6xB4x04) / 4, 0, 0x04, 0, 0, 0}; + le_uint16_t client_id = 0; + le_uint16_t unused = 0; + // The slots in this array have heterogeneous meanings. Specifically: + // [0] is the SC card status + // [1] through [6] are hand cards + // [7] through [14] are set cards + // [15] is the assist card + parray card_statuses; +} __packed__; // 6xB4x05: Update map state struct G_UpdateMap_GC_Ep3_6xB4x05 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - le_uint16_t width; - le_uint16_t height; - parray tiles; - parray unknown_a1; - uint8_t num_players; - uint8_t unknown_a2; - uint8_t unknown_a3; // Handler logic branches on this - parray unknown_a4; - le_uint16_t unknown_a5; - // 120 from subcommand start - parray unknown_a6; - le_uint32_t map_number; - le_uint32_t unknown_a7; // Handler logic branches on this - Ep3BattleRules rules; - parray unknown_a8; - uint8_t unknown_a9; // Handler logic branches on this - parray unknown_a10; -}; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateMap_GC_Ep3_6xB4x05) / 4, 0, 0x05, 0, 0, 0}; + Episode3::MapAndRulesState state; + uint8_t unknown_a1 = 0; + parray unused; +} __packed__; -// 6xB4x06: Unknown +// 6xB4x06: Apply condition effect -struct G_Unknown_GC_Ep3_6xB4x06 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - le_uint16_t unknown_a1; - le_uint16_t unknown_a2; - parray unknown_a3; -}; +struct G_ApplyConditionEffect_GC_Ep3_6xB4x06 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_ApplyConditionEffect_GC_Ep3_6xB4x06) / 4, 0, 0x06, 0, 0, 0}; + Episode3::EffectResult effect; +} __packed__; // 6xB4x07: Set battle decks struct G_UpdateDecks_GC_Ep3_6xB4x07 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - parray unknown_a1; - struct Entry { - ptext player_name; - uint8_t present; - parray unknown_a1; - parray card_ids; - parray unknown_a2; - le_uint16_t unknown_a3; - be_uint16_t unknown_a4; - }; - Entry entries[4]; -}; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateDecks_GC_Ep3_6xB4x07) / 4, 0, 0x07, 0, 0, 0}; + parray entries_present; + parray entries; +} __packed__; -// 6xB4x09: Unknown +// 6xB4x09: Set action state -struct G_Unknown_GC_Ep3_6xB4x09 { - le_uint16_t unknown_a1; // Probably client_id; client ignores command if < 0 - parray unknown_a2; - le_uint16_t unknown_a3; // Probably client_id - be_uint16_t unknown_a4; - le_uint16_t unknown_a5; - le_uint16_t unknown_a6; - parray unknown_a7; - parray unknown_a8; - be_uint16_t unknown_a9; - le_uint16_t unknown_a10; -}; +struct G_SetActionState_GC_Ep3_6xB4x09 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetActionState_GC_Ep3_6xB4x09) / 4, 0, 0x09, 0, 0, 0}; + le_uint16_t client_id = 0; + parray unknown_a1; + Episode3::ActionState state; +} __packed__; // 6xB4x0A: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB4x0A { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - le_uint16_t client_id; - int8_t unknown_a1; // must be between -1 and 8 inclusive - uint8_t unknown_a2; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x0A) / 4, 0, 0x0A, 0, 0, 0}; + le_uint16_t client_id = 0; + int8_t unknown_a1 = 0; // must be between -1 and 8 inclusive + uint8_t unknown_a2 = 0; parray unknown_a3; - le_uint16_t unknown_a4; - le_uint16_t unknown_a5; + le_uint16_t unknown_a4 = 0; + le_uint16_t unknown_a5 = 0; parray unknown_a6; parray unknown_a7; - le_uint32_t unknown_a8; + le_uint32_t unknown_a8 = 0; parray unknown_a9; struct Entry { parray unknown_a1; parray unknown_a2; parray unknown_a3; - }; - Entry entries[9]; + } __packed__; + parray entries; - le_uint16_t unknown_a10; + le_uint16_t unknown_a10 = 0; parray unknown_a11; - le_uint32_t unknown_a12; + le_uint32_t unknown_a12 = 0; parray unknown_a13; parray unknown_a14; parray unknown_a15; -}; +} __packed__; -// 6xB3x0B / CAx0B: Unknown +// 6xB3x0B / CAx0B: Redraw initial hand (immediately before battle) -struct G_Unknown_GC_Ep3_6xB3x0B_CAx0B { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint16_t client_id; - parray unknown_a2; -}; +struct G_RedrawInitialHand_GC_Ep3_6xB3x0B_CAx0B { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_RedrawInitialHand_GC_Ep3_6xB3x0B_CAx0B) / 4, 0, 0x0B, 0, 0, 0}; + be_uint32_t sequence_num = 0; + parray unused1; + le_uint16_t client_id = 0; + parray unused2; +} __packed__; -// 6xB3x0C / CAx0C: Unknown +// 6xB3x0C / CAx0C: End initial redraw phase -struct G_Unknown_GC_Ep3_6xB3x0C_CAx0C { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint16_t client_id; - parray unknown_a2; -}; +struct G_EndInitialRedrawPhase_GC_Ep3_6xB3x0C_CAx0C { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_EndInitialRedrawPhase_GC_Ep3_6xB3x0C_CAx0C) / 4, 0, 0x0C, 0, 0, 0}; + be_uint32_t sequence_num = 0; + parray unused1; + le_uint16_t client_id = 0; + parray unused2; +} __packed__; -// 6xB3x0D / CAx0D: Unknown +// 6xB3x0D / CAx0D: End non-attack phase -struct G_Unknown_GC_Ep3_6xB3x0D_CAx0D { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint16_t client_id; - parray unknown_a2; -}; +struct G_EndNonAttackPhase_GC_Ep3_6xB3x0D_CAx0D { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_EndNonAttackPhase_GC_Ep3_6xB3x0D_CAx0D) / 4, 0, 0x0D, 0, 0, 0}; + be_uint32_t sequence_num = 0; + parray unused1; + le_uint16_t client_id = 0; + parray unused2; +} __packed__; -// 6xB3x0E / CAx0E: Unknown +// 6xB3x0E / CAx0E: Discard card from hand -struct G_Unknown_GC_Ep3_6xB3x0E_CAx0E { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint16_t client_id; - le_uint16_t unknown_a2; -}; +struct G_DiscardCardFromHand_GC_Ep3_6xB3x0E_CAx0E { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_DiscardCardFromHand_GC_Ep3_6xB3x0E_CAx0E) / 4, 0, 0x0E, 0, 0, 0}; + be_uint32_t sequence_num = 0; + parray unused; + le_uint16_t client_id = 0; + le_uint16_t card_ref = 0xFFFF; +} __packed__; -// 6xB3x0F / CAx0F: Unknown +// 6xB3x0F / CAx0F: Set card from hand -struct G_Unknown_GC_Ep3_6xB3x0F_CAx0F { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint16_t client_id; - parray unknown_a2; - parray unknown_a3; - parray unused; -}; +struct G_SetCardFromHand_GC_Ep3_6xB3x0F_CAx0F { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_SetCardFromHand_GC_Ep3_6xB3x0F_CAx0F) / 4, 0, 0x0F, 0, 0, 0}; + be_uint32_t sequence_num = 0; + parray unused; + le_uint16_t client_id = 0; + le_uint16_t card_ref = 0xFFFF; + le_uint16_t set_index = 0; + le_uint16_t assist_target_player = 0; + Episode3::Location loc; +} __packed__; -// 6xB3x10 / CAx10: Unknown +// 6xB3x10 / CAx10: Move field character -struct G_Unknown_GC_Ep3_6xB3x10_CAx10 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint16_t client_id; - le_uint16_t unknown_a2; - // Note: This field's type is the same as the corresponding field in 6xB3x0F - parray unknown_a3; - parray unused; -}; +struct G_MoveFieldCharacter_GC_Ep3_6xB3x10_CAx10 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_MoveFieldCharacter_GC_Ep3_6xB3x10_CAx10) / 4, 0, 0x10, 0, 0, 0}; + be_uint32_t sequence_num = 0; + parray unused; + le_uint16_t client_id = 0; + le_uint16_t set_index = 0; + Episode3::Location loc; +} __packed__; -// 6xB3x11 / CAx11: Unknown +// 6xB3x11 / CAx11: Enqueue attack or defense -struct G_Unknown_GC_Ep3_6xB3x11_CAx11 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint16_t client_id; - parray unknown_a2; +struct G_EnqueueAttackOrDefense_GC_Ep3_6xB3x11_CAx11 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_EnqueueAttackOrDefense_GC_Ep3_6xB3x11_CAx11) / 4, 0, 0x11, 0, 0, 0}; + be_uint32_t sequence_num = 0; + parray unused1; + le_uint16_t client_id = 0; + parray unused2; + Episode3::ActionState entry; +} __packed__; - // Note: This is byteswapped by the same function as the corresponding part of - // 6xB4x29 - le_uint16_t unknown_a3; - parray unknown_a4; - le_uint16_t unknown_a5; - le_uint16_t unknown_a6; - parray unknown_a7; - parray unknown_a8; - parray unknown_a9; - le_uint16_t unknown_a10; -}; +// 6xB3x12 / CAx12: End attack list -// 6xB3x12 / CAx12: Unknown +struct G_EndAttackList_GC_Ep3_6xB3x12_CAx12 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_EndAttackList_GC_Ep3_6xB3x12_CAx12) / 4, 0, 0x12, 0, 0, 0}; + be_uint32_t sequence_num = 0; + parray unused1; + le_uint16_t client_id = 0; + parray unused2; +} __packed__; -struct G_Unknown_GC_Ep3_6xB3x12_CAx12 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint16_t client_id; - parray unknown_a3; -}; +// 6xB3x13 / CAx13: Set map state during setup -// 6xB3x13 / CAx13: Update game state +struct G_SetMapState_GC_Ep3_6xB3x13_CAx13 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_SetMapState_GC_Ep3_6xB3x13_CAx13) / 4, 0, 0x13, 0, 0, 0}; + parray unused; + Episode3::MapAndRulesState map_and_rules_state; + Episode3::OverlayState overlay_state; +} __packed__; -struct G_UpdateGameState_GC_Ep3_6xB3x13_CAx13 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; +// 6xB3x14 / CAx14: Set player deck during setup - // Note: This section's format is a guess based on the fact that the client - // calls the same function to byteswap it as for the update map command above. - le_uint16_t width; - le_uint16_t height; - parray tiles; - parray unknown_a2; - uint8_t num_players; - uint8_t unknown_a3; - uint8_t unknown_a4; - parray unknown_a5; - le_uint16_t unknown_a6; - parray unknown_a7; - le_uint32_t map_number; - le_uint32_t unknown_a8; - Ep3BattleRules rules; - parray unknown_a9; +struct G_SetPlayerDeck_GC_Ep3_6xB3x14_CAx14 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_SetPlayerDeck_GC_Ep3_6xB3x14_CAx14) / 4, 0, 0x14, 0, 0, 0}; + parray unused1; + le_uint16_t client_id = 0; + uint8_t is_cpu_player = 0; + uint8_t unused2 = 0; + Episode3::DeckEntry entry; +} __packed__; - parray overlay_tiles; - parray unknown_a10; - parray unknown_a11; - parray unknown_a12; -}; +// 6xB3x15 / CAx15: Hard-reset server state (unused) -// 6xB3x14 / CAx14: Update field state - -struct G_UpdateFieldState_GC_Ep3_6xB3x14_CAx14 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint16_t client_id; - parray unknown_a2; - - parray name; - le_uint32_t unknown_a3; - parray card_ids; - parray unknown_a4; - le_uint16_t unknown_a5; - parray unknown_a6; -}; - -// 6xB3x15: Unknown - -struct G_Unknown_GC_Ep3_6xB3x15 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; +struct G_HardResetServerState_GC_Ep3_6xB3x15 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_HardResetServerState_GC_Ep3_6xB3x15) / 4, 0, 0x15, 0, 0, 0}; // No arguments -}; +} __packed__; // 6xB5x17: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x17 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x17) / 4, 0, 0x17, 0, 0, 0}; // No arguments -}; +} __packed__; // 6xB5x1A: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x1A { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x1A) / 4, 0, 0x1A, 0, 0, 0}; // No arguments -}; +} __packed__; -// 6xB3x1B / CAx1B: Update names +// 6xB3x1B / CAx1B: Set player name during setup +// Curiously, this command can be used during a non-setup phase; the server +// should ignore the command's contents but still send a 6xB4x1C in response. -struct G_UpdateNames_GC_Ep3_6xB3x1B_CAx1B { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - parray unknown_a2; -}; +struct G_SetPlayerName_GC_Ep3_6xB3x1B_CAx1B { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_SetPlayerName_GC_Ep3_6xB3x1B_CAx1B) / 4, 0, 0x1B, 0, 0, 0}; + parray unused; + Episode3::NameEntry entry; +} __packed__; // 6xB4x1C: Set player names -struct G_Unknown_GC_Ep3_6xB4x1C { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - struct Entry { - ptext name; - uint8_t client_id; // 0xFF if slot is empty - uint8_t present; // 1 if slot is occupied; 0 if empty - parray unused; - }; - Entry entries[4]; -}; +struct G_SetPlayerNames_GC_Ep3_6xB4x1C { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetPlayerNames_GC_Ep3_6xB4x1C) / 4, 0, 0x1C, 0, 0, 0}; + parray entries; +} __packed__; -// 6xB3x1D / CAx1D: Unknown +// 6xB3x1D / CAx1D: Start battle -struct G_Unknown_GC_Ep3_6xB3x1D_CAx1D { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; -}; +struct G_StartBattle_GC_Ep3_6xB3x1D_CAx1D { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_StartBattle_GC_Ep3_6xB3x1D_CAx1D) / 4, 0, 0x1D, 0, 0, 0}; + parray unused; +} __packed__; -// 6xB4x1E: Unknown +// 6xB4x1E: Action result -struct G_Unknown_GC_Ep3_6xB4x1E { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t unknown_a1; - uint8_t unknown_a2; - uint8_t unknown_a3; +struct G_ActionResult_GC_Ep3_6xB4x1E { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_ActionResult_GC_Ep3_6xB4x1E) / 4, 0, 0x1E, 0, 0, 0}; + // TODO: Is this supposed to be big-endian or little-endian? The client makes + // it look like it should be little-endian, but logs from the Sega servers + // make it look like it should be big-endian. + be_uint32_t sequence_num = 0; + uint8_t error_code = 0; + uint8_t response_phase = 0; parray unused; -}; +} __packed__; // 6xB4x1F: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB4x1F { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - le_uint32_t unknown_a1; -}; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x1F) / 4, 0, 0x1F, 0, 0, 0}; + le_uint32_t unknown_a1 = 0; +} __packed__; // 6xB5x20: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x20 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - le_uint32_t player_tag; - le_uint32_t guild_card_number; - uint8_t client_id; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x20) / 4, 0, 0x20, 0, 0, 0}; + le_uint32_t player_tag = 0x00010000; + le_uint32_t guild_card_number = 0; + uint8_t client_id = 0; parray unused; -}; +} __packed__; // 6xB3x21 / CAx21: End battle -// Server should respond by sending a 6xB4x03 (update state flags), followed by -// a 6xB4x46 (start/end battle). -struct G_Unknown_GC_Ep3_6xB3x21_CAx21 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint32_t unknown_a2; -}; +struct G_EndBattle_GC_Ep3_6xB3x21_CAx21 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_EndBattle_GC_Ep3_6xB3x21_CAx21) / 4, 0, 0x21, 0, 0, 0}; + parray unused1; + le_uint32_t unused2 = 0; +} __packed__; // 6xB4x22: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB4x22 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x22) / 4, 0, 0x22, 0, 0, 0}; // No arguments -}; +} __packed__; // 6xB4x23: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB4x23 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x23) / 4, 0, 0x23, 0, 0, 0}; // This command does nothing at all. If unknown_a1 == 1, then it calls another // function, but that function also does nothing. - uint8_t unknown_a1; - uint8_t unknown_a2; + uint8_t unknown_a1 = 0; + uint8_t unknown_a2 = 0; parray unused; -}; +} __packed__; // 6xB5x27: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x27 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x27) / 4, 0, 0x27, 0, 0, 0}; // Note: This command uses header_b1 as well, which looks like another client // ID (it must be < 4, though it does not always match unknown_a1 below). - le_uint32_t unknown_a1; // Probably client ID (must be < 4) - le_uint32_t unknown_a2; // Must be < 0x10 - le_uint32_t unknown_a3; - le_uint32_t unused; // Curiously, this usually contains a memory address -}; + le_uint32_t unknown_a1 = 0; // Probably client ID (must be < 4) + le_uint32_t unknown_a2 = 0; // Must be < 0x10 + le_uint32_t unknown_a3 = 0; + le_uint32_t unused = 0; // Curiously, this usually contains a memory address +} __packed__; -// 6xB3x28 / CAx28: Unknown +// 6xB3x28 / CAx28: End defense list -struct G_Unknown_GC_Ep3_6xB3x28_CAx28 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - uint8_t unused1; - int8_t unknown_a2; +struct G_EndDefenseList_GC_Ep3_6xB3x28_CAx28 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_EndDefenseList_GC_Ep3_6xB3x28_CAx28) / 4, 0, 0x28, 0, 0, 0}; + be_uint32_t sequence_num = 0; + parray unused1; + uint8_t client_id = 0; parray unused2; -}; +} __packed__; -// 6xB4x29: Unknown +// 6xB4x29: Set action state +// TODO: How is this different from 6xB4x09? Looks like the server never sends +// this (at least, from what I've disassembled so far.) -struct G_Unknown_GC_Ep3_6xB4x29 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - parray unknown_a1; - - // Note: This is byteswapped by the same function as the corresponding part of - // 6xB3x11 - le_uint16_t unknown_a2; - parray unknown_a3; - le_uint16_t unknown_a4; - le_uint16_t unknown_a5; - parray unknown_a6; - parray unknown_a7; - parray unknown_a8; - le_uint16_t unknown_a9; -}; +struct G_SetActionState_GC_Ep3_6xB4x29 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetActionState_GC_Ep3_6xB4x29) / 4, 0, 0x29, 0, 0, 0}; + uint8_t unknown_a1 = 0; + parray unknown_a2; + Episode3::ActionState state; +} __packed__; // 6xB4x2A: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB4x2A { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x2A) / 4, 0, 0x2A, 0, 0, 0}; parray unknown_a1; - le_uint16_t unknown_a2; + le_uint16_t unknown_a2 = 0; parray unused; -}; +} __packed__; // 6xB3x2B / CAx2B: Unknown struct G_Unknown_GC_Ep3_6xB3x2B_CAx2B { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint16_t unknown_a2; - parray unknown_a3; -}; + G_CardBattleCommandHeader header = {0xB3, sizeof(G_Unknown_GC_Ep3_6xB3x2B_CAx2B) / 4, 0, 0x2B, 0, 0, 0}; + parray unused1; + le_uint16_t unused2 = 0; + parray unused3; +} __packed__; // 6xB4x2C: Unknown struct G_Unknown_GC_Ep3_6xB4x2C { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - uint8_t unknown_a1; - uint8_t unknown_a2; // Only used if the preceding field == 3 - parray unknown_a3; - parray unknown_a4; - parray unknown_a5; -}; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x2C) / 4, 0, 0x2C, 0, 0, 0}; + uint8_t change_type = 0; + uint8_t client_id = 0; + parray card_refs; + Episode3::Location loc; + parray unknown_a2; +} __packed__; // 6xB5x2D: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x2D { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x2D) / 4, 0, 0x2D, 0, 0, 0}; // This array is indexed into by a global variable. I don't have any examples // of this command, so I don't know how long the array should be - 4 is a // probably-incorrect guess. parray unknown_a1; -}; +} __packed__; -// 6xB5x2E: End game -// Note: This is sent as a C9 command, not a server data (CA) command. A CAx21 -// usually follows soon after this, to which the server responds with a 6xB4x46, -// which actually ends the battle. +// 6xB5x2E: Notify other players that battle is about to end -struct G_Unknown_GC_Ep3_6xB5x2E { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - uint8_t unknown_a1; // Command ignored unless this is 0 or 1 +struct G_BattleEndNotification_GC_Ep3_6xB5x2E { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_BattleEndNotification_GC_Ep3_6xB5x2E) / 4, 0, 0x2E, 0, 0, 0}; + uint8_t unknown_a1 = 0; // Command ignored unless this is 0 or 1 parray unused; -}; +} __packed__; // 6xB5x2F: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x2F { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x2F) / 4, 0, 0x2F, 0, 0, 0}; parray unknown_a1; parray unknown_a2; ptext deck_name; parray unknown_a3; - le_uint16_t unknown_a4; + le_uint16_t unknown_a4 = 0; parray card_ids; parray unused; - le_uint32_t unknown_a5; - le_uint16_t unknown_a6; - le_uint16_t unknown_a7; -}; + le_uint32_t unknown_a5 = 0; + le_uint16_t unknown_a6 = 0; + le_uint16_t unknown_a7 = 0; +} __packed__; // 6xB5x30: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x30 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x30) / 4, 0, 0x30, 0, 0, 0}; // No arguments -}; +} __packed__; // 6xB5x31: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x31 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x31) / 4, 0, 0x31, 0, 0, 0}; // Note: This command uses header_b1 for... something. - uint8_t unknown_a1; // Must be 0 or 1 - uint8_t unknown_a2; // Must be < 4 - uint8_t unknown_a3; // Must be < 4 - uint8_t unknown_a4; // Must be < 0x14 - uint8_t unknown_a5; // Used as an array index + uint8_t unknown_a1 = 0; // Must be 0 or 1 + uint8_t unknown_a2 = 0; // Must be < 4 + uint8_t unknown_a3 = 0; // Must be < 4 + uint8_t unknown_a4 = 0; // Must be < 0x14 + uint8_t unknown_a5 = 0; // Used as an array index parray unused; -}; +} __packed__; // 6xB5x32: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x32 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x32) / 4, 0, 0x32, 0, 0, 0}; // Note: This command uses header_b1 for... something. - le_uint16_t unknown_a1; - le_uint16_t unknown_a2; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; parray unknown_a3; -}; +} __packed__; -// 6xB4x33: Unknown +// 6xB4x33: Subtract ally ATK points (e.g. for photon blast) -struct G_Unknown_GC_Ep3_6xB4x33 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - uint8_t unknown_a1; - uint8_t unused; - le_uint16_t unknown_a2; -}; +struct G_SubtractAllyATKPoints_GC_Ep3_6xB4x33 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SubtractAllyATKPoints_GC_Ep3_6xB4x33) / 4, 0, 0x33, 0, 0, 0}; + uint8_t client_id = 0; + uint8_t ally_cost = 0; + le_uint16_t card_ref = 0xFFFF; +} __packed__; -// 6xB3x34 / CAx34: Unknown +// 6xB3x34 / CAx34: Photon blast request -struct G_Unknown_GC_Ep3_6xB3x34_CAx34 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - uint8_t client_id; - uint8_t unknown_a2; - le_uint16_t unknown_a3; // Possibly DeckCardRef -}; +struct G_PhotonBlastRequest_GC_Ep3_6xB3x34_CAx34 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_PhotonBlastRequest_GC_Ep3_6xB3x34_CAx34) / 4, 0, 0x34, 0, 0, 0}; + parray unused; + uint8_t ally_client_id = 0; + uint8_t reason = 0; + le_uint16_t card_ref = 0xFFFF; +} __packed__; -// 6xB4x35: Unknown +// 6xB4x35: Photon blast status -struct G_Unknown_GC_Ep3_6xB4x35 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - uint8_t unknown_a1; - uint8_t unknown_a2; - le_uint16_t unknown_a3; -}; +struct G_PhotonBlastStatus_GC_Ep3_6xB4x35 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_PhotonBlastStatus_GC_Ep3_6xB4x35) / 4, 0, 0x35, 0, 0, 0}; + uint8_t client_id = 0; + uint8_t accepted = 0; + le_uint16_t card_ref = 0xFFFF; +} __packed__; // 6xB5x36: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x36 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - uint8_t unknown_a1; // Must be < 12 (maybe lobby or spectator team client ID) + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x36) / 4, 0, 0x36, 0, 0, 0}; + uint8_t unknown_a1 = 0; // Must be < 12 (maybe lobby or spectator team client ID) parray unused; -}; +} __packed__; -// 6xB3x37: Unknown +// 6xB3x37 / CAx37: Ready to advance from starting rolls phase -struct G_Unknown_GC_Ep3_6xB3x37 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; +struct G_AdvanceFromStartingRollsPhase_GC_Ep3_6xB3x37 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_AdvanceFromStartingRollsPhase_GC_Ep3_6xB3x37) / 4, 0, 0x37, 0, 0, 0}; parray unused1; - uint8_t unknown_a1; + uint8_t client_id = 0; parray unused2; -}; +} __packed__; // 6xB5x38: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x38 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - uint8_t unknown_a1; - uint8_t unknown_a2; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x38) / 4, 0, 0x38, 0, 0, 0}; + uint8_t unknown_a1 = 0; + uint8_t unknown_a2 = 0; parray unused; -}; +} __packed__; -// 6xB4x39: Unknown +// 6xB4x39: Update all player statistics -struct G_Unknown_GC_Ep3_6xB4x39 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - struct Entry { - parray unknown_a1; - parray unknown_a2; - }; - Entry entries[4]; -}; +struct G_UpdateAllPlayerStatistics_GC_Ep3_6xB4x39 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateAllPlayerStatistics_GC_Ep3_6xB4x39) / 4, 0, 0x39, 0, 0, 0}; + parray stats; +} __packed__; // 6xB3x3A / CAx3A: Unknown struct G_Unknown_GC_Ep3_6xB3x3A_CAx3A { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; -}; + G_CardBattleCommandHeader header = {0xB3, sizeof(G_Unknown_GC_Ep3_6xB3x3A_CAx3A) / 4, 0, 0x3A, 0, 0, 0}; + parray unused; +} __packed__; // 6xB4x3B: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB4x3B { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x3B) / 4, 0, 0x3B, 0, 0, 0}; parray unused; -}; +} __packed__; // 6xB5x3C: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x3C { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x3C) / 4, 0, 0x3C, 0, 0, 0}; // Note: This command uses header_b1 for... something. - uint8_t unknown_a1; + uint8_t unknown_a1 = 0; parray unused; -}; +} __packed__; // 6xB4x3D: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB4x3D { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x3D) / 4, 0, 0x3D, 0, 0, 0}; parray unknown_a1; struct Entry { - uint8_t type; // 1 = human, 2 = COM + uint8_t type = 0; // 1 = human, 2 = COM ptext player_name; ptext deck_name; // Seems to only be used for COM players parray unknown_a1; parray card_ids; parray unused; - le_uint16_t unknown_a2; - le_uint16_t unknown_a3; - }; - Entry entries[4]; - le_uint32_t map_number; - uint8_t unknown_a2; - uint8_t unknown_a3; - uint8_t unknown_a4; - uint8_t unknown_a5; -}; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + } __packed__; + parray entries; + le_uint32_t map_number = 0; + uint8_t unknown_a2 = 0; + uint8_t unknown_a3 = 0; + uint8_t unknown_a4 = 0; + uint8_t unknown_a5 = 0; +} __packed__; // 6xB5x3E: Make card auction bid struct G_MakeCardAuctionBid_GC_Ep3_6xB5x3E { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_MakeCardAuctionBid_GC_Ep3_6xB5x3E) / 4, 0, 0x3E, 0, 0, 0}; // Note: This command uses header.unknown_a1 for the bidder's client ID. - uint8_t card_index; // Index of card in EF command - uint8_t bid_value; // 1-99 + uint8_t card_index = 0; // Index of card in EF command + uint8_t bid_value = 0; // 1-99 parray unused; -}; +} __packed__; // 6xB5x3F: Open menu -struct G_OpenMenu_GC_Ep3_6xB5x3F { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; +struct G_OpenBlockingMenu_GC_Ep3_6xB5x3F { + G_CardBattleCommandHeader header = {0xB5, sizeof(G_OpenBlockingMenu_GC_Ep3_6xB5x3F) / 4, 0, 0x3F, 0, 0, 0}; // Menu type should be one of these values: // 0x01/0x02 = battle prep menu // 0x11 = card auction counter menu (join or cancel) // 0x12 = go directly to card auction state (client sends EF command) // Other values will likely crash the client. - int8_t menu_type; // Must be in the range [-1, 0x14] - uint8_t client_id; + int8_t menu_type = 0; // Must be in the range [-1, 0x14] + uint8_t client_id = 0; parray unused1; - le_uint32_t unknown_a3; + le_uint32_t unknown_a3 = 0; parray unused2; -}; +} __packed__; // 6xB3x40 / CAx40: Map list request -// If sent to a client as 6xB3x40, the client ignores the command completely. If -// sent to the server, the server should respond with a 6xB6x40 command. +// The server should respond with a 6xB6x40 command. struct G_MapListRequest_GC_Ep3_6xB3x40_CAx40 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; -}; + G_CardBattleCommandHeader header = {0xB3, sizeof(G_MapListRequest_GC_Ep3_6xB3x40_CAx40) / 4, 0, 0x40, 0, 0, 0}; + be_uint32_t sequence_num = 0; + be_uint32_t unknown_a1 = 0; +} __packed__; // 6xB3x41 / CAx41: Map data request -// If sent to a client as 6xB3x41, the client ignores the command completely. If -// sent to the server, the server should respond with a 6xB6x41 command. +// The server should respond with a 6xB6x41 command. struct G_MapDataRequest_GC_Ep3_6xB3x41_CAx41 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint32_t map_number; -}; + G_CardBattleCommandHeader header = {0xB3, sizeof(G_MapDataRequest_GC_Ep3_6xB3x41_CAx41) / 4, 0, 0x41, 0, 0, 0}; + be_uint32_t sequence_num = 0; + be_uint32_t unknown_a1 = 0; + le_uint32_t map_number = 0; +} __packed__; // 6xB5x42: Initiate card auction // Sending this command to a client has the same effect as sending a 6xB5x3F -// command to tell it to open the auction menu. However, under normal operation, -// the server doens't need to do this - the client sends this when all of the -// following conditions are met: -// 1. The client has a VIP card. (This is stored client-side in seq flag 7000). +// command to tell it to open the auction menu. (This works even if the client +// doesn't have a VIP card or there are fewer than 4 players in the current +// game.) Under normal operation, the server doesn't need to do this - the +// client sends this when all of the following conditions are met: +// 1. The client has a VIP card. (This is stored client-side in seq flag 7000.) // 2. The client is in a game with 4 players. // 3. All clients are at the auction counter. struct G_InitiateCardAuction_GC_Ep3_6xB5x42 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_InitiateCardAuction_GC_Ep3_6xB5x42) / 4, 0, 0x42, 0, 0, 0}; // This command uses header.unknown_a1 (probably for the client's ID). -}; +} __packed__; // 6xB5x43: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x43 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x43) / 4, 0, 0x43, 0, 0, 0}; struct Entry { // Both fields here are masked. To get the actual values used by the game, // XOR the values here with 0x39AB. - le_uint16_t masked_card_id; // Must be < 0x2F1 (when unmasked) - le_uint16_t masked_unknown_a1; // Must be in [1, 99] (when unmasked) - }; - Entry entries[0x14]; -}; + le_uint16_t masked_card_id = 0xFFFF; // Must be < 0x2F1 (when unmasked) + le_uint16_t masked_unknown_a1 = 0; // Must be in [1, 99] (when unmasked) + } __packed__; + parray entries; +} __packed__; // 6xB5x44: Card auction bid summary struct G_CardAuctionBidSummary_GC_Ep3_6xB5x44 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_CardAuctionBidSummary_GC_Ep3_6xB5x44) / 4, 0, 0x44, 0, 0, 0}; // Note: This command uses header.unknown_a1 for the bidder's client ID. parray bids; // In same order as cards in the EF command -}; +} __packed__; // 6xB5x45: Card auction results struct G_CardAuctionResults_GC_Ep3_6xB5x45 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_CardAuctionResults_GC_Ep3_6xB5x45) / 4, 0, 0x45, 0, 0, 0}; // Note: This command uses header.unknown_a1 for the sender's client ID. // This array is indexed by [card_index][client_id], and contains the final // bid for each player on each card (or 0 if they did not bid on that card). parray, 8> bids_by_player; -}; +} __packed__; -// 6xB4x46: Start or end battle +// 6xB4x46: Server version strings +// This command doesn't seem to be necessary to actually play the game; the +// client just copies the included strings to global buffers and then ignores +// them. Sega's servers sent this twice for each battle, however: once after the +// initial setup phase (before starter rolls) and once when the results screen +// appeared. -struct G_Unknown_GC_Ep3_6xB4x46 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; +struct G_ServerVersionStrings_GC_Ep3_6xB4x46 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_ServerVersionStrings_GC_Ep3_6xB4x46) / 4, 0, 0x46, 0, 0, 0}; // In all of the examples (from Sega's servers) that I've seen of this // command, these fields have the following values: // version_signature = "[V1][FINAL2.0] 03/09/13 15:30 by K.Toya" // date_str1 = "Mar 7 2007 21:42:40" ptext version_signature; ptext date_str1; // Possibly card definitions revision date - // This field is blank when starting a battle, and contains the current time - // (formatted like "YYYY/MM/DD hh:mm:ss") when ending a battle. + // In Sega's implementation, it seems this field is blank when starting a + // battle, and contains the current time (in the format "YYYY/MM/DD hh:mm:ss") + // when ending a battle. ptext date_str2; // It seems Sega used to send 0 here when starting a battle, and 0x04157580 // when ending a battle. Since the field is unused by the client, it's not - // clear what that value means (if anything). - le_uint32_t unused; -}; + // clear what that value means, if anything. This behavior may be another + // uninitialized memory bug in the server implementation (of which there are + // ample other examples). + le_uint32_t unused = 0; +} __packed__; // 6xB5x47: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB5x47 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_GC_Ep3_6xB5x47) / 4, 0, 0x47, 0, 0, 0}; // Note: This command uses header_b1, which must be < 12. - le_uint32_t unknown_a1; -}; + le_uint32_t unknown_a1 = 0; +} __packed__; -// 6xB3x48 / CAx48: Unknown +// 6xB3x48 / CAx48: End turn -struct G_Unknown_GC_Ep3_6xB3x48_CAx48 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - uint8_t unknown_a2; +struct G_EndTurn_GC_Ep3_6xB3x48_CAx48 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_EndTurn_GC_Ep3_6xB3x48_CAx48) / 4, 0, 0x48, 0, 0, 0}; + be_uint32_t sequence_num = 0; + parray unused1; + uint8_t client_id = 0; parray unused2; -}; +} __packed__; -// 6xB3x49 / CAx49: Unknown +// 6xB3x49 / CAx49: Card counts +// This command is sent when a client joins a game, but it is completely ignored +// by the server. (Not just newserv - the server implementation within Episode 3 +// itself also ignores this command.) Sega presumably could have used this to +// detect the presence of unreleased cards to ban cheaters, but the effects of +// the non-saveable Have All Cards AR code don't appear in this data, so this +// would have been ineffective. -struct G_Unknown_GC_Ep3_6xB3x49_CAx49 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - be_uint32_t sequence_number; - be_uint32_t unknown_a1; - le_uint32_t unknown_a2; - parray unknown_a3; -}; +struct G_CardCounts_GC_Ep3_6xB3x49_CAx49 { + G_CardBattleCommandHeader header = {0xB3, sizeof(G_CardCounts_GC_Ep3_6xB3x49_CAx49) / 4, 0, 0x49, 0, 0, 0}; + be_uint32_t sequence_num = 0; + be_uint32_t unknown_a1 = 0; + uint8_t basis = 0; + parray unused; + // This is encrypted with the trivial algorithm (see decrypt_trivial_gci_data) + // using the basis in the preceding field + parray card_id_to_count; +} __packed__; // 6xB4x4A: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB4x4A { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x4A) / 4, 0, 0x4A, 0, 0, 0}; // Note: entry_count appears not to be bounds-checked; presumably the server // could send up to 0xFF entries, but those after the 8th would not be // byteswapped before the client handles them. - uint8_t unknown_a1; - uint8_t entry_count; - le_uint16_t unknown_a2; - parray entries; -}; + uint8_t client_id = 0; + uint8_t entry_count = 0; + le_uint16_t round_num = 0; + parray card_refs; +} __packed__; // 6xB4x4B: Unknown +// TODO: Document this from Episode 3 client/server disassembly struct G_Unknown_GC_Ep3_6xB4x4B { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x4B) / 4, 0, 0x4B, 0, 0, 0}; // If any of the entries [0][1] through [0][10] or [1][1] through [1][10] are // < -99 or > 99, the entire command is ignored. parray, 2> unknown_a1; -}; +} __packed__; -// 6xB4x4C: Unknown +// 6xB4x4C: Update action chain -struct G_Unknown_GC_Ep3_6xB4x4C { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - uint8_t unknown_a1; // Must be < 4 - int8_t unknown_a2; // Must be in [-1, 8] - parray unknown_a3; +struct G_UpdateActionChain_GC_Ep3_6xB4x4C { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateActionChain_GC_Ep3_6xB4x4C) / 4, 0, 0x4C, 0, 0, 0}; + uint8_t client_id = 0; + int8_t index = 0; + parray unused; + Episode3::ActionChain chain; +} __packed__; - parray unknown_a4; - le_uint16_t unknown_a5; - le_uint16_t unknown_a6; - parray unknown_a7; - parray unknown_a8; - le_uint32_t unknown_a9; - parray unknown_a10; -}; +// 6xB4x4D: Update action metadata -// 6xB4x4D: Unknown +struct G_UpdateActionMetadata_GC_Ep3_6xB4x4D { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateActionMetadata_GC_Ep3_6xB4x4D) / 4, 0, 0x4D, 0, 0, 0}; + uint8_t client_id = 0; + int8_t index = 0; + parray unused; + Episode3::ActionMetadata metadata; +} __packed__; -struct G_Unknown_GC_Ep3_6xB4x4D { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - uint8_t unknown_a1; // Must be < 4 - int8_t unknown_a2; // Must be in [-1, 8] - parray unknown_a3; +// 6xB4x4E: Update card conditions - le_uint16_t unknown_a4; - parray unknown_a5; - le_uint32_t unknown_a6; - parray unknown_a7; - parray unknown_a8; - parray unknown_a9; -}; +struct G_UpdateCardConditions_GC_Ep3_6xB4x4E { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_UpdateCardConditions_GC_Ep3_6xB4x4E) / 4, 0, 0x4E, 0, 0, 0}; + uint8_t client_id = 0; + int8_t index = 0; + parray unused; + parray conditions; +} __packed__; -// 6xB4x4E: Unknown +// 6xB4x4F: Clear set card conditions -struct G_Unknown_GC_Ep3_6xB4x4E { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - uint8_t unknown_a1; // Must be < 4 - int8_t unknown_a2; // Must be in [-1, 8] - parray unknown_a3; - struct Entry { - parray unknown_a1; - parray unknown_a2; - parray unknown_a3; - }; - Entry entries[9]; -}; +struct G_ClearSetCardConditions_GC_Ep3_6xB4x4F { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_ClearSetCardConditions_GC_Ep3_6xB4x4F) / 4, 0, 0x4F, 0, 0, 0}; + uint8_t client_id = 0; + uint8_t unused = 0; + // For each 1 bit in this mask, the conditions of the corresponding card + // should be deleted. The low bit corresponds to the SC card; the next bit + // corresponds to set slot 1, the next bit to set slot 2, etc. (The upper 7 + // bits of this field are unused.) + le_uint16_t clear_mask = 0; +} __packed__; -// 6xB4x4F: Unknown +// 6xB4x50: Set trap tile locations -struct G_Unknown_GC_Ep3_6xB4x4F { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - uint8_t unknown_a1; - uint8_t unused; - le_uint16_t unknown_a2; // Bitmask; the 9 low-order bits are used -}; - -// 6xB4x50: Unknown - -struct G_Unknown_GC_Ep3_6xB4x50 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - parray unknown_a1; -}; +struct G_SetTrapTileLocations_GC_Ep3_6xB4x50 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_SetTrapTileLocations_GC_Ep3_6xB4x50) / 4, 0, 0x50, 0, 0, 0}; + // Each entry in this array corresponds to one of the 5 trap types, in order. + // Each entry is a, [x, y] pair; if that trap type is not present, its + // location entry is FF FF. + parray, 5> locations; + parray unused; +} __packed__; // 6xB4x51: Tournament match info struct G_Unknown_GC_Ep3_6xB4x51 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x51) / 4, 0, 0x51, 0, 0, 0}; ptext match_description; struct Entry { ptext team_name; parray, 2> player_names; - }; - Entry teams[2]; - le_uint16_t unknown_a1; - le_uint16_t unknown_a2; - le_uint16_t unknown_a3; - le_uint16_t unknown_a4; - le_uint32_t meseta_amount; + } __packed__; + parray teams; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint16_t unknown_a4 = 0; + le_uint32_t meseta_amount = 0; // This field apparently is supposed to contain a %s token (as for printf) // that is replaced with meseta_amount. ptext meseta_reward_text; -}; +} __packed__; // 6xB4x52: Unknown struct G_Unknown_GC_Ep3_6xB4x52 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - le_uint16_t unknown_a1; - le_uint16_t unknown_a2; - le_uint16_t unknown_a3; - le_uint16_t size; // Number of valid bytes in the data field (clamped to 0xFF) + G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_GC_Ep3_6xB4x52) / 4, 0, 0x52, 0, 0, 0}; + le_uint16_t unknown_a1 = 0; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint16_t size = 0; // Number of valid bytes in the data field (clamped to 0xFF) parray data; -}; +} __packed__; -// 6xB4x53: Unknown +// 6xB4x53: Reject battle start request +// This is sent in response to a CAx1D command if setup isn't complete (e.g. if +// some names/decks are missing or invalid). +// Note: It seems the client ignores everything in this structure; the command +// handler just sets a global state flag and returns immediately. -struct G_Unknown_GC_Ep3_6xB4x53 { - G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; - // Note: It seems the client ignores everything in this structure. The command - // just sets a global state flag, then returns immediately. - - parray unknown_a1; - - le_uint16_t width; - le_uint16_t height; - parray tiles; - parray unknown_a2; - uint8_t num_players; - uint8_t unknown_a3; - uint8_t unknown_a4; - parray unknown_a5; - le_uint16_t unknown_a6; - parray unknown_a7; - le_uint32_t map_number; - // Command may be larger than this structure -}; - - - -#pragma pack(pop) +struct G_RejectBattleStartRequest_GC_Ep3_6xB4x53 { + G_CardBattleCommandHeader header = {0xB4, sizeof(G_RejectBattleStartRequest_GC_Ep3_6xB4x53) / 4, 0, 0x53, 0, 0, 0}; + Episode3::SetupPhase setup_phase; + Episode3::RegistrationPhase registration_phase; + parray unused; + Episode3::MapAndRulesState state; +} __packed__; diff --git a/src/Episode3.cc b/src/Episode3.cc deleted file mode 100644 index 4dc25db4..00000000 --- a/src/Episode3.cc +++ /dev/null @@ -1,936 +0,0 @@ -#include "Episode3.hh" - -#include - -#include -#include - -#include "Loggers.hh" -#include "Compression.hh" -#include "Text.hh" - -using namespace std; - - - -static const vector name_for_card_type({ - "HunterSC", - "ArkzSC", - "Item", - "Creature", - "Action", - "Assist", -}); - -static const unordered_map description_for_when({ - {0x01, "Set"}, // TODO: Is 01 this, or "Permanent"? - {0x02, "Attack"}, - {0x03, "??? (TODO)"}, - {0x04, "Before turn"}, - {0x05, "Destroyed"}, - {0x0A, "Permanent"}, // only used on Tollaw; could be same as 01 - {0x0B, "Battle"}, - {0x0C, "Opponent destroyed"}, // TODO: but this is also used for some support things like Shifta, and for Snatch, which also applies when opponents are not destroyed - {0x0D, "Attack lands"}, - {0x0E, "Before attack phase"}, - {0x16, "Battle end"}, - {0x17, "Each defense"}, - {0x20, "Each attack"}, - {0x22, "Act phase"}, - {0x27, "Move phase"}, - {0x29, "Set and act phases"}, - {0x33, "Defense phase"}, - {0x3D, "Battle"}, // TODO: how is this different from 3D and 0B? - {0x3E, "Battle"}, // TODO: how is this different from 3D and 0B? - {0x3F, "Each defense"}, // TODO: how is this different from 17? - {0x46, "On specific turn"}, -}); - -static const unordered_map description_for_expr_token({ - {"f", "Number of FCs controlled by current SC"}, - {"d", "Die roll"}, - {"ap", "Attacker AP"}, // Unused - {"tp", "Attacker TP"}, - {"hp", "Attacker HP"}, // TODO: How is this different from ehp? - {"mhp", "Attacker maximum HP"}, - {"dm", "Unknown: dm"}, // Unused - {"tdm", "Unknown: tdm"}, // Unused - {"tf", "Number of SC\'s destroyed FCs"}, - {"ac", "Remaining ATK points"}, - {"php", "Unknown: php"}, // Unused - {"dc", "Unknown: dc"}, // Unused - {"cs", "Unknown: cs"}, // Unused - {"a", "Unknown: a"}, // Unused - {"kap", "Action cards AP"}, - {"ktp", "Action cards TP"}, - {"dn", "Unknown: dn"}, // Unused - {"hf", "Unknown: hf"}, // Unused - {"df", "Number of destroyed ally FCs (including SC\'s own)"}, - {"ff", "Number of ally FCs (including SC\'s own)"}, - {"ef", "Number of enemy FCs"}, - {"bi", "Number of Native FCs on either team"}, - {"ab", "Number of A.Beast FCs on either team"}, - {"mc", "Number of Machine FCs on either team"}, - {"dk", "Number of Dark FCs on either team"}, - {"sa", "Number of Sword-type items on either team"}, - {"gn", "Number of Gun-type items on either team"}, - {"wd", "Number of Cane-type items on either team"}, - {"tt", "Unknown: tt"}, // Unused - {"lv", "Dice bonus"}, - {"adm", "Attack damage"}, - {"ddm", "Defending damage"}, - {"sat", "Number of Sword-type items on SC\'s team"}, - {"edm", "Defending damage"}, // TODO: How is this different from ddm? - {"ldm", "Unknown: ldm"}, // Unused - {"rdm", "Defending damage"}, // TODO: How is this different from ddm/edm? - {"fdm", "Final damage (after defense)"}, - {"ndm", "Unknown: ndm"}, // Unused - {"ehp", "Attacker HP"}, -}); - -// Arguments are encoded as 3-character null-terminated strings (why?!), and are -// used for adding conditions to effects (e.g. making them only trigger in -// certain situations) or otherwise customizing their results. -// Argument meanings: -// a01 = ??? -// cXY/CXY = linked items (require item with cYX/CYX to be equipped as well) -// dXY = roll one die; require result between X and Y inclusive -// e00 = effect lasts while equipped? (in contrast to tXX) -// hXX = require HP >= XX -// iXX = require HP <= XX -// nXX = require condition XX (see description_for_n_condition) -// oXX = seems to be "require previous random-condition effect to have happened" -// TODO: this is used as both o01 (recovery) and o11 (reflection) -// conditions - why the difference? -// pXX = who to target (see description_for_p_target) -// rXX = randomly pass with XX% chance (if XX == 00, 100% chance?) -// sXY = require card cost between X and Y ATK points (inclusive) -// tXX = lasts XX turns, or activate after XX turns - -static const vector description_for_n_condition({ - /* n00 */ "Always true", - /* n01 */ "??? (TODO)", - /* n02 */ "Destroyed with a single attack?", - /* n03 */ "Unknown", // Unused - /* n04 */ "Attack has Pierce", - /* n05 */ "Attack has Rampage", - /* n06 */ "Native attribute", - /* n07 */ "A.Beast attribute", - /* n08 */ "Machine attribute", - /* n09 */ "Dark attribute", - /* n10 */ "Sword-type item", - /* n11 */ "Gun-type item", - /* n12 */ "Cane-type item", - /* n13 */ "Guard item", - /* n14 */ "Story Character", - /* n15 */ "Attacker does not use action cards", - /* n16 */ "Aerial attribute", - /* n17 */ "Same AP as opponent", - /* n18 */ "Opponent is SC", - /* n19 */ "Has Paralyzed condition", - /* n20 */ "Has Frozen condition", -}); - -static const vector description_for_p_target({ - /* p00 */ "Unknown: p00", // Unused; probably invalid - /* p01 */ "SC / FC who set the card", - /* p02 */ "Attacking SC / FC", - /* p03 */ "Unknown: p03", // Unused - /* p04 */ "Unknown: p04", // Unused - /* p05 */ "Unknown: p05", // Unused - /* p06 */ "??? (TODO)", - /* p07 */ "??? (TODO; Weakness)", - /* p08 */ "FC's owner SC", - /* p09 */ "Unknown: p09", // Unused - /* p10 */ "All ally FCs", - /* p11 */ "All ally FCs", // TODO: how is this different from p10? - /* p12 */ "All non-aerial FCs on both teams", - /* p13 */ "All FCs on both teams that are Frozen", - /* p14 */ "All FCs on both teams that have <= 3 HP", - /* p15 */ "All FCs except SCs", // TODO: used during attacks only? - /* p16 */ "All FCs except SCs", // TODO: used during attacks only? how is this different from p15? - /* p17 */ "This card", - /* p18 */ "SC who equipped this card", - /* p19 */ "Unknown: p19", // Unused - /* p20 */ "Unknown: p20", // Unused - /* p21 */ "Unknown: p21", // Unused - /* p22 */ "All characters (SCs & FCs) including this card", // TODO: But why does Shifta apply only to allies then? - /* p23 */ "All characters (SCs & FCs) except this card", - /* p24 */ "All FCs on both teams that have Paralysis", - /* p25 */ "Unknown: p25", // Unused - /* p26 */ "Unknown: p26", // Unused - /* p27 */ "Unknown: p27", // Unused - /* p28 */ "Unknown: p28", // Unused - /* p29 */ "Unknown: p29", // Unused - /* p30 */ "Unknown: p30", // Unused - /* p31 */ "Unknown: p31", // Unused - /* p32 */ "Unknown: p32", // Unused - /* p33 */ "Unknown: p33", // Unused - /* p34 */ "Unknown: p34", // Unused - /* p35 */ "All characters (SCs & FCs) within range", // Used for Explosion effect - /* p36 */ "All ally SCs within range, but not the caster", // Resta - /* p37 */ "All FCs or all opponent FCs (TODO)", // TODO: when to use which selector? is a3 involved here somehow? - /* p38 */ "All allies except items within range (and not this card)", - /* p39 */ "All FCs that cost 4 or more points", - /* p40 */ "All FCs that cost 3 or fewer points", - /* p41 */ "Unknown: p41", // Unused - /* p42 */ "Attacker during defense phase", // Only used by TP Defense - /* p43 */ "Owner SC of defending FC during attack", - /* p44 */ "SC\'s own creature FCs within range", - /* p45 */ "Both attacker and defender", // Used for Snatch, which moves EXP from one to the other - /* p46 */ "All SCs & FCs one space left or right of this card", - /* p47 */ "FC\'s owner Boss SC", // Only used for Gibbles+ which explicitly mentions Boss SC, so it looks like this is p08 but for bosses - /* p48 */ "Everything within range, including this card\'s user", // Madness - /* p49 */ "All ally FCs within range except this card", -}); - -struct Ep3AbilityDescription { - uint8_t command; - bool has_expr; - const char* name; - const char* description; -}; - -static const std::vector name_for_effect_command({ - {0x00, false, nullptr, nullptr}, - {0x01, true, "AP Boost", "Temporarily increase AP by N"}, - {0x02, false, "Rampage", "Rampage"}, - {0x03, true, "Multi Strike", "Duplicate attack N times"}, - {0x04, true, "Damage Modifier 1", "Set attack damage / AP to N after action cards applied (step 1)"}, - {0x05, false, "Immobile", "Give Immobile condition"}, - {0x06, false, "Hold", "Give Hold condition"}, - {0x07, false, nullptr, nullptr}, - {0x08, true, "TP Boost", "Add N TP temporarily during attack"}, - {0x09, true, "Give Damage", "Cause direct N HP loss"}, - {0x0A, false, "Guom", "Give Guom condition"}, - {0x0B, false, "Paralyze", "Give Paralysis condition"}, - {0x0C, false, nullptr, nullptr}, - {0x0D, false, "A/H Swap", "Swap AP and HP temporarily"}, - {0x0E, false, "Pierce", "Attack SC directly even if they have items equipped"}, - {0x0F, false, nullptr, nullptr}, - {0x10, true, "Heal", "Increase HP by N"}, - {0x11, false, "Return to Hand", "Return card to hand"}, - {0x12, false, nullptr, nullptr}, - {0x13, false, nullptr, nullptr}, - {0x14, false, "Acid", "Give Acid condition"}, - {0x15, false, nullptr, nullptr}, - {0x16, true, "Mighty Knuckle", "Temporarily increase AP by N, and set ATK dice to zero"}, - {0x17, true, "Unit Blow", "Temporarily increase AP by N * number of this card set within phase"}, - {0x18, false, "Curse", "Give Curse condition"}, - {0x19, false, "Combo (AP)", "Temporarily increase AP by number of this card set within phase"}, - {0x1A, false, "Pierce/Rampage Block", "Block attack if Pierce/Rampage (?)"}, - {0x1B, false, "Ability Trap", "Temporarily disable opponent abilities"}, - {0x1C, false, "Freeze", "Give Freeze condition"}, - {0x1D, false, "Anti-Abnormality", "Cure all conditions"}, - {0x1E, false, nullptr, nullptr}, - {0x1F, false, "Explosion", "Damage all SCs and FCs by number of this same card set * 2"}, - {0x20, false, nullptr, nullptr}, - {0x21, false, nullptr, nullptr}, - {0x22, false, nullptr, nullptr}, - {0x23, false, "Return to Deck", "Cancel discard and move to bottom of deck instead"}, - {0x24, false, "Aerial", "Give Aerial status"}, - {0x25, true, "AP Loss", "Make attacker temporarily lose N AP during defense"}, - {0x26, true, "Bonus From Leader", "Gain AP equal to the number of cards of type N on the field"}, - {0x27, false, "Free Maneuver", "Enable movement over occupied tiles"}, - {0x28, false, "Haste", "Make move actions free"}, - {0x29, true, "Clone", "Make setting this card free if at least one card of type N is already on the field"}, - {0x2A, true, "DEF Disable by Cost", "Disable use of any defense cards costing between (N / 10) and (N % 10) points, inclusive"}, - {0x2B, true, "Filial", "Increase controlling SC\'s HP by N when this card is destroyed"}, - {0x2C, true, "Snatch", "Steal N EXP during attack"}, - {0x2D, true, "Hand Disrupter", "DIscard N cards from hand immediately"}, - {0x2E, false, "Drop", "Give Drop condition"}, - {0x2F, false, "Action Disrupter", "Destroy all action cards used by attacker"}, - {0x30, true, "Set HP", "Set HP to N (?) (TODO)"}, - {0x31, false, "Native Shield", "Block attacks from Native creatures"}, - {0x32, false, "A.Beast Shield", "Block attacks from A.Beast creatures"}, - {0x33, false, "Machine Shield", "Block attacks from Machine creatures"}, - {0x34, false, "Dark Shield", "Block attacks from Dark creatures"}, - {0x35, false, "Sword Shield", "Block attacks from Sword items"}, - {0x36, false, "Gun Shield", "Block attacks from Gun items"}, - {0x37, false, "Cane Shield", "Block attacks from Cane items"}, - {0x38, false, nullptr, nullptr}, - {0x39, false, nullptr, nullptr}, - {0x3A, false, "Defender", "Make attacks go to setter of this card instead of original target"}, - {0x3B, false, "Survival Decoys", "Redirect damage for multi-sided attack"}, - {0x3C, true, "Give/Take EXP", "Give N EXP, or take if N is negative"}, - {0x3D, false, nullptr, nullptr}, - {0x3E, false, "Death Companion", "If this card has 1 or 2 HP, set its HP to N"}, - {0x3F, true, "EXP Decoy", "If defender has EXP, lose EXP instead of getting damage when attacked"}, - {0x40, true, "Set MV", "Set MV to N"}, - {0x41, true, "Group", "Temporarily increase AP by N * number of this card on field, excluding itself"}, - {0x42, false, "Berserk", "User of this card receives the same damage as target, and isn't helped by target's defense cards"}, - {0x43, false, "Guard Creature", "Attacks on controlling SC damage this card instead"}, - {0x44, false, "Tech", "Technique cards cost 1 fewer ATK point"}, - {0x45, false, "Big Swing", "Increase all attacking ATK costs by 1"}, - {0x46, false, nullptr, nullptr}, - {0x47, false, "Shield Weapon", "Limit attacker\'s choice of target to guard items"}, - {0x48, false, "ATK Dice Boost", "Increase ATK dice roll by 1"}, - {0x49, false, nullptr, nullptr}, - {0x4A, false, "Major Pierce", "If SC has over half of max HP, attacks target SC instead of equipped items"}, - {0x4B, false, "Heavy Pierce", "If SC has 3 or more items equipped, attacks target SC instead of equipped items"}, - {0x4C, false, "Major Rampage", "If SC has over half of max HP, attacks target SC and all equipped items"}, - {0x4D, false, "Heavy Rampage", "If SC has 3 or more items equipped, attacks target SC and all equipped items"}, - {0x4E, true, "AP Growth", "Permanently increase AP by N"}, - {0x4F, true, "TP Growth", "Permanently increase TP by N"}, - {0x50, true, "Reborn", "If any card of type N is on the field, this card goes to the hand when destroyed instead of being discarded"}, - {0x51, true, "Copy", "Temporarily set AP/TP to N percent (or 100% if N is 0) of opponent\'s values"}, - {0x52, false, nullptr, nullptr}, - {0x53, true, "Misc. Guards", "Add N to card's defense value"}, - {0x54, true, "AP Override", "Set AP to N temporarily"}, - {0x55, true, "TP Override", "Set TP to N temporarily"}, - {0x56, false, "Return", "Return card to hand on destruction instead of discarding"}, - {0x57, false, "A/T Swap Perm", "Permanently swap AP and TP"}, - {0x58, false, "A/H Swap Perm", "Permanently swap AP and HP"}, - {0x59, true, "Slayers/Assassins", "Temporarily increase AP during attack"}, - {0x5A, false, "Anti-Abnormality", "Remove all conditions"}, - {0x5B, false, "Fixed Range", "Use SC\'s range instead of weapon or attack card ranges"}, - {0x5C, false, "Elude", "SC does not lose HP when equipped items are destroyed"}, - {0x5D, false, "Parry", "Forward attack to a random FC within one tile of original target, excluding attacker and original target"}, - {0x5E, false, "Block Attack", "Completely block attack"}, - {0x5F, false, nullptr, nullptr}, - {0x60, false, nullptr, nullptr}, - {0x61, true, "Combo (TP)", "Gain TP equal to the number of cards of type N on the field"}, - {0x62, true, "Misc. AP Bonuses", "Temporarily increase AP by N"}, - {0x63, true, "Misc. TP Bonuses", "Temporarily increase TP by N"}, - {0x64, false, nullptr, nullptr}, - {0x65, true, "Misc. Defense Bonuses", "Decrease damage by N"}, - {0x66, true, "Mostly Halfguards", "Reduce damage from incoming attack by N"}, - {0x67, false, "Periodic Field", "Swap immunity to tech or physical attacks"}, - {0x68, false, "Unlimited Summoning", "Allow unlimited summoning"}, - {0x69, false, nullptr, nullptr}, - {0x6A, true, "MV Bonus", "Increase MV by N"}, - {0x6B, true, "Forward Damage", "Give N damage back to attacker during defense (?) (TODO)"}, - {0x6C, true, "Weak Spot / Influence", "Temporarily decrease AP by N"}, - {0x6D, true, "Damage Modifier 2", "Set attack damage / AP after action cards applied (step 2)"}, - {0x6E, true, "Weak Hit Block", "Block all attacks of N damage or less"}, - {0x6F, true, "AP Silence", "Temporarily decrease AP of opponent by N"}, - {0x70, true, "TP Silence", "Temporarily decrease TP of opponent by N"}, - {0x71, false, "A/T Swap", "Temporarily swap AP and TP"}, - {0x72, true, "Halfguard", "Halve damage from attacks that would inflict N or more damage"}, - {0x73, false, nullptr, nullptr}, - {0x74, true, "Rampage AP Loss", "Temporarily reduce AP by N"}, - {0x75, false, nullptr, nullptr}, - {0x76, false, "Reflect", "Generate reverse attack"}, -}); - -void Ep3CardDefinition::Stat::decode_code() { - this->type = static_cast(this->code / 1000); - int16_t value = this->code - (this->type * 1000); - if (value != 999) { - switch (this->type) { - case Type::BLANK: - this->stat = 0; - break; - case Type::STAT: - case Type::PLUS_STAT: - case Type::EQUALS_STAT: - this->stat = value; - break; - case Type::MINUS_STAT: - this->stat = -value; - break; - default: - throw runtime_error("invalid card stat type"); - } - } else { - this->stat = 0; - this->type = static_cast(this->type + 4); - } -} - -string Ep3CardDefinition::Stat::str() const { - switch (this->type) { - case Type::BLANK: - return ""; - case Type::STAT: - return string_printf("%hhd", this->stat); - case Type::PLUS_STAT: - return string_printf("+%hhd", this->stat); - case Type::MINUS_STAT: - return string_printf("-%d", -this->stat); - case Type::EQUALS_STAT: - return string_printf("=%hhd", this->stat); - case Type::UNKNOWN: - return "?"; - case Type::PLUS_UNKNOWN: - return "+?"; - case Type::MINUS_UNKNOWN: - return "-?"; - case Type::EQUALS_UNKNOWN: - return "=?"; - default: - return string_printf("[%02hhX %02hhX]", this->type, this->stat); - } -} - - - -bool Ep3CardDefinition::Effect::is_empty() const { - return (this->command == 0 && - this->expr.is_filled_with(0) && - this->when == 0 && - this->arg1.is_filled_with(0) && - this->arg2.is_filled_with(0) && - this->arg3.is_filled_with(0) && - this->unknown_a3.is_filled_with(0)); -} - -string Ep3CardDefinition::Effect::str_for_arg(const std::string& arg) { - if (arg.empty()) { - return arg; - } - if (arg.size() != 3) { - return arg + "/(invalid)"; - } - size_t value; - try { - value = stoul(arg.c_str() + 1, nullptr, 10); - } catch (const invalid_argument&) { - return arg + "/(invalid)"; - } - - switch (arg[0]) { - case 'a': - return arg + "/(unknown)"; - case 'C': - case 'c': - return string_printf("%s/Req. linked item (%zu=>%zu)", arg.c_str(), value / 10, value % 10); - case 'd': - return string_printf("%s/Req. die roll in [%zu, %zu]", arg.c_str(), value / 10, value % 10); - case 'e': - return arg + "/While equipped"; - case 'h': - return string_printf("%s/Req. HP >= %zu", arg.c_str(), value); - case 'i': - return string_printf("%s/Req. HP <= %zu", arg.c_str(), value); - case 'n': - try { - return string_printf("%s/Req. condition: %s", arg.c_str(), description_for_n_condition.at(value)); - } catch (const out_of_range&) { - return arg + "/(unknown)"; - } - case 'o': - return arg + "/Req. prev effect conditions passed"; - case 'p': - try { - return string_printf("%s/Target: %s", arg.c_str(), description_for_p_target.at(value)); - } catch (const out_of_range&) { - return arg + "/(unknown)"; - } - case 'r': - return string_printf("%s/Req. random with %zu%% chance", arg.c_str(), value == 0 ? 100 : value); - case 's': - return string_printf("%s/Req. cost in [%zu, %zu]", arg.c_str(), value / 10, value % 10); - case 't': - return string_printf("%s/Turns: %zu", arg.c_str(), value); - default: - return arg + "/(unknown)"; - } -} - -string Ep3CardDefinition::Effect::str() const { - string cmd_str = string_printf("(%hhu) %02hhX", this->effect_num, this->command); - try { - const char* name = name_for_effect_command.at(this->command).name; - if (name) { - cmd_str += ':'; - cmd_str += name; - } - } catch (const out_of_range&) { } - - string when_str = string_printf("%02hhX", this->when); - try { - const char* name = description_for_when.at(this->when); - if (name) { - when_str += ':'; - when_str += name; - } - } catch (const out_of_range&) { } - - string expr_str = this->expr; - if (!expr_str.empty()) { - expr_str = ", expr=" + expr_str; - } - - string arg1str = this->str_for_arg(this->arg1); - string arg2str = this->str_for_arg(this->arg2); - string arg3str = this->str_for_arg(this->arg3); - string a3str = format_data_string(this->unknown_a3.data(), sizeof(this->unknown_a3)); - return string_printf("(cmd=%s%s, when=%s, arg1=%s, arg2=%s, arg3=%s, a3=%s)", - cmd_str.c_str(), expr_str.c_str(), when_str.c_str(), arg1str.data(), - arg2str.data(), arg3str.data(), a3str.c_str()); -} - - - -void Ep3CardDefinition::decode_range() { - // If the cell representing the FC is nonzero, the card has a range from a - // list of constants. Otherwise, its range is already defined in the range - // array and should be left alone. - uint8_t index = (this->range[4] >> 8) & 0xF; - if (index != 0) { - this->range.clear(0); - switch (index) { - case 1: // Single cell in front of FC - this->range[3] = 0x00000100; - break; - case 2: // Cell in front of FC and the front-left and front-right (Slash) - this->range[3] = 0x00001110; - break; - case 3: // 3 cells in a line in front of FC - this->range[1] = 0x00000100; - this->range[2] = 0x00000100; - this->range[3] = 0x00000100; - break; - case 4: // All 8 cells around FC - this->range[3] = 0x00001110; - this->range[4] = 0x00001010; - this->range[5] = 0x00001110; - break; - case 5: // 2 cells in a line in front of FC - this->range[2] = 0x00000100; - this->range[3] = 0x00000100; - break; - case 6: // Entire field (renders as "A") - for (size_t x = 0; x < 6; x++) { - this->range[x] = 0x000FFFFF; - } - break; - case 7: // Superposition of 4 and 5 (unused) - this->range[2] = 0x00000100; - this->range[3] = 0x00001110; - this->range[4] = 0x00001010; - this->range[5] = 0x00001110; - break; - case 8: // All 8 cells around FC and FC's cell - this->range[3] = 0x00001110; - this->range[4] = 0x00001110; - this->range[5] = 0x00001110; - break; - case 9: // No cells - break; - // The table in the DOL file only appears to contain 9 entries; there are - // some pointers immediately after. So probably if a card specified A-F, - // its range would be filled in with garbage in the original game. - default: - throw runtime_error("invalid fixed range index"); - } - } -} - -string name_for_rarity(uint8_t rarity) { - static const vector names({ - "N1", - "R1", - "S", - "E", - "N2", - "N3", - "N4", - "R2", - "R3", - "R4", - "SS", - "D1", - "D2", - "INVIS", - }); - try { - return names.at(rarity - 1); - } catch (const out_of_range&) { - return string_printf("(%02hhX)", rarity); - } -} - -string name_for_target_mode(uint8_t target_mode) { - static const vector names({ - "NONE", - "SINGLE", - "MULTI", - "SELF", - "TEAM", - "ALL", - "MULTI-ALLY", - "ALL-ALLY", - "ALL-ATTACK", - "OWN-FCS", - }); - try { - return names.at(target_mode); - } catch (const out_of_range&) { - return string_printf("(%02hhX)", target_mode); - } -} - -string string_for_colors(const parray& colors) { - string ret; - for (size_t x = 0; x < 8; x++) { - if (colors[x]) { - ret += '0' + colors[x]; - } - } - if (ret.empty()) { - return "none"; - } - return ret; -} - -string string_for_assist_turns(uint8_t turns) { - if (turns == 90) { - return "ONCE"; - } else if (turns == 99) { - return "FOREVER"; - } else { - return string_printf("%hhu", turns); - } -} - -string string_for_range(const parray& range) { - string ret; - for (size_t x = 0; x < 6; x++) { - ret += string_printf("%05" PRIX32 "/", range[x].load()); - } - while (starts_with(ret, "00000/")) { - ret = ret.substr(6); - } - if (!ret.empty()) { - ret.resize(ret.size() - 1); - } - return ret; -} - -string Ep3CardDefinition::str() const { - string type_str; - try { - type_str = name_for_card_type.at(this->type); - } catch (const out_of_range&) { - type_str = string_printf("%02hhX", this->type); - } - string rarity_str = name_for_rarity(this->rarity); - string target_mode_str = name_for_target_mode(this->target_mode); - string range_str = string_for_range(this->range); - string assist_turns_str = string_for_assist_turns(this->assist_turns); - string hp_str = this->hp.str(); - string ap_str = this->ap.str(); - string tp_str = this->tp.str(); - string mv_str = this->mv.str(); - string left_str = string_for_colors(this->left_colors); - string right_str = string_for_colors(this->right_colors); - string top_str = string_for_colors(this->top_colors); - string effects_str; - for (size_t x = 0; x < 3; x++) { - if (this->effects[x].is_empty()) { - continue; - } - if (!effects_str.empty()) { - effects_str += ", "; - } - effects_str += this->effects[x].str(); - } - return string_printf( - "[Card: %04" PRIX32 " name=%s type=%s-%02hhX rare=%s cost=%hhX+%hhX " - "target=%s range=%s assist_turns=%s cannot_move=%s cannot_attack=%s " - "hidden=%s hp=%s ap=%s tp=%s mv=%s left=%s right=%s top=%s a2=%04hX " - "a3=%04hX assist_effect=[%hu, %hu] drop_rates=[%hu, %hu] effects=[%s]]", - this->card_id.load(), - this->name.data(), - type_str.c_str(), - this->subtype, - rarity_str.c_str(), - this->self_cost, - this->ally_cost, - target_mode_str.c_str(), - range_str.c_str(), - assist_turns_str.c_str(), - this->cannot_move ? "true" : "false", - this->cannot_attack ? "true" : "false", - this->hide_in_deck_edit ? "true" : "false", - hp_str.c_str(), - ap_str.c_str(), - tp_str.c_str(), - mv_str.c_str(), - left_str.c_str(), - right_str.c_str(), - top_str.c_str(), - this->unknown_a2.load(), - this->unknown_a3.load(), - this->assist_effect[0].load(), - this->assist_effect[1].load(), - this->drop_rates[0].load(), - this->drop_rates[1].load(), - effects_str.c_str()); -} - - - -Ep3DataIndex::Ep3DataIndex(const string& directory, bool debug) - : debug(debug) { - - unordered_map> card_tags; - unordered_map card_text; - if (this->debug) { - try { - string data = prs_decompress(load_file(directory + "/cardtext.mnr")); - StringReader r(data); - - while (!r.eof()) { - uint32_t card_id = stoul(r.get_cstr()); - - // Read all pages for this card - string text; - string first_page; - for (;;) { - string line = r.get_cstr(); - if (line.empty()) { - break; - } - if (first_page.empty()) { - first_page = line; - } - text += '\n'; - text += line; - } - - // In orig_text, turn all \t into $ (following newserv conventions) - string orig_text = text; - for (char& ch : orig_text) { - if (ch == '\t') { - ch = '$'; - } - } - - // Preprocess first page: first, delete all color markers - size_t offset = first_page.find("\tC"); - while (offset != string::npos) { - first_page = first_page.substr(0, offset) + first_page.substr(offset + 3); - offset = first_page.find("\tC"); - } - // Preprocess first page: delete all lines that don't start with \t - offset = first_page.find('\t'); - if (offset == string::npos) { - first_page.clear(); - } else { - first_page = first_page.substr(offset); - } - // Preprocess first page: merge lines that don't begin with \t - for (offset = 0; offset < first_page.size(); offset++) { - if (first_page[offset] == '\n' && first_page[offset + 1] != '\t') { - first_page = first_page.substr(0, offset) + first_page.substr(offset + 1); - offset--; - } - } - - // Split first page into tags, and collapse whitespace in the tag names - vector tags; - auto lines = split(first_page, '\n'); - for (const auto& line : lines) { - string tag; - if (line[0] == '\t' && line[1] == 'D') { - tag = "D: " + line.substr(2); - } else if (line[0] == '\t' && line[1] == 'S') { - tag = "S: " + line.substr(2); - } - if (!tag.empty()) { - for (size_t offset = tag.find(" "); offset != string::npos; offset = tag.find(" ")) { - tag = tag.substr(0, offset) + tag.substr(offset + 1); - } - tags.emplace_back(move(tag)); - } - } - - if (!card_text.emplace(card_id, move(orig_text)).second) { - throw runtime_error("duplicate card text id"); - } - if (!card_tags.emplace(card_id, move(tags)).second) { - throw logic_error("duplicate card tags id"); - } - - r.go((r.where() + 0x3FF) & (~0x3FF)); - } - - } catch (const exception& e) { - static_game_data_log.warning("Failed to load card text: %s", e.what()); - } - } - - try { - this->compressed_card_definitions = load_file(directory + "/cardupdate.mnr"); - string data = prs_decompress(this->compressed_card_definitions); - // There's a footer after the card definitions, but we ignore it - if (data.size() % sizeof(Ep3CardDefinition) != sizeof(Ep3CardDefinitionsFooter)) { - throw runtime_error(string_printf( - "decompressed card update file size %zX is not aligned with card definition size %zX (%zX extra bytes)", - data.size(), sizeof(Ep3CardDefinition), data.size() % sizeof(Ep3CardDefinition))); - } - const auto* def = reinterpret_cast(data.data()); - size_t max_cards = data.size() / sizeof(Ep3CardDefinition); - for (size_t x = 0; x < max_cards; x++) { - // The last card entry has the build date and some other metadata (and - // isn't a real card, obviously), so skip it. Seems like the card ID is - // always a large number that won't fit in a uint16_t, so we use that to - // determine if the entry is a real card or not. - if (def[x].card_id & 0xFFFF0000) { - continue; - } - shared_ptr entry(new CardEntry({def[x], {}, {}})); - if (!this->card_definitions.emplace(entry->def.card_id, entry).second) { - throw runtime_error(string_printf( - "duplicate card id: %08" PRIX32, entry->def.card_id.load())); - } - - entry->def.hp.decode_code(); - entry->def.ap.decode_code(); - entry->def.tp.decode_code(); - entry->def.mv.decode_code(); - entry->def.decode_range(); - - if (this->debug) { - try { - entry->text = move(card_text.at(def[x].card_id)); - } catch (const out_of_range&) { } - try { - entry->debug_tags = move(card_tags.at(def[x].card_id)); - } catch (const out_of_range&) { } - } - } - - static_game_data_log.info("Indexed %zu Episode 3 card definitions", this->card_definitions.size()); - } catch (const exception& e) { - 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 entry; - - if (ends_with(filename, ".mnmd")) { - entry.reset(new MapEntry(load_object_file(directory + "/" + filename))); - } else if (ends_with(filename, ".mnm")) { - entry.reset(new MapEntry(load_file(directory + "/" + filename))); - } - - if (entry.get()) { - if (!this->maps.emplace(entry->map.map_number, entry).second) { - throw runtime_error("duplicate map number"); - } - string name = entry->map.name; - 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()); - } - } -} - -Ep3DataIndex::MapEntry::MapEntry(const Ep3Map& map) : map(map) { } - -Ep3DataIndex::MapEntry::MapEntry(const string& compressed) - : compressed_data(compressed) { - string decompressed = prs_decompress(this->compressed_data); - if (decompressed.size() != sizeof(Ep3Map)) { - throw runtime_error(string_printf( - "decompressed data size is incorrect (expected %zu bytes, read %zu bytes)", - sizeof(Ep3Map), decompressed.size())); - } - this->map = *reinterpret_cast(decompressed.data()); -} - -string Ep3DataIndex::MapEntry::compressed() const { - if (this->compressed_data.empty()) { - this->compressed_data = prs_compress(&this->map, sizeof(this->map)); - } - return this->compressed_data; -} - -const string& Ep3DataIndex::get_compressed_card_definitions() const { - if (this->compressed_card_definitions.empty()) { - throw runtime_error("card definitions are not available"); - } - return this->compressed_card_definitions; -} - -shared_ptr Ep3DataIndex::get_card_definition( - uint32_t id) const { - return this->card_definitions.at(id); -} - -std::set Ep3DataIndex::all_card_ids() const { - std::set ret; - for (const auto& it : this->card_definitions) { - ret.emplace(it.first); - } - return ret; -} - -const string& Ep3DataIndex::get_compressed_map_list() const { - if (this->compressed_map_list.empty()) { - // TODO: Write a version of prs_compress that takes iovecs (or something - // similar) so we can eliminate all this string copying here. - StringWriter entries_w; - StringWriter strings_w; - - for (const auto& map_it : this->maps) { - Ep3MapList::Entry e; - const auto& map = map_it.second->map; - e.map_x = map.map_x; - e.map_y = map.map_y; - e.scene_data2 = map.scene_data2; - e.map_number = map.map_number.load(); - e.width = map.width; - e.height = map.height; - e.map_tiles = map.map_tiles; - e.modification_tiles = map.modification_tiles; - - e.name_offset = strings_w.size(); - strings_w.write(map.name.data(), map.name.len()); - strings_w.put_u8(0); - e.location_name_offset = strings_w.size(); - strings_w.write(map.location_name.data(), map.location_name.len()); - strings_w.put_u8(0); - e.quest_name_offset = strings_w.size(); - strings_w.write(map.quest_name.data(), map.quest_name.len()); - strings_w.put_u8(0); - e.description_offset = strings_w.size(); - strings_w.write(map.description.data(), map.description.len()); - strings_w.put_u8(0); - - e.unknown_a2 = 0xFF000000; - - entries_w.put(e); - } - - Ep3MapList header; - header.num_maps = this->maps.size(); - header.unknown_a1 = 0; - header.strings_offset = entries_w.size(); - header.total_size = sizeof(Ep3MapList) + entries_w.size() + strings_w.size(); - - PRSCompressor prs; - prs.add(&header, sizeof(header)); - prs.add(entries_w.str()); - prs.add(strings_w.str()); - - StringWriter compressed_w; - 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()); - } - return this->compressed_map_list; -} - -shared_ptr Ep3DataIndex::get_map(uint32_t id) const { - return this->maps.at(id); -} - -std::set Ep3DataIndex::all_map_ids() const { - std::set ret; - for (const auto& it : this->maps) { - ret.emplace(it.first); - } - return ret; -} diff --git a/src/Episode3.hh b/src/Episode3.hh deleted file mode 100644 index 344f15a7..00000000 --- a/src/Episode3.hh +++ /dev/null @@ -1,374 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -#include "Text.hh" - - - -// Note: Much of the structures and enums here are based on the card list file, -// and comparing the card text with the data in the file. Some inferences may be -// incorrect here, since Episode 3's card text is wrong in various places. - -struct Ep3CardDefinition { - enum Rarity : uint8_t { - N1 = 0x01, - R1 = 0x02, - S = 0x03, - E = 0x04, - N2 = 0x05, - N3 = 0x06, - N4 = 0x07, - R2 = 0x08, - R3 = 0x09, - R4 = 0x0A, - SS = 0x0B, - D1 = 0x0C, - D2 = 0x0D, - INVIS = 0x0E, - }; - - enum Type : uint8_t { - SC_HUNTERS = 0x00, // No subtypes - SC_ARKZ = 0x01, // No subtypes - ITEM = 0x02, // Subtype 01 = sword, 02 = gun, 03 = cane. TODO: there are many more subtypes than those 3 - CREATURE = 0x03, // No subtypes (TODO: Where are attributes stored then?) - ACTION = 0x04, // TODO: What do the subtypes mean? Are they actually flags instead? - ASSIST = 0x05, // No subtypes - }; - - struct Stat { - enum Type : uint8_t { - BLANK = 0, - STAT = 1, - PLUS_STAT = 2, - MINUS_STAT = 3, - EQUALS_STAT = 4, - UNKNOWN = 5, - PLUS_UNKNOWN = 6, - MINUS_UNKNOWN = 7, - EQUALS_UNKNOWN = 8, - }; - be_uint16_t code; - Type type; - int8_t stat; - - void decode_code(); - std::string str() const; - } __attribute__((packed)); - - struct Effect { - uint8_t effect_num; - uint8_t command; // See name_for_effect_command in Episode3.cc for details - ptext expr; // May be blank if the command doesn't use it - uint8_t when; // See description_for_when in Episode3.cc for details - ptext arg1; - ptext arg2; - ptext arg3; - parray unknown_a3; - - bool is_empty() const; - static std::string str_for_arg(const std::string& arg); - std::string str() const; - } __attribute__((packed)); - - be_uint32_t card_id; - parray jp_name; - int8_t type; // Type enum. If <0, then this is the end of the card list - uint8_t self_cost; // ATK dice points required - uint8_t ally_cost; // ATK points from allies required; PBs use this - uint8_t unused_a0; // Always 0 - Stat hp; - Stat ap; - Stat tp; - Stat mv; - parray left_colors; - parray right_colors; - parray top_colors; - parray range; - be_uint32_t unused_a1; // Always 0 - // Target modes: - // 00 = no targeting (used for defense cards, mags, shields, etc.) - // 01 = single enemy - // 02 = multiple enemies (with range) - // 03 = self (assist) - // 04 = team (assist) - // 05 = everyone (assist) - // 06 = multiple allies (with range); only used by Shifta - // 07 = all allies including yourself; see Anti, Resta, Leilla - // 08 = all (attack); see e.g. Last Judgment, Earthquake - // 09 = your own FCs but not SCs; see Traitor - uint8_t target_mode; - uint8_t assist_turns; // 90 (dec) = once, 99 (dec) = forever - uint8_t cannot_move; // 0 for SC and creature cards; 1 for everything else - uint8_t cannot_attack; // 1 for shields, mags, defense actions, and assist cards - uint8_t unused_a2; // Always 0 - uint8_t hide_in_deck_edit; // 0 = player can use this card (appears in deck edit) - uint8_t subtype; // e.g. gun, sword, etc. (used for checking if SCs can use it) - uint8_t rarity; // Rarity enum - be_uint16_t unknown_a2; - be_uint16_t unknown_a3; - // These two fields seem to always contain the same value, and are always 0 - // for non-assist cards and nonzero for assists. Each assist card has a unique - // value here and no effects, which makes it look like this is how assist - // effects are implemented. There seems to be some 1k-modulation going on here - // too; most cards are in the range 101-174 but a few have e.g. 1150, 2141. A - // few pairs of cards have the same effect, which makes it look like some - // other fields are also involved in determining their effects (see e.g. Skip - // Draw / Skip Move, Dice Fever / Dice Fever +, Reverse Card / Rich +). - parray assist_effect; - // Drop rates are decimal-encoded with the following fields: - // - rate % 10 (that is, the lowest decimal place) specifies the required game - // mode. 0 means any mode, 1 means offline only, 2 means 1P free-battle, 3 - // means 2P+ free battle, 4 means story mode. - // - (rate / 10) % 100 (that is, the tens and hundreds decimal places) specify - // something else, but it's not clear what exactly. - // - rate / 1000 (the thousands decimal place) specifies the level class - // required to get this drop. - // - rate / 10000 (the ten-thousands decimal place) must be either 0, 1, or 2, - // but it's not clear yet what each value means. - // The drop rates are completely ignored if any of the following are true - // (which means the card can never be found in a normal post-battle draw): - // - type is SC_HUNTERS or SC_ARKZ - // - unknown_a3 is 0x23 or 0x24 - // - rarity is E, D1, D2, or INVIS - // - hide_in_deck_edit is 1 (specifically 1; other nonzero values here don't - // prevent the card from appearing in post-battle draws) - parray drop_rates; - ptext name; - ptext jp_short_name; - ptext short_name; - Effect effects[3]; - uint8_t unused_a3; - - void decode_range(); - std::string str() const; -} __attribute__((packed)); // 0x128 bytes in total - -struct Ep3CardDefinitionsFooter { - be_uint32_t num_cards1; - be_uint32_t unknown_a1; - be_uint32_t num_cards2; - be_uint32_t unknown_a2[11]; - be_uint32_t unknown_offset_a3; - be_uint32_t unknown_a4[3]; - be_uint32_t footer_offset; - be_uint32_t unknown_a5[3]; -} __attribute__((packed)); - -struct Ep3Deck { - ptext name; - be_uint32_t client_id; // 0-3 - // List of card IDs. The card count is the number of nonzero entries here - // before a zero entry (or 50 if no entries are nonzero). The first card ID is - // the SC card, which the game implicitly subtracts from the limit - so a - // valid deck should actually have 31 cards in it. - parray card_ids; - be_uint32_t unknown_a1; - // Last modification time - le_uint16_t year; - uint8_t month; - uint8_t day; - uint8_t hour; - uint8_t minute; - uint8_t second; - uint8_t unknown_a2; -} __attribute__((packed)); // 0x84 bytes in total - -struct Ep3Config { - // Offsets in comments in this struct are relative to start of 61/98 command - /* 0728 */ parray unknown_a1; - /* 1B5C */ parray 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 unknown_a3; - /* 299C */ ptext name; - // Other records are probably somewhere in here - e.g. win/loss, play time, etc. - /* 29AC */ parray unknown_a4; -} __attribute__((packed)); - -struct Ep3BattleRules { - // When this structure is used in a map/quest definition, FF in any of these - // fields means the user is allowed to override it. Any non-FF fields are - // fixed for the map/quest and cannot be overridden. - uint8_t overall_time_limit; // In increments of 5 minutes; 0 = unlimited - uint8_t phase_time_limit; // In seconds; 0 = unlimited - uint8_t allowed_cards; // 0 = any, 1 = N-rank only, 2 = N and R, 3 = N, R, and S - uint8_t min_dice; // 0 = default (1) - // 4 - uint8_t max_dice; // 0 = default (6) - uint8_t disable_deck_shuffle; // 0 = shuffle on, 1 = off - uint8_t disable_deck_loop; // 0 = loop on, 1 = off - uint8_t char_hp; - // 8 - uint8_t hp_type; // 0 = defeat player, 1 = defeat team, 2 = common hp - uint8_t no_assist_cards; // 1 = assist cards disallowed - uint8_t disable_dialogue; // 0 = dialogue on, 1 = dialogue off - uint8_t dice_exchange_mode; // 0 = high attack, 1 = high defense, 2 = none - // C - uint8_t disable_dice_boost; // 0 = dice boost on, 1 = off - parray unused; -} __attribute__((packed)); - - - -struct Ep3MapList { - be_uint32_t num_maps; - be_uint32_t unknown_a1; // Always 0? - be_uint32_t strings_offset; // From after total_size field (add 0x10 to this value) - be_uint32_t total_size; // Including header, entries, and strings - - struct Entry { // Should be 0x220 bytes in total - be_uint16_t map_x; - be_uint16_t map_y; - be_uint16_t scene_data2; - be_uint16_t map_number; - // Text offsets are from the beginning of the strings block after all map - // entries (that is, add strings_offset to them to get the string offset) - be_uint32_t name_offset; - be_uint32_t location_name_offset; - be_uint32_t quest_name_offset; - be_uint32_t description_offset; - be_uint16_t width; - be_uint16_t height; - parray map_tiles; - parray modification_tiles; - be_uint32_t unknown_a2; // Seems to always be 0xFF000000 - } __attribute__((packed)); - - // Variable-length fields: - // Entry entries[num_maps]; - // char strings[...EOF]; // Null-terminated strings, pointed to by offsets in Entry structs -} __attribute__((packed)); - -struct Ep3CompressedMapHeader { // .mnm file format - le_uint32_t map_number; - le_uint32_t compressed_data_size; - // Compressed data immediately follows (which decompresses to an Ep3Map) -} __attribute__((packed)); - -struct Ep3Map { // .mnmd format; also the format of (decompressed) Ep3 quests - /* 0000 */ be_uint32_t unknown_a1; - /* 0004 */ be_uint32_t map_number; - /* 0008 */ uint8_t width; - /* 0009 */ uint8_t height; - /* 000A */ uint8_t scene_data2; // TODO: What is this? - // All alt_maps fields (including the floats) past num_alt_maps are filled in - // with FF. For example, if num_alt_maps == 8, the last two fields in each - // alt_maps array are filled with FF. - /* 000B */ uint8_t num_alt_maps; // TODO: What are the alt maps for? - // In the map_tiles array, the values are: - // 00 = not a valid tile - // 01 = valid tile unless punched out (later) - // 02 = team A start (1v1) - // 03, 04 = team A start (2v2) - // 05 = ??? - // 06, 07 = team B start (2v2) - // 08 = team B start (1v1) - // Note that the game displays the map reversed vertically in the preview - // window. For example, player 1 is on team A, which usually starts at the top - // of the map as defined in this struct, or at the bottom as shown in the - // preview window. - /* 000C */ parray map_tiles; - /* 010C */ parray unknown_a2; - /* 0118 */ parray alt_maps1[0x0A]; - /* 0B18 */ parray alt_maps2[0x0A]; - /* 1518 */ parray alt_maps_unknown_a3[0x0A]; - /* 17E8 */ parray alt_maps_unknown_a4[0x0A]; - /* 1AB8 */ parray unknown_a5; - // In the modification_tiles array, the values are: - // 10 = blocked (as if the corresponding map_tiles value was 00) - // 20 = blocked (maybe one of 10 or 20 are passable by Aerial characters though) - // 30, 31 = teleporters (green, red) - // 40-44 = ???? (used in 244, 2E4, 2F9) - // 50 = appears as improperly-z-buffered teal cube in preview - // TODO: There may be more values that are valid here. - /* 1C68 */ parray modification_tiles; - /* 1D68 */ parray unknown_a6; - /* 1DDC */ Ep3BattleRules default_rules; - /* 1DEC */ parray unknown_a7; - /* 1DF0 */ ptext name; - /* 1E04 */ ptext location_name; - /* 1E18 */ ptext quest_name; // == location_name if not a quest - /* 1E54 */ ptext description; - /* 1FE4 */ be_uint16_t map_x; - /* 1FE6 */ be_uint16_t map_y; - struct NPCDeck { - ptext name; - parray card_ids; // Last one appears to always be FFFF - } __attribute__((packed)); - /* 1FE8 */ NPCDeck npc_decks[3]; // Unused if name[0] == 0 - struct NPCCharacter { - parray unknown_a1; - parray unknown_a2; - ptext name; - parray unknown_a3; - } __attribute__((packed)); - /* 20F0 */ NPCCharacter npc_chars[3]; // Unused if name[0] == 0 - /* 242C */ parray unknown_a8; // Always FF? - /* 2440 */ ptext before_message; - /* 25D0 */ ptext after_message; - /* 2760 */ ptext dispatch_message; // Usually "You can only dispatch " or blank - struct DialogueSet { - be_uint16_t unknown_a1; - be_uint16_t unknown_a2; // Always 0x0064 if valid, 0xFFFF if unused? - ptext strings[4]; - } __attribute__((packed)); // Total size: 0x104 bytes - /* 28F0 */ DialogueSet dialogue_sets[3][0x10]; // Up to 0x10 per valid NPC - /* 59B0 */ be_uint16_t reward_card_id; // TODO: This could be an array. The only examples I've seen have only one here - /* 59B2 */ parray unknown_a9; - /* 5A18 */ -} __attribute__((packed)); - -class Ep3DataIndex { -public: - explicit Ep3DataIndex(const std::string& directory, bool debug = false); - - struct CardEntry { - Ep3CardDefinition def; - std::string text; - std::vector debug_tags; // Empty unless debug == true - }; - - class MapEntry { - public: - Ep3Map map; - - MapEntry(const Ep3Map& map); - MapEntry(const std::string& compressed_data); - - std::string compressed() const; - - private: - mutable std::string compressed_data; - }; - - const std::string& get_compressed_card_definitions() const; - std::shared_ptr get_card_definition(uint32_t id) const; - std::set all_card_ids() const; - - const std::string& get_compressed_map_list() const; - std::shared_ptr get_map(uint32_t id) const; - std::set all_map_ids() const; - -private: - bool debug; - - std::string compressed_card_definitions; - std::unordered_map> card_definitions; - - // The compressed map list is generated on demand from the maps map below. - // It's marked mutable because the logical consistency of the Ep3DataIndex - // object is not violated from the caller's perspective even if we don't - // generate the compressed map list at load time. - mutable std::string compressed_map_list; - std::map> maps; -}; diff --git a/src/Episode3/AssistServer.cc b/src/Episode3/AssistServer.cc new file mode 100644 index 00000000..c0a1ffdc --- /dev/null +++ b/src/Episode3/AssistServer.cc @@ -0,0 +1,286 @@ +#include "AssistServer.hh" + +#include "Server.hh" + +using namespace std; + +namespace Episode3 { + + + +// Note: This order matches the order that the cards are defined in the original +// code. This is relevant for consistency of results when choosing a random card +// (for God Whim). +const vector ALL_ASSIST_CARD_IDS = { + 0x0018, 0x0019, 0x001A, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, + 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, 0x0100, 0x0101, 0x0102, 0x0103, + 0x0104, 0x0105, 0x0106, 0x0107, 0x0108, 0x0109, 0x010A, 0x010B, 0x010C, + 0x010D, 0x010E, 0x010F, 0x0121, 0x0125, 0x0126, 0x0127, 0x0128, 0x0129, + 0x012A, 0x012B, 0x012C, 0x012D, 0x012E, 0x012F, 0x0130, 0x0131, 0x0132, + 0x0133, 0x0134, 0x0135, 0x0136, 0x0137, 0x0138, 0x0139, 0x013A, 0x013B, + 0x013C, 0x013D, 0x013E, 0x013F, 0x0140, 0x0141, 0x0142, 0x0143, 0x0144, + 0x0145, 0x0146, 0x0148, 0x014A, 0x014B, 0x014C, 0x014D, 0x014E, 0x023F, + 0x0240, 0x0241, 0x0242}; + +AssistEffect assist_effect_number_for_card_id(uint16_t card_id) { + static const unordered_map card_id_to_effect({ + {0x00F5, /* 0x0001 */ AssistEffect::DICE_HALF}, + {0x00F6, /* 0x0002 */ AssistEffect::DICE_PLUS_1}, + {0x00F7, /* 0x0003 */ AssistEffect::DICE_FEVER}, + {0x00F8, /* 0x0004 */ AssistEffect::CARD_RETURN}, + {0x00F9, /* 0x0005 */ AssistEffect::LAND_PRICE}, + {0x00FA, /* 0x0006 */ AssistEffect::POWERLESS_RAIN}, + {0x00FB, /* 0x0007 */ AssistEffect::BRAVE_WIND}, + {0x00FC, /* 0x0008 */ AssistEffect::SILENT_COLOSSEUM}, + {0x00FD, /* 0x0009 */ AssistEffect::RESISTANCE}, + {0x00FE, /* 0x000A */ AssistEffect::INDEPENDENT}, + {0x00FF, /* 0x000B */ AssistEffect::ASSISTLESS}, + {0x0100, /* 0x000C */ AssistEffect::ATK_DICE_2}, + {0x0101, /* 0x000D */ AssistEffect::DEFLATION}, + {0x0102, /* 0x000E */ AssistEffect::INFLATION}, + {0x0103, /* 0x000F */ AssistEffect::EXCHANGE}, + {0x0104, /* 0x0010 */ AssistEffect::INFLUENCE}, + {0x0105, /* 0x0011 */ AssistEffect::SKIP_SET}, + {0x0106, /* 0x0012 */ AssistEffect::SKIP_MOVE}, + {0x0121, /* 0x0013 */ AssistEffect::SKIP_ACT}, + {0x0137, /* 0x0014 */ AssistEffect::SKIP_DRAW}, + {0x0107, /* 0x0015 */ AssistEffect::FLY}, + {0x0108, /* 0x0016 */ AssistEffect::NECROMANCER}, + {0x0109, /* 0x0017 */ AssistEffect::PERMISSION}, + {0x010A, /* 0x0018 */ AssistEffect::SHUFFLE_ALL}, + {0x010B, /* 0x0019 */ AssistEffect::LEGACY}, + {0x010C, /* 0x001A */ AssistEffect::ASSIST_REVERSE}, + {0x010D, /* 0x001B */ AssistEffect::STAMINA}, + {0x010E, /* 0x001C */ AssistEffect::AP_ABSORPTION}, + {0x010F, /* 0x001D */ AssistEffect::HEAVY_FOG}, + {0x0125, /* 0x001E */ AssistEffect::TRASH_1}, + {0x0126, /* 0x001F */ AssistEffect::EMPTY_HAND}, + {0x0127, /* 0x0020 */ AssistEffect::HITMAN}, + {0x0128, /* 0x0021 */ AssistEffect::ASSIST_TRASH}, + {0x0129, /* 0x0022 */ AssistEffect::SHUFFLE_GROUP}, + {0x012A, /* 0x0023 */ AssistEffect::ASSIST_VANISH}, + {0x012B, /* 0x0024 */ AssistEffect::CHARITY}, + {0x012C, /* 0x0025 */ AssistEffect::INHERITANCE}, + {0x012D, /* 0x0026 */ AssistEffect::FIX}, + {0x012E, /* 0x0027 */ AssistEffect::MUSCULAR}, + {0x012F, /* 0x0028 */ AssistEffect::CHANGE_BODY}, + {0x0130, /* 0x0029 */ AssistEffect::GOD_WHIM}, + {0x0131, /* 0x002A */ AssistEffect::GOLD_RUSH}, + {0x0132, /* 0x002B */ AssistEffect::ASSIST_RETURN}, + {0x0133, /* 0x002C */ AssistEffect::REQUIEM}, + {0x0134, /* 0x002D */ AssistEffect::RANSOM}, + {0x0135, /* 0x002E */ AssistEffect::SIMPLE}, + {0x0136, /* 0x002F */ AssistEffect::SLOW_TIME}, + {0x023F, /* 0x0030 */ AssistEffect::QUICK_TIME}, + {0x0138, /* 0x0031 */ AssistEffect::TERRITORY}, + {0x0139, /* 0x0032 */ AssistEffect::OLD_TYPE}, + {0x013A, /* 0x0033 */ AssistEffect::FLATLAND}, + {0x013B, /* 0x0034 */ AssistEffect::IMMORTALITY}, + {0x013C, /* 0x0035 */ AssistEffect::SNAIL_PACE}, + {0x013D, /* 0x0036 */ AssistEffect::TECH_FIELD}, + {0x013E, /* 0x0037 */ AssistEffect::FOREST_RAIN}, + {0x013F, /* 0x0038 */ AssistEffect::CAVE_WIND}, + {0x0140, /* 0x0039 */ AssistEffect::MINE_BRIGHTNESS}, + {0x0141, /* 0x003A */ AssistEffect::RUIN_DARKNESS}, + {0x0142, /* 0x003B */ AssistEffect::SABER_DANCE}, + {0x0143, /* 0x003C */ AssistEffect::BULLET_STORM}, + {0x0144, /* 0x003D */ AssistEffect::CANE_PALACE}, + {0x0145, /* 0x003E */ AssistEffect::GIANT_GARDEN}, + {0x0146, /* 0x003F */ AssistEffect::MARCH_OF_THE_MEEK}, + {0x0148, /* 0x0040 */ AssistEffect::SUPPORT}, + {0x014A, /* 0x0041 */ AssistEffect::RICH}, + {0x014B, /* 0x0042 */ AssistEffect::REVERSE_CARD}, + {0x014C, /* 0x0043 */ AssistEffect::VENGEANCE}, + {0x014D, /* 0x0044 */ AssistEffect::SQUEEZE}, + {0x014E, /* 0x0045 */ AssistEffect::HOMESICK}, + {0x0240, /* 0x0046 */ AssistEffect::BOMB}, + {0x0241, /* 0x0047 */ AssistEffect::SKIP_TURN}, + {0x0242, /* 0x0048 */ AssistEffect::BATTLE_ROYALE}, + {0x0018, /* 0x0049 */ AssistEffect::DICE_FEVER_PLUS}, + {0x0019, /* 0x004A */ AssistEffect::RICH_PLUS}, + {0x001A, /* 0x004B */ AssistEffect::CHARITY_PLUS}, + }); + try { + return card_id_to_effect.at(card_id); + } catch (const out_of_range&) { + return AssistEffect::NONE; + } +} + + + +AssistServer::AssistServer(shared_ptr server) + : w_server(server), + assist_effects(AssistEffect::NONE), + num_assist_cards_set(0), + client_ids_with_assists(0xFF), + active_assist_effects(AssistEffect::NONE), + num_active_assists(0) { } + +shared_ptr AssistServer::server() { + auto s = this->w_server.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +shared_ptr AssistServer::server() const { + auto s = this->w_server.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +uint16_t AssistServer::card_id_for_card_ref(uint16_t card_ref) const { + return this->server()->card_id_for_card_ref(card_ref); +} + +shared_ptr AssistServer::definition_for_card_id( + uint16_t card_id) const { + return this->server()->definition_for_card_id(card_id); +} + +uint32_t AssistServer::compute_num_assist_effects_for_client(uint16_t client_id) { + this->populate_effects(); + this->num_assist_cards_set = 0; + if (this->should_block_assist_effects_for_client(client_id)) { + this->num_active_assists = 0; + return 0; + } + + for (size_t z = 0; z < 4; z++) { + auto ce = this->assist_card_defs[z]; + auto hes = this->hand_and_equip_states[z]; + if (ce && (!hes || (hes->assist_delay_turns < 1))) { + bool affected = false; + if (ce->def.target_mode == TargetMode::TEAM) { + auto this_deck_entry = this->deck_entries[client_id]; + auto other_deck_entry = this->deck_entries[z]; + if (this_deck_entry && other_deck_entry && + (this_deck_entry->team_id == other_deck_entry->team_id)) { + affected = true; + } + } else if ((ce->def.target_mode == TargetMode::SELF) && (z == client_id)) { + affected = true; + } else if (ce->def.target_mode == TargetMode::EVERYONE) { + affected = true; + } + if (affected) { + this->client_ids_with_assists[this->num_assist_cards_set++] = z; + } + } + } + + this->recompute_effects(); + return this->num_assist_cards_set; +} + +uint32_t AssistServer::compute_num_assist_effects_for_team(uint32_t team_id) { + this->num_assist_cards_set = 0; + for (size_t z = 0; z < 4; z++) { + auto ce = this->assist_card_defs[z]; + auto hes = this->hand_and_equip_states[z]; + if (ce && (!hes || (hes->assist_delay_turns < 1))) { + bool affected = false; + if (ce->def.target_mode == TargetMode::TEAM) { + if (this->deck_entries[z] && (this->deck_entries[z]->team_id == team_id)) { + affected = true; + } + } else if (ce->def.target_mode == TargetMode::EVERYONE) { + affected = true; + } + if (affected) { + this->client_ids_with_assists[this->num_assist_cards_set++] = z; + } + } + } + this->recompute_effects(); + return this->num_assist_cards_set; +} + +bool AssistServer::should_block_assist_effects_for_client(uint16_t client_id) const { + for (size_t z = 0; z < 4; z++) { + auto eff = this->assist_effects[z]; + auto ce = this->assist_card_defs[z]; + if (((eff == AssistEffect::RESISTANCE) || (eff == AssistEffect::INDEPENDENT)) && ce) { + if (ce->def.target_mode == TargetMode::TEAM) { + if (this->deck_entries[client_id] && this->deck_entries[z] && + (this->deck_entries[client_id]->team_id == this->deck_entries[z]->team_id)) { + return true; + } + } else if ((ce->def.target_mode == TargetMode::SELF) && (client_id == z)) { + return true; + } else if (ce->def.target_mode == TargetMode::EVERYONE) { + return true; + } + } + } + return false; +} + +AssistEffect AssistServer::get_active_assist_by_index(size_t index) const { + if (index < this->num_active_assists) { + return this->active_assist_effects[index]; + } + return AssistEffect::NONE; +} + +void AssistServer::populate_effects() { + for (size_t z = 0; z < 4; z++) { + this->assist_card_defs[z] = nullptr; + this->assist_effects[z] = AssistEffect::NONE; + 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) + : hes->assist_card_id.load(); + this->assist_effects[z] = assist_effect_number_for_card_id(card_id); + if (this->assist_effects[z] != AssistEffect::NONE) { + this->assist_card_defs[z] = this->definition_for_card_id(card_id); + } + } + } +} + +void AssistServer::recompute_effects() { + for (size_t z = 0; z < 4; z++) { + this->active_assist_effects[z] = AssistEffect::NONE; + this->active_assist_card_defs[z] = nullptr; + } + this->num_active_assists = 0; + + if (this->num_assist_cards_set != 0) { + for (size_t z = 0; z < this->num_assist_cards_set; z++) { + auto eff = this->assist_effects[this->client_ids_with_assists[z]]; + if (eff == AssistEffect::RESISTANCE || eff == AssistEffect::INDEPENDENT) { + return; + } + } + + // Note: this->num_assist_cards_set is > 0 when we get here + for (size_t z = 0; z < this->num_assist_cards_set - 1; z++) { + for (size_t w = z + 1; w < this->num_assist_cards_set; w++) { + uint8_t z_client_id = this->client_ids_with_assists[z]; + uint8_t w_client_id = this->client_ids_with_assists[w]; + if (this->hand_and_equip_states[w_client_id]->assist_card_set_number < + this->hand_and_equip_states[z_client_id]->assist_card_set_number) { + this->client_ids_with_assists[z] = w_client_id; + this->client_ids_with_assists[w] = z_client_id; + } + } + } + + this->num_active_assists = this->num_assist_cards_set; + for (size_t z = 0; z < this->num_assist_cards_set; z++) { + this->active_assist_effects[z] = this->assist_effects[this->client_ids_with_assists[z]]; + this->active_assist_card_defs[z] = this->assist_card_defs[this->client_ids_with_assists[z]]; + } + } + return; +} + + + +} // namespace Episode3 diff --git a/src/Episode3/AssistServer.hh b/src/Episode3/AssistServer.hh new file mode 100644 index 00000000..cbbcf061 --- /dev/null +++ b/src/Episode3/AssistServer.hh @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include +#include + +#include "DataIndex.hh" +#include "PlayerState.hh" +#include "DeckState.hh" + +namespace Episode3 { + + + +class Server; + +extern const std::vector ALL_ASSIST_CARD_IDS; + +AssistEffect assist_effect_number_for_card_id(uint16_t card_id); + +class AssistServer { +public: + explicit AssistServer(std::shared_ptr server); + std::shared_ptr server(); + std::shared_ptr server() const; + + uint16_t card_id_for_card_ref(uint16_t card_ref) const; + std::shared_ptr definition_for_card_id(uint16_t card_id) const; + + uint32_t compute_num_assist_effects_for_client(uint16_t client_id); + uint32_t compute_num_assist_effects_for_team(uint32_t team_id); + + bool should_block_assist_effects_for_client(uint16_t client_id) const; + AssistEffect get_active_assist_by_index(size_t index) const; + + void populate_effects(); + void recompute_effects(); + +private: + std::weak_ptr w_server; + +public: + parray assist_effects; + std::shared_ptr assist_card_defs[4]; + uint32_t num_assist_cards_set; + parray client_ids_with_assists; + parray active_assist_effects; + std::shared_ptr active_assist_card_defs[4]; + uint32_t num_active_assists; + std::shared_ptr hand_and_equip_states[4]; + std::shared_ptr> card_short_statuses[4]; + std::shared_ptr deck_entries[4]; + std::shared_ptr> set_card_action_chains[4]; + std::shared_ptr> set_card_action_metadatas[4]; +}; + + + +} // namespace Episode3 diff --git a/src/Episode3/Card.cc b/src/Episode3/Card.cc new file mode 100644 index 00000000..ff224734 --- /dev/null +++ b/src/Episode3/Card.cc @@ -0,0 +1,1235 @@ +#include "Card.hh" + +#include "Server.hh" +#include "../CommandFormats.hh" + +using namespace std; + +namespace Episode3 { + + + +Card::Card( + uint16_t card_id, + uint16_t card_ref, + uint16_t client_id, + shared_ptr server) + : w_server(server), + w_player_state(server->get_player_state(client_id)), + client_id(client_id), + card_id(card_id), + card_ref(card_ref), + card_flags(0), + loc(0, 0, Direction::RIGHT), + facing_direction(Direction::INVALID_FF), + action_chain(), + action_metadata(), + num_ally_fcs_destroyed_at_set_time(0), + num_cards_destroyed_by_team_at_set_time(0), + unknown_a9(1), + last_attack_preliminary_damage(0), + last_attack_final_damage(0), + num_destroyed_ally_fcs(0), + current_defense_power(0) { } + +void Card::init() { + this->clear_action_chain_and_metadata_and_most_flags(); + this->team_id = this->player_state()->get_team_id(); + this->def_entry = this->server()->definition_for_card_id(this->card_id); + if (!this->def_entry) { + // The original implementation replaces the card ID and definition with 0009 + // (Saber) if the SC is Hunters-side, and 0056 (Booma) if the SC is + // Arkz-side. This could break things later on in the battle, and even if it + // doesn't, it certainly isn't behavior that the player would expect, so we + // prevent it instead. + throw runtime_error("card definition is missing"); + } + this->sc_card_ref = this->player_state()->get_sc_card_ref(); + this->sc_def_entry = this->server()->definition_for_card_id( + this->player_state()->get_sc_card_id()); + this->sc_card_type = this->player_state()->get_sc_card_type(); + this->max_hp = this->def_entry->def.hp.stat; + this->current_hp = this->def_entry->def.hp.stat; + if (this->sc_card_ref == this->card_ref) { + int16_t rules_char_hp = this->server()->base()->map_and_rules1->rules.char_hp; + int16_t base_char_hp = (rules_char_hp == 0) ? 15 : rules_char_hp; + int16_t hp = clamp(base_char_hp + this->def_entry->def.hp.stat, 1, 99); + this->max_hp = hp; + this->current_hp = hp; + } + this->ap = this->def_entry->def.ap.stat; + this->tp = this->def_entry->def.tp.stat; + this->num_ally_fcs_destroyed_at_set_time = this->server()->team_num_ally_fcs_destroyed[this->team_id]; + this->num_cards_destroyed_by_team_at_set_time = this->server()->team_num_cards_destroyed[this->team_id]; + this->action_chain.chain.card_ap = this->ap; + this->action_chain.chain.card_tp = this->tp; + this->loc.direction = this->player_state()->start_facing_direction; + if (this->sc_card_ref != this->card_ref) { + this->send_6xB4x4E_4C_4D_if_needed(); + } +} + +shared_ptr Card::server() { + auto s = this->w_server.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +shared_ptr Card::server() const { + auto s = this->w_server.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +shared_ptr Card::player_state() { + auto s = this->w_player_state.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +shared_ptr Card::player_state() const { + auto s = this->w_player_state.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +ssize_t Card::apply_abnormal_condition( + const CardDefinition::Effect& eff, + uint8_t def_effect_index, + uint16_t target_card_ref, + uint16_t sc_card_ref, + int16_t value, + int8_t dice_roll_value, + int8_t random_percent) { + + ssize_t existing_cond_index; + for (size_t z = 0; z < this->action_chain.conditions.size(); z++) { + const auto& cond = this->action_chain.conditions[z]; + if (cond.type == eff.type) { + existing_cond_index = z; + if (eff.type == ConditionType::MV_BONUS || + ((cond.card_definition_effect_index == def_effect_index) && + (cond.card_ref == target_card_ref))) { + break; + } + } else { + existing_cond_index = -1; + } + } + + ssize_t cond_index = existing_cond_index; + if (existing_cond_index < 0) { + cond_index = existing_cond_index; + for (size_t z = 0; z < this->action_chain.conditions.size(); z++) { + if (this->action_chain.conditions[z].type == ConditionType::NONE) { + cond_index = z; + break; + } + } + } + + if (cond_index < 0) { + return -1; + } + + int16_t existing_cond_value = 0; + auto& cond = this->action_chain.conditions[cond_index]; + if ((eff.type == ConditionType::MV_BONUS) && (cond.type == ConditionType::MV_BONUS)) { + existing_cond_value = clamp(cond.value, -99, 99); + } + + this->server()->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond( + cond, this->shared_from_this()); + cond.type = eff.type; + cond.card_ref = target_card_ref; + cond.condition_giver_card_ref = sc_card_ref; + cond.card_definition_effect_index = def_effect_index; + cond.order = 10; + if (dice_roll_value < 0) { + cond.dice_roll_value = this->player_state()->roll_dice_with_effects(1); + } else { + cond.dice_roll_value = dice_roll_value; + } + cond.flags = 0; + cond.value = value + existing_cond_value; + cond.value8 = value + existing_cond_value; + cond.random_percent = random_percent; + + switch (eff.arg1[0]) { + case 'a': + cond.a_arg_value = atoi(&eff.arg1[1]); + break; + case 'e': + cond.remaining_turns = 99; + break; + case 'f': + cond.remaining_turns = 100; + break; + case 'r': + cond.remaining_turns = 102; + break; + case 't': + cond.remaining_turns = atoi(&eff.arg1[1]); + } + + this->server()->card_special->update_condition_orders(this->shared_from_this()); + return cond_index; +} + +void Card::apply_ap_adjust_assists_to_attack( + shared_ptr attacker_card, + int16_t* inout_attacker_ap, + int16_t* in_defense_power) const { + uint8_t client_id = attacker_card->get_client_id(); + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->server()->assist_server->get_active_assist_by_index(z); + if ((eff == AssistEffect::FIX) && + attacker_card && + (attacker_card->def_entry->def.type != CardType::HUNTERS_SC) && + (attacker_card->def_entry->def.type != CardType::ARKZ_SC)) { + *inout_attacker_ap = 2; + } else if ((eff == AssistEffect::SILENT_COLOSSEUM) && + (*inout_attacker_ap - *in_defense_power >= 7)) { + *inout_attacker_ap = 0; + } + } + + num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->server()->assist_server->get_active_assist_by_index(z); + if ((eff == AssistEffect::AP_ABSORPTION) && + (attacker_card->action_chain.chain.attack_medium == AttackMedium::PHYSICAL)) { + *inout_attacker_ap = 0; + } + } +} + +bool Card::card_type_is_sc_or_creature() const { + return (this->def_entry->def.type == CardType::HUNTERS_SC) || + (this->def_entry->def.type == CardType::ARKZ_SC) || + (this->def_entry->def.type == CardType::CREATURE); +} + +bool Card::check_card_flag(uint32_t flags) const { + return this->card_flags & flags; +} + +void Card::commit_attack( + int16_t damage, + shared_ptr attacker_card, + G_ApplyConditionEffect_GC_Ep3_6xB4x06* cmd, + size_t strike_number, + int16_t* out_effective_damage) { + int16_t effective_damage = damage; + this->server()->card_special->adjust_attack_damage_due_to_conditions( + this->shared_from_this(), &effective_damage, attacker_card->get_card_ref()); + + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->server()->assist_server->get_active_assist_by_index(z); + if ((eff == AssistEffect::RANSOM) && + (attacker_card->action_chain.chain.attack_medium == AttackMedium::PHYSICAL)) { + uint8_t team_id = this->player_state()->get_team_id(); + int16_t exp_amount = clamp(this->server()->team_exp[team_id], 0, effective_damage); + this->server()->team_exp[team_id] -= exp_amount; + effective_damage -= exp_amount; + this->server()->compute_team_dice_boost(team_id); + this->server()->update_battle_state_flags_and_send_6xB4x03_if_needed(); + if (cmd) { + cmd->effect.ap += exp_amount; + } + } + } + + if (this->action_metadata.check_flag(0x10)) { + effective_damage = 0; + } + + + auto attacker_ps = attacker_card->player_state(); + attacker_ps->stats.damage_given += effective_damage; + this->player_state()->stats.damage_taken += effective_damage; + + this->current_hp = clamp( + this->current_hp - effective_damage, 0, this->max_hp); + + if ((effective_damage > 0) && + (attacker_ps->stats.max_attack_damage < effective_damage)) { + attacker_ps->stats.max_attack_damage = effective_damage; + } + + this->last_attack_final_damage = effective_damage; + if (effective_damage > 0) { + this->card_flags = this->card_flags | 4; + } + if (this->current_hp < 1) { + this->destroy_set_card(attacker_card); + } + + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd_to_send; + if (cmd) { + cmd_to_send = *cmd; + } + cmd_to_send.effect.flags = (strike_number == 0) ? 0x11 : 0x01; + cmd_to_send.effect.attacker_card_ref = attacker_card->card_ref; + cmd_to_send.effect.target_card_ref = this->card_ref; + cmd_to_send.effect.value = effective_damage; + this->server()->send(cmd_to_send); + + this->propagate_shared_hp_if_needed(); + + if ((this->def_entry->def.type == CardType::HUNTERS_SC) || + (this->def_entry->def.type == CardType::ARKZ_SC)) { + this->player_state()->stats.sc_damage_taken += effective_damage; + } + + if (out_effective_damage) { + *out_effective_damage = effective_damage; + } +} + +int16_t Card::compute_defense_power_for_attacker_card( + shared_ptr attacker_card) { + if (!attacker_card) { + return 0; + } + + this->action_metadata.defense_power = 0; + this->action_metadata.defense_bonus = 0; + + for (size_t z = 0; z < this->action_metadata.defense_card_ref_count; z++) { + if (attacker_card->card_ref != this->action_metadata.original_attacker_card_refs[z]) { + continue; + } + auto ce = this->server()->definition_for_card_ref( + this->action_metadata.defense_card_refs[z]); + if (ce) { + this->action_metadata.defense_power += ce->def.hp.stat; + } + } + + this->server()->card_special->apply_action_conditions( + 3, attacker_card, this->shared_from_this(), 0x08, nullptr); + this->server()->card_special->apply_action_conditions( + 3, attacker_card, this->shared_from_this(), 0x10, nullptr); + return this->action_metadata.defense_power + this->action_metadata.defense_bonus; +} + +void Card::destroy_set_card(shared_ptr attacker_card) { + this->current_hp = 0; + if (!(this->card_flags & 2)) { + if (!this->server()->ruler_server->card_ref_or_any_set_card_has_condition_46(this->card_ref)) { + this->server()->card_special->on_card_destroyed( + attacker_card, this->shared_from_this()); + + this->card_flags = this->card_flags | 2; + this->update_stats_on_destruction(); + this->player_state()->stats.num_owned_cards_destroyed++; + + if (attacker_card && (attacker_card->team_id != this->team_id)) { + attacker_card->player_state()->stats.num_opponent_cards_destroyed++; + this->server()->add_team_exp(this->team_id ^ 1, 3); + } + + if ((this->sc_card_type == CardType::HUNTERS_SC) && (this->def_entry->def.type == CardType::ITEM)) { + auto sc_card = this->player_state()->get_sc_card(); + if (!(sc_card->card_flags & 2) && + !sc_card->get_attack_condition_value(ConditionType::ELUDE, 0xFFFF, 0xFF, 0xFFFF, nullptr)) { + int16_t hp = sc_card->get_current_hp(); + sc_card->set_current_hp(hp - 1); + sc_card->player_state()->stats.sc_damage_taken++; + if (attacker_card && (attacker_card->team_id != this->team_id)) { + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd; + cmd.effect.flags = 0x41; + cmd.effect.attacker_card_ref = attacker_card->card_ref; + cmd.effect.target_card_ref = sc_card->card_ref; + cmd.effect.value = 1; + this->server()->send(cmd); + } + if (sc_card->get_current_hp() < 1) { + sc_card->destroy_set_card(attacker_card); + } + } + } + + if ((this->server()->base()->map_and_rules1->rules.hp_type == HPType::DEFEAT_TEAM) && + (this->player_state()->get_sc_card().get() == this)) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = this->player_state()->get_set_card(set_index); + if (card) { + card->card_flags |= 2; + } + } + } + + for (size_t client_id = 0; client_id < 4; client_id++) { + if (!this->server()->player_states[client_id]) { + continue; + } + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->server()->assist_server->get_active_assist_by_index(z); + if (eff == AssistEffect::HOMESICK) { + if (client_id == this->client_id) { + this->player_state()->return_set_card_to_hand2(this->card_ref); + } + } else if (eff == AssistEffect::INHERITANCE) { + uint8_t other_team_id = this->server()->player_states[client_id]->get_team_id(); + uint8_t this_team_id = this->player_state()->get_team_id(); + if (this_team_id == other_team_id) { + this->server()->add_team_exp(team_id, this->max_hp); + } + } + } + } + + } else if (this->w_destroyer_sc_card.expired() && attacker_card) { + this->w_destroyer_sc_card = attacker_card->player_state()->get_sc_card(); + } + } +} + +int32_t Card::error_code_for_move_to_location(const Location& loc) const { + if (this->player_state()->assist_flags & 0x80) { + return -0x76; + } + if (this->card_flags & 2) { + return -0x60; + } + if (!this->server()->ruler_server->card_ref_can_move( + this->client_id, this->card_ref, 1)) { + return -0x7B; + } + // Note: The original code passes non-null pointers here but ignores the + // values written to them; we use nulls since the behavior should be the same. + if (!this->server()->ruler_server->get_move_path_length_and_cost( + this->client_id, this->card_ref, loc, nullptr, nullptr)) { + return -0x79; + } + return 0; +} + +void Card::execute_attack(shared_ptr attacker_card) { + if (!attacker_card) { + return; + } + + this->card_flags = this->card_flags & 0xFFFFFFF3; + int16_t attack_ap = this->action_metadata.attack_bonus; + int16_t attack_tp = 0; + int16_t defense_power = this->compute_defense_power_for_attacker_card(attacker_card); + if ((attack_ap == 0) && !this->action_metadata.check_flag(0x20)) { + return; + } + + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd; + cmd.effect.flags = 0x01; + cmd.effect.attacker_card_ref = attacker_card->card_ref; + cmd.effect.target_card_ref = this->card_ref; + if (attacker_card->action_chain.chain.attack_medium == AttackMedium::UNKNOWN_03) { + for (size_t strike_num = 0; strike_num < attacker_card->action_chain.chain.strike_count; strike_num++) { + this->current_hp = min( + this->current_hp + attacker_card->action_chain.chain.effective_tp, + this->max_hp); + } + this->propagate_shared_hp_if_needed(); + cmd.effect.tp = attacker_card->action_chain.chain.effective_tp; + cmd.effect.value = -cmd.effect.tp; + this->server()->send(cmd); + + } else { + uint16_t attacker_card_ref = attacker_card->get_card_ref(); + this->server()->card_special->compute_attack_ap( + this->shared_from_this(), &attack_ap, attacker_card_ref); + this->apply_ap_adjust_assists_to_attack(attacker_card, &attack_ap, &defense_power); + int16_t raw_damage = attack_ap - defense_power; + // Note: The original code uses attack_tp here, even though it is always + // zero at this point + int16_t preliminary_damage = max(raw_damage, 0) - attack_tp; + this->last_attack_preliminary_damage = preliminary_damage; + uint16_t targeted_card_ref = this->get_card_ref(); + + uint32_t unknown_a9 = 0; + auto target = this->server()->card_special->compute_replaced_target_based_on_conditions( + targeted_card_ref, 1, 0, attacker_card_ref, 0xFFFF, 0, &unknown_a9, 0xFF, 0, 0xFFFF); + if (!target) { + target = this->shared_from_this(); + } + if (unknown_a9 != 0) { + preliminary_damage = 0; + } + + if (!(this->card_flags & 2) && + (!attacker_card || !(attacker_card->card_flags & 2))) { + this->server()->card_special->unknown_80244E20( + attacker_card, this->shared_from_this(), &preliminary_damage); + } + + cmd.effect.current_hp = min(attack_ap, 99); + cmd.effect.ap = min(defense_power, 99); + cmd.effect.tp = attack_tp; + this->player_state()->stats.num_attacks_taken++; + if (!(target->card_flags & 2)) { + for (size_t strike_num = 0; strike_num < attacker_card->action_chain.chain.strike_count; strike_num++) { + int16_t final_effective_damage = 0; + target->commit_attack( + preliminary_damage, attacker_card, &cmd, strike_num, &final_effective_damage); + // TODO: Is this the right interpretation? The original code does some + // fancy bitwise magic that I didn't bother to decipher, because this + // interpretation makes sense based on how the game works. + this->player_state()->stats.action_card_negated_damage += max( + 0, this->current_defense_power - final_effective_damage); + } + } else { + target->commit_attack(0, attacker_card, &cmd, 0, nullptr); + } + if (this != target.get()) { + this->commit_attack(0, attacker_card, &cmd, 0, nullptr); + } + + this->server()->send_6xB4x39(); + } +} + +bool Card::get_attack_condition_value( + ConditionType cond_type, + uint16_t card_ref, + uint8_t def_effect_index, + uint16_t value, + uint16_t* out_value) const { + return this->action_chain.get_condition_value( + cond_type, card_ref, def_effect_index, value, out_value); +} + +shared_ptr Card::get_definition() const { + return this->def_entry; +} + +uint16_t Card::get_card_ref() const { + return this->card_ref; +} + +uint8_t Card::get_client_id() const { + return this->client_id; +} + +uint8_t Card::get_current_hp() const { + return this->current_hp; +} + +uint8_t Card::get_max_hp() const { + return this->max_hp; +} + +CardShortStatus Card::get_short_status() { + CardShortStatus ret; + if (this->def_entry->def.type == CardType::ITEM) { + this->loc = this->player_state()->get_sc_card()->loc; + } + ret.card_ref = this->card_ref; + ret.current_hp = this->current_hp; + ret.max_hp = this->max_hp; + ret.card_flags = this->card_flags; + ret.loc = this->loc; + return ret; +} + +uint8_t Card::get_team_id() const { + return this->team_id; +} + +int32_t Card::move_to_location(const Location& loc) { + int32_t code = this->error_code_for_move_to_location(loc); + if (code) { + return code; + } + + uint32_t path_cost; + uint32_t path_length; + if (!this->server()->ruler_server->get_move_path_length_and_cost( + this->client_id, this->card_ref, loc, &path_length, &path_cost)) { + return -0x79; + } + + this->player_state()->stats.total_move_distance += path_length; + this->player_state()->subtract_atk_points(path_cost); + this->loc = loc; + this->card_flags = this->card_flags | 0x80; + + for (size_t warp_type = 0; warp_type < 5; warp_type++) { + for (size_t warp_end = 0; warp_end < 2; warp_end++) { + if ((this->server()->warp_positions[warp_type][warp_end][0] == this->loc.x) && + (this->server()->warp_positions[warp_type][warp_end][1] == this->loc.y)) { + G_Unknown_GC_Ep3_6xB4x2C cmd; + cmd.loc.x = this->loc.x; + cmd.loc.y = this->loc.y; + this->loc.x = this->server()->warp_positions[warp_type][warp_end ^ 1][0]; + this->loc.y = this->server()->warp_positions[warp_type][warp_end ^ 1][1]; + cmd.change_type = 0; + cmd.card_refs[0] = this->card_ref; + this->server()->send(cmd); + return 0; + } + } + } + + return 0; +} + +void Card::propagate_shared_hp_if_needed() { + if ((this->server()->base()->map_and_rules1->rules.hp_type == HPType::COMMON_HP) && + ((this->def_entry->def.type == CardType::HUNTERS_SC) || (this->def_entry->def.type == CardType::ARKZ_SC))) { + for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) { + auto other_ps = this->server()->player_states[other_client_id]; + if ((other_client_id != this->client_id) && other_ps && + (other_ps->get_team_id() == this->team_id)) { + other_ps->get_sc_card()->set_current_hp(this->current_hp, false); + } + } + } +} + + +void Card::send_6xB4x4E_4C_4D_if_needed(bool always_send) { + ssize_t index = -1; + if (this->card_ref == this->player_state()->get_sc_card_ref()) { + index = 0; + } else { + for (size_t set_index = 0; set_index < 8; set_index++) { + if (this->card_ref == this->player_state()->get_set_ref(set_index)) { + index = set_index + 1; + break; + } + } + } + + if (index < 0) { + return; + } + + this->action_chain.chain.card_ap = this->ap; + this->action_chain.chain.card_tp = this->tp; + this->send_6xB4x4E_if_needed(always_send); + + auto& chain = this->player_state()->set_card_action_chains->at(index); + if (always_send || (chain != this->action_chain)) { + chain = this->action_chain; + if (!this->server()->get_should_copy_prev_states_to_current_states()) { + G_UpdateActionChain_GC_Ep3_6xB4x4C cmd; + cmd.client_id = this->client_id; + cmd.index = index; + cmd.chain = this->action_chain.chain; + this->server()->send(cmd); + } + } + + auto& metadata = this->player_state()->set_card_action_metadatas->at(index); + if (always_send || (metadata != this->action_metadata)) { + metadata = this->action_metadata; + G_UpdateActionMetadata_GC_Ep3_6xB4x4D cmd; + cmd.client_id = this->client_id; + cmd.index = index; + cmd.metadata = this->action_metadata; + this->server()->send(cmd); + } +} + +void Card::send_6xB4x4E_if_needed(bool always_send) { + ssize_t chain_index = -1; + if (this->card_ref == this->player_state()->get_sc_card_ref()) { + chain_index = 0; + } else { + for (size_t set_index = 0; set_index < 8; set_index++) { + if (this->card_ref == this->player_state()->get_set_ref(set_index)) { + chain_index = set_index + 1; + break; + } + } + } + + if (chain_index >= 0) { + auto& prev_conds = this->player_state()->set_card_action_chains->at(chain_index).conditions; + const auto& curr_conds = this->action_chain.conditions; + if ((prev_conds != curr_conds) || (always_send != 0)) { + prev_conds = curr_conds; + if (!this->server()->get_should_copy_prev_states_to_current_states()) { + G_UpdateCardConditions_GC_Ep3_6xB4x4E cmd; + cmd.client_id = this->client_id; + cmd.index = chain_index; + cmd.conditions = this->action_chain.conditions; + this->server()->send(cmd); + } + } + } +} + +void Card::set_current_and_max_hp(int16_t hp) { + this->current_hp = hp; + this->max_hp = hp; +} + +void Card::set_current_hp( + uint32_t new_hp, bool propagate_shared_hp, bool enforce_max_hp) { + if (!enforce_max_hp) { + new_hp = max(new_hp, 0); + this->max_hp = max(this->max_hp, new_hp); + } else { + new_hp = clamp(new_hp, 0, this->max_hp); + } + + this->current_hp = new_hp; + this->player_state()->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + + if (propagate_shared_hp) { + this->propagate_shared_hp_if_needed(); + } +} + +void Card::update_stats_on_destruction() { + this->player_state()->num_destroyed_fcs++; + this->server()->team_num_ally_fcs_destroyed[this->team_id]++; + this->server()->team_num_cards_destroyed[this->team_id]++; + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->player_states[client_id]; + if (other_ps && (other_ps->get_team_id() == this->team_id)) { + auto card = other_ps->get_sc_card(); + if (card) { + card->num_destroyed_ally_fcs++; + } + for (size_t set_index = 0; set_index < 8; set_index++) { + card = other_ps->get_set_card(set_index); + if (card) { + card->num_destroyed_ally_fcs++; + } + } + } + } +} + +void Card::clear_action_chain_and_metadata_and_most_flags() { + this->card_flags &= 0x8000FA7F; + this->action_chain.clear_inner(); + this->action_chain.chain.acting_card_ref = this->card_ref; + this->action_metadata.clear(); + this->action_metadata.card_ref = this->card_ref; +} + +void Card::compute_action_chain_results( + bool apply_action_conditions, bool ignore_this_card_ap_tp) { + this->action_chain.compute_attack_medium(this->server()); + this->action_chain.chain.strike_count = 1; + this->action_chain.chain.ap_effect_bonus = 0; + this->action_chain.chain.tp_effect_bonus = 0; + + int16_t card_ap; + int16_t card_tp; + auto stat_swap_type = this->server()->card_special->compute_stat_swap_type(this->shared_from_this()); + this->server()->card_special->get_effective_ap_tp( + stat_swap_type, &card_ap, &card_tp, this->get_current_hp(), this->ap, this->tp); + + int16_t effective_tp = card_tp; + int16_t effective_ap = card_ap; + for (size_t z = 0; (!ignore_this_card_ap_tp && (z < 8) && (z < this->action_chain.chain.attack_action_card_ref_count)); z++) { + auto ce = this->server()->definition_for_card_ref(this->action_chain.chain.attack_action_card_refs[z]); + if (ce) { + effective_ap += ce->def.ap.stat; + effective_tp += ce->def.tp.stat; + } + } + + // Add AP/TP from MAG items to SC's AP/TP + if (this->def_entry->def.is_sc()) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = this->player_state()->get_set_card(set_index); + if ((card && (card->def_entry->def.card_class() == CardClass::MAG_ITEM)) && !(card->card_flags & 2)) { + this->server()->card_special->get_effective_ap_tp( + stat_swap_type, &card_ap, &card_tp, card->get_current_hp(), card->ap, card->tp); + effective_ap += card_ap; + effective_tp += card_tp; + } + } + } + + if ((this->def_entry->def.type == CardType::ITEM) && this->sc_def_entry) { + auto sc_card = this->player_state()->get_sc_card(); + sc_card->compute_action_chain_results(apply_action_conditions, true); + effective_ap += sc_card->action_chain.chain.effective_ap + sc_card->action_chain.chain.ap_effect_bonus; + effective_tp += sc_card->action_chain.chain.effective_tp + sc_card->action_chain.chain.tp_effect_bonus; + } + + if (!this->action_chain.check_flag(0x10)) { + this->action_chain.chain.effective_ap = min(effective_ap, 99); + } + if (!this->action_chain.check_flag(0x20)) { + this->action_chain.chain.effective_tp = min(effective_tp, 99); + } + + if (apply_action_conditions) { + this->server()->card_special->apply_action_conditions( + 3, this->shared_from_this(), this->shared_from_this(), 1, nullptr); + } + + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); + for (size_t z = 0; z < num_assists; z++) { + switch (this->server()->assist_server->get_active_assist_by_index(z)) { + case AssistEffect::POWERLESS_RAIN: + if (this->card_type_is_sc_or_creature() && + (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL)) { + this->action_chain.chain.ap_effect_bonus -= 2; + } + break; + case AssistEffect::BRAVE_WIND: + if (this->card_type_is_sc_or_creature() && + (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL)) { + this->action_chain.chain.ap_effect_bonus += 2; + } + break; + case AssistEffect::INFLUENCE: + if (this->card_type_is_sc_or_creature()) { + int16_t count = this->player_state()->count_set_refs(); + this->action_chain.chain.ap_effect_bonus += (count >> 1); + } + break; + case AssistEffect::AP_ABSORPTION: + if (this->action_chain.chain.attack_medium == AttackMedium::TECH) { + this->action_chain.chain.tp_effect_bonus += 2; + } + break; + case AssistEffect::TECH_FIELD: + if (this->card_type_is_sc_or_creature()) { + this->action_chain.chain.tp_effect_bonus += 2; + } + break; + case AssistEffect::FOREST_RAIN: + if (this->def_entry->def.card_class() == CardClass::NATIVE_CREATURE) { + this->action_chain.chain.ap_effect_bonus += 2; + } + break; + case AssistEffect::CAVE_WIND: + if (this->def_entry->def.card_class() == CardClass::A_BEAST_CREATURE) { + this->action_chain.chain.ap_effect_bonus += 2; + } + break; + case AssistEffect::MINE_BRIGHTNESS: + if (this->def_entry->def.card_class() == CardClass::MACHINE_CREATURE) { + this->action_chain.chain.ap_effect_bonus += 2; + } + break; + case AssistEffect::RUIN_DARKNESS: + if (this->def_entry->def.card_class() == CardClass::DARK_CREATURE) { + this->action_chain.chain.ap_effect_bonus += 2; + } + break; + case AssistEffect::SABER_DANCE: + if (this->def_entry->def.card_class() == CardClass::SWORD_ITEM) { + this->action_chain.chain.ap_effect_bonus += 2; + } + break; + case AssistEffect::BULLET_STORM: + if (this->def_entry->def.card_class() == CardClass::GUN_ITEM) { + this->action_chain.chain.ap_effect_bonus += 2; + } + break; + case AssistEffect::CANE_PALACE: + if (this->def_entry->def.card_class() == CardClass::CANE_ITEM) { + this->action_chain.chain.tp_effect_bonus += 2; + } + break; + case AssistEffect::GIANT_GARDEN: + if (!this->def_entry->def.is_sc() && (this->def_entry->def.self_cost > 3)) { + this->action_chain.chain.ap_effect_bonus += 2; + } + break; + case AssistEffect::MARCH_OF_THE_MEEK: + if (!this->def_entry->def.is_sc() && (this->def_entry->def.self_cost <= 3)) { + this->action_chain.chain.ap_effect_bonus += 2; + } + break; + case AssistEffect::SUPPORT: + if (this->def_entry->def.is_sc()) { + size_t num_scs_in_range = 0; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->get_player_state(client_id); + if (!other_ps || (client_id == this->client_id) || (other_ps->get_team_id() != this->team_id)) { + continue; + } + auto other_sc_card = other_ps->get_sc_card(); + if (other_sc_card && + (abs(this->loc.x - other_sc_card->loc.x) < 2) && + (abs(this->loc.y - other_sc_card->loc.y) < 2)) { + num_scs_in_range++; + } + } + if (num_scs_in_range > 0) { + this->action_chain.chain.ap_effect_bonus += 3; + } + } + break; + case AssistEffect::VENGEANCE: + if (!this->def_entry->def.is_sc()) { + this->action_chain.chain.ap_effect_bonus += + (this->server()->team_num_ally_fcs_destroyed[this->team_id] / 3); + } + break; + default: + break; + } + } + + int16_t damage = 0; + if (this->action_chain.chain.attack_medium == AttackMedium::TECH) { + damage = this->action_chain.chain.effective_tp + this->action_chain.chain.tp_effect_bonus; + } else if (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL) { + damage = this->action_chain.chain.effective_ap + this->action_chain.chain.ap_effect_bonus; + } + this->action_chain.chain.damage = min( + damage * this->action_chain.chain.damage_multiplier, 99); + + if (apply_action_conditions) { + this->server()->card_special->apply_action_conditions( + 3, this->shared_from_this(), this->shared_from_this(), 2, nullptr); + if (this->action_chain.check_flag(0x100)) { + this->action_chain.chain.damage = min(this->action_chain.chain.damage + 5, 99); + } + } + + num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->get_client_id()); + for (size_t z = 0; z < num_assists; z++) { + switch (this->server()->assist_server->get_active_assist_by_index(z)) { + case AssistEffect::AP_ABSORPTION: + if (this->action_chain.chain.attack_medium == AttackMedium::PHYSICAL) { + this->action_chain.chain.damage = 0; + } + break; + case AssistEffect::SILENT_COLOSSEUM: + if (this->action_chain.chain.damage >= 7) { + this->action_chain.chain.damage = 0; + } + break; + case AssistEffect::FIX: + if (!this->def_entry->def.is_sc()) { + this->action_chain.chain.damage = 2; + } + break; + default: + break; + } + } +} + +void Card::unknown_802380C0() { + this->card_flags &= 0xFFFFF7FB; + this->action_metadata.clear_flags(0x30); + this->action_chain.clear_flags(0x140); + this->unknown_80237F98(0); +} + +void Card::unknown_80237F98(bool require_condition_20_or_21) { + bool should_send_updates = false; + for (ssize_t z = 8; z >= 0; z--) { + if (this->action_chain.conditions[z].type != ConditionType::NONE) { + if (!require_condition_20_or_21 || + this->server()->card_special->condition_has_when_20_or_21( + this->action_chain.conditions[z])) { + ActionState as; + auto& cond = this->action_chain.conditions[z]; + if (!this->server()->card_special->is_card_targeted_by_condition( + cond, as, this->shared_from_this())) { + this->server()->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond( + cond, this->shared_from_this()); + should_send_updates = true; + } else if (this->action_chain.conditions[z].remaining_turns == 0) { + if (--this->action_chain.conditions[z].a_arg_value < 1) { + this->server()->card_special->apply_stat_deltas_to_card_from_condition_and_clear_cond( + cond, this->shared_from_this()); + should_send_updates = true; + } + } + } + } + } + + this->compute_action_chain_results(1, false); + this->unknown_80236554(nullptr, nullptr); + if (should_send_updates) { + this->send_6xB4x4E_4C_4D_if_needed(); + } +} + +void Card::unknown_80237F88() { + this->card_flags &= 0xFFFFF8FF; +} + +void Card::unknown_80235AA0() { + this->facing_direction = Direction::INVALID_FF; + this->server()->card_special->unknown_80249060(this->shared_from_this()); +} + +void Card::unknown_80235AD4() { + this->clear_action_chain_and_metadata_and_most_flags(); + this->server()->card_special->unknown_80249254(this->shared_from_this()); +} + +void Card::unknown_80235B10() { + this->server()->card_special->unknown_80244BE4(this->shared_from_this()); +} + +void Card::unknown_80236374(shared_ptr other_card, const ActionState* as) { + auto check_card = [&](shared_ptr card) -> void { + if (card) { + if (!card->unknown_80236554(other_card, as)) { + card->action_metadata.clear_flags(0x20); + } else { + card->action_metadata.set_flags(0x20); + } + } + }; + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->player_states[client_id]; + if (ps) { + if (this->server()->get_current_team_turn2() != ps->get_team_id()) { + check_card(ps->get_sc_card()); + for (size_t set_index = 0; set_index < 8; set_index++) { + check_card(ps->get_set_card(set_index)); + } + } + } + } + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->player_states[client_id]; + if (ps) { + if (this->server()->get_current_team_turn2() == ps->get_team_id()) { + check_card(ps->get_sc_card()); + for (size_t set_index = 0; set_index < 8; set_index++) { + check_card(ps->get_set_card(set_index)); + } + } + } + } +} + +void Card::unknown_802379BC(uint16_t card_ref) { + this->action_chain.chain.unknown_card_ref_a3 = + (card_ref == 0xFFFF) ? this->card_ref : card_ref; +} + +void Card::unknown_802379DC(const ActionState& pa) { + this->action_metadata.add_defense_card_ref( + pa.defense_card_ref, this->shared_from_this(), pa.original_attacker_card_ref); + this->server()->card_special->unknown_8024A9D8(pa, 0xFFFF); + for (size_t z = 0; z < this->action_metadata.target_card_ref_count; z++) { + shared_ptr card = this->server()->card_for_set_card_ref(this->action_metadata.target_card_refs[z]); + if (card) { + card->action_chain.set_action_subphase_from_card(this->shared_from_this()); + card->send_6xB4x4E_4C_4D_if_needed(); + } + } + this->send_6xB4x4E_4C_4D_if_needed(); +} + +void Card::unknown_80237A90(const ActionState& pa, uint16_t action_card_ref) { + auto s = this->server(); + + this->facing_direction = pa.facing_direction; + this->action_chain.add_attack_action_card_ref(action_card_ref, s); + + for (size_t z = 0; z < 4; z++) { + if (s->ruler_server->count_rampage_targets_for_attack(pa, z) != 0) { + this->action_chain.set_flags(0x200 << z); + } + if (s->ruler_server->attack_action_has_pierce_and_not_rampage(pa, z)) { + this->action_chain.set_flags(0x2000 << z); + } + } + + if (s->ruler_server->any_attack_action_card_is_support_tech_or_support_pb(pa)) { + this->action_chain.set_flags(0x20000); + } + + if (this->action_chain.chain.target_card_ref_count == 0) { + for (size_t z = 0; (z < 4 * 9) && (pa.target_card_refs[z] != 0xFFFF); z++) { + this->action_chain.add_target_card_ref(pa.target_card_refs[z]); + shared_ptr sc_card = s->card_for_set_card_ref(pa.target_card_refs[z]); + if (sc_card) { + sc_card->action_metadata.add_target_card_ref(this->card_ref); + sc_card->card_flags |= 8; + sc_card->send_6xB4x4E_4C_4D_if_needed(); + } + } + } + + if (this->action_chain.chain.attack_number & 0x80) { + this->action_chain.chain.attack_number = s->num_pending_attacks_with_cards; + s->attack_cards[s->num_pending_attacks_with_cards] = this->shared_from_this(); + s->pending_attacks_with_cards[s->num_pending_attacks_with_cards] = pa; + s->num_pending_attacks_with_cards++; + } + s->card_special->unknown_8024A9D8(pa, action_card_ref); + this->send_6xB4x4E_4C_4D_if_needed(); +} + +void Card::unknown_8023813C() { + this->unknown_a9++; + for (ssize_t z = 8; z >= 0; z--) { + auto& cond = this->action_chain.conditions[z]; + if (cond.type != ConditionType::NONE) { + ActionState as; + if ((this->card_flags & 2) || + !this->server()->card_special->is_card_targeted_by_condition(cond, as, this->shared_from_this())) { + cond.remaining_turns = 1; + } + if (cond.remaining_turns < 99) { + 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()); + } + } + } + } + this->server()->card_special->unknown_80244CA8(this->shared_from_this()); +} + +bool Card::is_guard_item() const { + return (this->def_entry->def.card_class() == CardClass::GUARD_ITEM); +} + +bool Card::unknown_80236554(shared_ptr other_card, const ActionState* as) { + bool ret = false; + + int16_t attack_bonus = 0; + if (other_card) { + if (!as) { + for (size_t z = 0; z < other_card->action_chain.chain.target_card_ref_count; z++) { + if (other_card->action_chain.chain.target_card_refs[z] == this->get_card_ref()) { + attack_bonus = other_card->action_chain.chain.damage; + ret = true; + break; + } + } + } else { + for (size_t z = 0; (z < 4 * 9) && (as->target_card_refs[z] != 0xFFFF); z++) { + if (as->target_card_refs[z] == this->get_card_ref()) { + attack_bonus = other_card->action_chain.chain.damage; + ret = true; + break; + } + } + } + } + + this->action_metadata.attack_bonus = max(attack_bonus, 0); + this->last_attack_preliminary_damage = 0; + this->last_attack_final_damage = 0; + + if (other_card) { + this->server()->card_special->apply_action_conditions( + 3, other_card, this->shared_from_this(), 0x20, as); + this->server()->card_special->apply_action_conditions( + 0x17, other_card, this->shared_from_this(), 0x40, as); + if (other_card->action_chain.check_flag(0x20000)) { + this->action_metadata.attack_bonus = 0; + return ret; + } + } + if (!(this->card_flags & 2)) { + return ret; + } + this->action_metadata.attack_bonus = 0; + return ret; +} + +void Card::unknown_802362D8(shared_ptr other_card) { + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->player_states[client_id]; + if (ps) { + shared_ptr card = ps->get_sc_card(); + if (card) { + card->execute_attack(other_card); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + shared_ptr card = ps->get_set_card(set_index); + if (card) { + card->execute_attack(other_card); + } + } + } + } +} + +void Card::unknown_80237734() { + if (!this->action_chain.unknown_8024DEC4()) { + return; + } + + if (this->player_state()->stats.max_attack_combo_size < this->action_chain.chain.attack_action_card_ref_count) { + this->player_state()->stats.max_attack_combo_size = this->action_chain.chain.attack_action_card_ref_count; + } + + ActionState as; + as.attacker_card_ref = this->get_card_ref(); + as.target_card_refs = this->action_chain.chain.target_card_refs; + this->server()->replace_targets_due_to_destruction_or_conditions(&as); + this->action_chain.chain.target_card_refs = as.target_card_refs; + this->action_chain.chain.target_card_ref_count = 0; + for (size_t z = 0; z < 4 * 9; z++) { + if (this->action_chain.chain.target_card_refs[z] != 0xFFFF) { + this->action_chain.chain.target_card_ref_count++; + } else { + break; + } + } + + for (size_t z = 0; z < this->action_chain.chain.target_card_ref_count; z++) { + shared_ptr card = this->server()->card_for_set_card_ref(this->action_chain.chain.target_card_refs[z]); + if (card) { + card->current_defense_power = card->action_metadata.attack_bonus; + if (!this->action_chain.check_flag(0x40)) { + this->server()->card_special->unknown_8024A6DC(this->shared_from_this(), card); + } + } + } + + this->compute_action_chain_results(1, 0); + if (!this->action_chain.check_flag(0x40)) { + this->server()->card_special->unknown_8024997C(this->shared_from_this()); + } + if (!(this->card_flags & 2)) { + this->compute_action_chain_results(1, 0); + this->server()->card_special->unknown_8024504C(this->shared_from_this()); + } + this->compute_action_chain_results(1, 0); + this->unknown_80236374(this->shared_from_this(), nullptr); + this->unknown_802362D8(this->shared_from_this()); + if (!this->action_chain.check_flag(0x40)) { + this->server()->card_special->unknown_8024A394(this->shared_from_this()); + } + this->player_state()->stats.num_attacks_given++; + this->action_chain.clear_flags(8); + this->action_chain.set_flags(4); + this->card_flags |= 0x200; + this->action_chain.clear_target_card_refs(); + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->player_states[client_id]; + if (ps) { + ps->unknown_8023C110(); + } + } + this->send_6xB4x4E_4C_4D_if_needed(); +} + + + +} // namespace Episode3 diff --git a/src/Episode3/Card.hh b/src/Episode3/Card.hh new file mode 100644 index 00000000..27ba73fd --- /dev/null +++ b/src/Episode3/Card.hh @@ -0,0 +1,131 @@ +#pragma once + +#include + +#include + +#include "../Text.hh" +#include "../CommandFormats.hh" +#include "DataIndex.hh" + +namespace Episode3 { + + + +class ServerBase; +class Server; +class PlayerState; + +class Card : public std::enable_shared_from_this { +public: + Card( + uint16_t card_id, + uint16_t card_ref, + uint16_t client_id, + std::shared_ptr server); + void init(); + std::shared_ptr server(); + std::shared_ptr server() const; + std::shared_ptr player_state(); + std::shared_ptr player_state() const; + + ssize_t apply_abnormal_condition( + const CardDefinition::Effect& eff, + uint8_t def_effect_index, + uint16_t target_card_ref, + uint16_t sc_card_ref, + int16_t value, + int8_t dice_roll_value, + int8_t random_percent); + void apply_ap_adjust_assists_to_attack( + std::shared_ptr attacker_card, + int16_t* inout_attacker_ap, + int16_t* in_defense_power) const; + bool card_type_is_sc_or_creature() const; + bool check_card_flag(uint32_t flags) const; + void commit_attack( + int16_t damage, + std::shared_ptr attacker_card, + G_ApplyConditionEffect_GC_Ep3_6xB4x06* cmd, + size_t strike_number, + int16_t* out_effective_damage); + int16_t compute_defense_power_for_attacker_card( + std::shared_ptr attacker_card); + void destroy_set_card(std::shared_ptr attacker_card); + int32_t error_code_for_move_to_location(const Location& loc) const; + void execute_attack(std::shared_ptr attacker_card); + bool get_attack_condition_value( + ConditionType cond_type, + uint16_t card_ref, + uint8_t def_effect_index, + uint16_t value, + uint16_t* out_value) const; + std::shared_ptr get_definition() const; + uint16_t get_card_ref() const; + uint8_t get_client_id() const; + uint8_t get_current_hp() const; + uint8_t get_max_hp() const; + CardShortStatus get_short_status(); + uint8_t get_team_id() const; + int32_t move_to_location(const Location& loc); + void propagate_shared_hp_if_needed(); + void send_6xB4x4E_4C_4D_if_needed(bool always_send = false); + void send_6xB4x4E_if_needed(bool always_send = false); + void set_current_and_max_hp(int16_t hp); + void set_current_hp( + uint32_t new_hp, bool propagate_shared_hp = true, bool enforce_max_hp = true); + void update_stats_on_destruction(); + void clear_action_chain_and_metadata_and_most_flags(); + void compute_action_chain_results( + bool apply_action_conditions, bool ignore_this_card_ap_tp); + void unknown_802380C0(); + void unknown_80237F98(bool require_condition_20_or_21); + void unknown_80237F88(); + void unknown_80235AA0(); + void unknown_80235AD4(); + void unknown_80235B10(); + void unknown_80236374(std::shared_ptr other_card, const ActionState* as); + void unknown_802379BC(uint16_t card_ref); + void unknown_802379DC(const ActionState& pa); + void unknown_80237A90(const ActionState& pa, uint16_t action_card_ref); + void unknown_8023813C(); + bool is_guard_item() const; + bool unknown_80236554(std::shared_ptr other_card, const ActionState* as); + void unknown_802362D8(std::shared_ptr other_card); + void unknown_80237734(); + +private: + std::weak_ptr w_server; + std::weak_ptr w_player_state; + +public: + int16_t max_hp; + int16_t current_hp; + std::shared_ptr def_entry; + uint8_t client_id; + uint16_t card_id; + uint16_t card_ref; + uint16_t sc_card_ref; + std::shared_ptr sc_def_entry; + CardType sc_card_type; + uint8_t team_id; + uint32_t card_flags; + Location loc; + Direction facing_direction; + ActionChainWithConds action_chain; + ActionMetadata action_metadata; + int16_t ap; + int16_t tp; + uint32_t num_ally_fcs_destroyed_at_set_time; + uint32_t num_cards_destroyed_by_team_at_set_time; + uint32_t unknown_a9; + int16_t last_attack_preliminary_damage; + int16_t last_attack_final_damage; + uint32_t num_destroyed_ally_fcs; + std::weak_ptr w_destroyer_sc_card; + int16_t current_defense_power; +}; + + + +} // namespace Episode3 diff --git a/src/Episode3/CardSpecial.cc b/src/Episode3/CardSpecial.cc new file mode 100644 index 00000000..2d4f728c --- /dev/null +++ b/src/Episode3/CardSpecial.cc @@ -0,0 +1,4515 @@ +#include "Server.hh" + +#include + +using namespace std; + +namespace Episode3 { + + + +CardSpecial::DiceRoll::DiceRoll() { + this->clear(); +} + +void CardSpecial::DiceRoll::clear() { + this->client_id = 0; + this->unknown_a2 = 0; + this->value = 0; + this->value_used_in_expr = 0; + this->unknown_a5 = 0xFFFF; +} + + + +CardSpecial::AttackEnvStats::AttackEnvStats() { + this->clear(); +} + +void CardSpecial::AttackEnvStats::clear() { + this->num_set_cards = 0; + this->dice_roll_value1 = 0; + this->effective_ap = 0; + this->effective_tp = 0; + this->current_hp = 0; + this->max_hp = 0; + this->effective_ap_if_not_tech = 0; + this->effective_ap_if_not_physical = 0; + this->player_num_destroyed_fcs = 0; + this->player_num_atk_points = 0; + this->defined_max_hp = 0; + this->dice_roll_value2 = 0; + this->card_cost = 0; + this->total_num_set_cards = 0; + this->action_cards_ap = 0; + this->action_cards_tp = 0; + this->unknown_a1 = 0; + this->num_item_or_creature_cards_in_hand = 0; + this->num_destroyed_ally_fcs = 0; + this->target_team_num_set_cards = 0; + this->condition_giver_team_num_set_cards = 0; + this->num_native_creatures = 0; + this->num_a_beast_creatures = 0; + this->num_machine_creatures = 0; + this->num_dark_creatures = 0; + this->num_sword_type_items = 0; + this->num_gun_type_items = 0; + this->num_cane_type_items = 0; + this->effective_ap_if_not_tech2 = 0; + this->team_dice_boost = 0; + this->sc_effective_ap = 0; + this->attack_bonus = 0; + this->num_sword_type_items_on_team = 0; + this->target_attack_bonus = 0; + this->last_attack_preliminary_damage = 0; + this->last_attack_damage = 0; + this->total_last_attack_damage = 0; + this->last_attack_damage_count = 0; + this->target_current_hp = 0; +} + +uint32_t CardSpecial::AttackEnvStats::at(size_t offset) const { + constexpr size_t count = sizeof(*this) / sizeof(uint32_t); + return reinterpret_cast*>(this)->at(offset); +} + + + +CardSpecial::CardSpecial(shared_ptr server) + : w_server(server), unknown_a2(0) { } + +shared_ptr CardSpecial::server() { + auto s = this->w_server.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +shared_ptr CardSpecial::server() const { + auto s = this->w_server.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +void CardSpecial::adjust_attack_damage_due_to_conditions( + shared_ptr target_card, int16_t* inout_damage, uint16_t attacker_card_ref) { + shared_ptr attacker_card = this->server()->card_for_set_card_ref(attacker_card_ref); + auto attack_medium = attacker_card ? attacker_card->action_chain.chain.attack_medium : AttackMedium::UNKNOWN; + + for (size_t z = 0; z < 9; z++) { + const auto& cond = target_card->action_chain.conditions[z]; + if (cond.type == ConditionType::NONE) { + continue; + } + if (this->card_ref_has_ability_trap(cond)) { + continue; + } + + if (!this->server()->ruler_server->check_usability_or_apply_condition_for_card_refs( + cond.card_ref, + target_card->get_card_ref(), + attacker_card_ref, + cond.card_definition_effect_index, + attack_medium)) { + continue; + } + + switch (cond.type) { + case ConditionType::WEAK_HIT_BLOCK: + if (*inout_damage <= cond.value) { + *inout_damage = 0; + } + break; + + case ConditionType::EXP_DECOY: { + auto target_ps = target_card->player_state(); + if (target_ps) { + uint8_t target_team_id = target_ps->get_team_id(); + int16_t exp_deduction = this->server()->team_exp[target_team_id]; + if (exp_deduction < *inout_damage) { + *inout_damage = *inout_damage - exp_deduction; + this->server()->team_exp[target_team_id] = 0; + } else { + this->server()->team_exp[target_team_id] = exp_deduction - *inout_damage; + exp_deduction = *inout_damage; + *inout_damage = 0; + } + this->send_6xB4x06_for_exp_change( + target_card, attacker_card_ref, -exp_deduction, true); + this->compute_team_dice_boost(target_team_id); + } + break; + } + + case ConditionType::UNKNOWN_73: + if (cond.value <= *inout_damage) { + *inout_damage = 0; + } + break; + + case ConditionType::HALFGUARD: + if (cond.value <= *inout_damage) { + *inout_damage /= 2; + } + break; + + default: + break; + } + } +} + +void CardSpecial::adjust_dice_boost_if_team_has_condition_52( + uint8_t team_id, uint8_t* inout_dice_boost, shared_ptr card) { + if (!card || (team_id == 0xFF) || !inout_dice_boost || (card->card_flags & 3)) { + return; + } + auto ps = card->player_state(); + if (!ps || (ps->get_team_id() != team_id)) { + return; + } + + for (size_t z = 0; z < 9; z++) { + if (!this->card_ref_has_ability_trap(card->action_chain.conditions[z]) && + (card->action_chain.conditions[z].type == ConditionType::UNKNOWN_52)) { + *inout_dice_boost = *inout_dice_boost * card->action_chain.conditions[z].value8; + } + } +} + +void CardSpecial::apply_action_conditions( + uint8_t when, + shared_ptr attacker_card, + shared_ptr defender_card, + uint32_t flags, + const ActionState* as) { + ActionState temp_as; + + if (attacker_card == defender_card) { + temp_as = this->create_attack_state_from_card_action_chain(attacker_card); + if (as) { + temp_as = *as; + } + } else { + temp_as = this->create_defense_state_for_card_pair_action_chains( + attacker_card, defender_card); + } + this->apply_defense_conditions(temp_as, when, defender_card, flags); +} + +bool CardSpecial::apply_attribute_guard_if_possible( + uint32_t flags, + CardClass card_class, + shared_ptr card, + uint16_t condition_giver_card_ref, + uint16_t attacker_card_ref) { + shared_ptr condition_giver_card = this->server()->card_for_set_card_ref(condition_giver_card_ref); + if (condition_giver_card) { + auto ce = condition_giver_card->get_definition(); + if (ce && (ce->def.card_class() == card_class)) { + if (flags & 2) { + card->action_chain.reset(); + } + if (flags & 0x10) { + card->action_metadata.defense_power = 99; + card->action_metadata.defense_bonus = 0; + } + } + } + + shared_ptr attacker_card = this->server()->card_for_set_card_ref(attacker_card_ref); + if (attacker_card) { + auto ce = attacker_card->get_definition(); + if (ce && (ce->def.card_class() == card_class) && (flags & 0x10)) { + card->action_metadata.defense_power = 99; + card->action_metadata.defense_bonus = 0; + } + } + return true; +} + +bool CardSpecial::apply_defense_condition( + uint8_t when, + Condition* defender_cond, + uint8_t cond_index, + const ActionState& defense_state, + shared_ptr defender_card, + uint32_t flags, + bool unknown_p8) { + if (defender_cond->type == ConditionType::NONE) { + return false; + } + + auto orig_eff = this->original_definition_for_condition(*defender_cond); + + uint16_t attacker_card_ref = defense_state.attacker_card_ref; + if (attacker_card_ref == 0xFFFF) { + attacker_card_ref = defense_state.original_attacker_card_ref; + } + + bool defender_has_ability_trap = this->card_ref_has_ability_trap(*defender_cond); + + if (!(flags & 4) || + this->is_card_targeted_by_condition(*defender_cond, defense_state, defender_card)) { + if ((when == 2) && (defender_cond->type == ConditionType::GUOM) && (flags & 4)) { + CardShortStatus stat = defender_card->get_short_status(); + if (stat.card_flags & 4) { + 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); + cmd.effect.target_card_ref = defender_card->get_card_ref(); + cmd.effect.value = 0; + cmd.effect.operation = -static_cast(defender_cond->type); + cmd.effect.condition_index = cond_index; + this->server()->send(cmd); + this->apply_stat_deltas_to_card_from_condition_and_clear_cond( + *defender_cond, defender_card); + defender_card->send_6xB4x4E_4C_4D_if_needed(); + return false; + } + } + + if ((when == 4) && (flags & 4) && !defender_has_ability_trap && + (defender_cond->type == ConditionType::ACID)) { + int16_t hp = defender_card->get_current_hp(); + if (hp > 0) { + this->send_6xB4x06_for_stat_delta( + defender_card, defender_cond->card_ref, 0x20, -1, 0, 1); + defender_card->set_current_hp(hp - 1); + this->destroy_card_if_hp_zero(defender_card, defender_cond->condition_giver_card_ref); + } + } + + if (!orig_eff || (orig_eff->when != when)) { + flags = flags & 0xFFFFFFFB; + } + + if ((flags == 0) || defender_has_ability_trap) { + return false; + } + + DiceRoll dice_roll; + dice_roll.client_id = defender_card->get_client_id(); + dice_roll.unknown_a2 = 3; + dice_roll.value = defender_cond->dice_roll_value; + dice_roll.value_used_in_expr = false; + uint8_t original_cond_flags = defender_cond->flags; + + auto astats = this->compute_attack_env_stats( + defense_state, defender_card, dice_roll, defender_cond->card_ref, + defender_cond->condition_giver_card_ref); + + string expr = orig_eff->expr; + int16_t expr_value = this->evaluate_effect_expr(astats, expr.c_str(), dice_roll); + this->execute_effect( + *defender_cond, defender_card, expr_value, defender_cond->value, + orig_eff->type, flags, attacker_card_ref); + if (flags & 4) { + if (!(defender_card->card_flags & 2)) { + defender_card->compute_action_chain_results(true, false); + } + defender_card->action_chain.chain.card_ap = defender_card->ap; + defender_card->action_chain.chain.card_tp = defender_card->tp; + defender_card->send_6xB4x4E_4C_4D_if_needed(); + } + + if (dice_roll.value_used_in_expr && !(original_cond_flags & 1) && !unknown_p8) { + defender_cond->flags |= 1; + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd; + cmd.effect.flags = 0x08; + cmd.effect.attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid(attacker_card_ref, 0x10); + cmd.effect.target_card_ref = defender_cond->card_ref; + cmd.effect.dice_roll_value = dice_roll.value; + this->server()->send(cmd); + } + return true; + + } else { + if (defender_cond->type != ConditionType::NONE) { + 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, 0x0D); + cmd.effect.target_card_ref = defender_card->get_card_ref(); + cmd.effect.value = 0; + cmd.effect.operation = -static_cast(defender_cond->type); + this->server()->send(cmd); + } + this->apply_stat_deltas_to_card_from_condition_and_clear_cond( + *defender_cond, defender_card); + defender_card->send_6xB4x4E_4C_4D_if_needed(); + return false; + } +} + +bool CardSpecial::apply_defense_conditions( + const ActionState& as, + uint8_t when, + shared_ptr defender_card, + uint32_t flags) { + for (size_t z = 0; z < 9; z++) { + this->apply_defense_condition( + when, &defender_card->action_chain.conditions[z], z, as, defender_card, flags, 0); + } + return true; +} + +bool CardSpecial::apply_stat_deltas_to_all_cards_from_all_conditions_with_card_ref( + uint16_t card_ref) { + bool ret = false; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (!ps) { + continue; + } + auto sc_card = ps->get_sc_card(); + if (sc_card) { + ret |= this->apply_stats_deltas_to_card_from_all_conditions_with_card_ref( + card_ref, sc_card); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + auto set_card = ps->get_set_card(set_index); + if (set_card) { + ret |= this->apply_stats_deltas_to_card_from_all_conditions_with_card_ref( + card_ref, set_card); + } + } + } + + return ret; +} + +bool CardSpecial::apply_stat_deltas_to_card_from_condition_and_clear_cond( + Condition& cond, shared_ptr card) { + ConditionType cond_type = cond.type; + int16_t cond_value = clamp(cond.value, -99, 99); + uint8_t cond_flags = cond.flags; + uint16_t cond_card_ref = card->get_card_ref(); + cond.clear(); + + switch (cond_type) { + case ConditionType::UNKNOWN_0C: + if (cond_flags & 2) { + int16_t ap = clamp(card->ap, -99, 99); + int16_t tp = clamp(card->tp, -99, 99); + this->send_6xB4x06_for_stat_delta(card, cond_card_ref, 0xA0, tp - ap, 0, 0); + this->send_6xB4x06_for_stat_delta(card, cond_card_ref, 0x80, ap - tp, 0, 0); + card->ap = tp; + card->tp = ap; + } + break; + case ConditionType::A_H_SWAP: + if (cond_flags & 2) { + int16_t ap = clamp(card->ap, -99, 99); + int16_t hp = clamp(card->get_current_hp(), -99, 99); + if (hp != ap) { + this->send_6xB4x06_for_stat_delta(card, cond_card_ref, 0xA0, hp - ap, 0, 0); + this->send_6xB4x06_for_stat_delta(card, cond_card_ref, 0x20, ap - hp, 0, 0); + card->set_current_hp(ap,1,1); + card->ap = hp; + this->destroy_card_if_hp_zero(card, cond_card_ref); + } + } + break; + case ConditionType::AP_OVERRIDE: + if (cond_flags & 2) { + // Note: The original code calls a function here that returns a + // Condition pointer; however, the called function searches the card's + // condition list and then ignores the result and unconditionally + // returns null, completely obviating the non-null case here. We + // implement the non-null case for documentation purposes, but it + // appears to be completely dead code. It's unclear if this is a legit + // bug in the original code, or if it was a debug feature or + // late-development intentional change. + Condition* other_cond = nullptr; // return_null???(card, ConditionType::AP_OVERRIDE); + if (!other_cond) { + this->send_6xB4x06_for_stat_delta( + card, cond_card_ref, 0xA0, -cond_value, 0, 0); + card->ap = max(card->ap - cond_value, 0); + } else { + other_cond->value = clamp(other_cond->value + cond_value, -99, 99); + } + } + break; + case ConditionType::TP_OVERRIDE: + if (cond_flags & 2) { + // Like AP_OVERRIDE above, the non-null case here is dead code in the + // original code as well. + Condition* other_cond = nullptr; // return_null???(card, ConditionType::TP_OVERRIDE) + if (!other_cond) { + this->send_6xB4x06_for_stat_delta(card, cond_card_ref, 0x80, -cond_value, 0, 0); + card->tp = max(card->tp - cond_value, 0); + } else { + other_cond->value = clamp(other_cond->value + cond_value, -99, 99); + } + } + break; + case ConditionType::MISC_AP_BONUSES: + if (cond_flags & 2) { + this->send_6xB4x06_for_stat_delta(card, cond_card_ref, 0xA0, -cond_value, 0, 0); + card->ap = max(card->ap - cond_value, 0); + } + break; + case ConditionType::MISC_TP_BONUSES: + if (cond_flags & 2) { + this->send_6xB4x06_for_stat_delta(card, cond_card_ref, 0x80, -cond_value, 0, 0); + card->tp = max(card->tp - cond_value, 0); + } + break; + case ConditionType::AP_SILENCE: + if (cond_flags & 2) { + this->send_6xB4x06_for_stat_delta(card, cond_card_ref, 0xA0, cond_value, 0, 0); + card->ap = max(card->ap + cond_value, 0); + } + break; + case ConditionType::TP_SILENCE: + if (cond_flags & 2) { + this->send_6xB4x06_for_stat_delta(card, cond_card_ref, 0x80, cond_value, 0, 0); + card->tp = max(card->tp + cond_value, 0); + } + break; + default: + break; + } + + return true; +} + +bool CardSpecial::apply_stats_deltas_to_card_from_all_conditions_with_card_ref( + uint16_t card_ref, shared_ptr card) { + bool ret = false; + for (ssize_t z = 8; z >= 0; z--) { + auto& cond = card->action_chain.conditions[z]; + if ((cond.type != ConditionType::NONE) && (cond.card_ref == card_ref)) { + ret |= this->apply_stat_deltas_to_card_from_condition_and_clear_cond( + cond, card); + } + } + return ret; +} + +bool CardSpecial::card_has_condition_with_ref( + shared_ptr card, + ConditionType cond_type, + uint16_t card_ref, + uint16_t match_card_ref) const { + size_t z = 0; + while ((z < 9) && + ((card->action_chain.conditions[z].type != cond_type) || + (card->action_chain.conditions[z].card_ref == card_ref))) { + z++; + } + if (z >= 9) { + return false; + } + return (match_card_ref != 0xFFFF) ? (card_ref == match_card_ref) : true; +} + +bool CardSpecial::card_is_destroyed(shared_ptr card) const { + if (card->card_flags & 3) { + return true; + } + if (card->get_current_hp() > 0) { + return false; + } + return !this->server()->ruler_server->card_ref_or_any_set_card_has_condition_46( + card->get_card_ref()); +} + +void CardSpecial::compute_attack_ap( + shared_ptr target_card, + int16_t* out_value, + uint16_t attacker_card_ref) { + auto attacker_card = this->server()->card_for_set_card_ref(attacker_card_ref); + AttackMedium attacker_sc_attack_medium = attacker_card + ? attacker_card->action_chain.chain.attack_medium + : AttackMedium::UNKNOWN; + uint16_t target_card_ref = target_card->get_card_ref(); + + auto check_card = [&](shared_ptr card) -> void{ + if (!card || (card->card_flags & 3)) { + return; + } + for (size_t cond_index = 0; cond_index < 9; cond_index++) { + auto& cond = card->action_chain.conditions[cond_index]; + if (cond.type == ConditionType::NONE || + this->card_ref_has_ability_trap(cond) || + !this->server()->ruler_server->check_usability_or_apply_condition_for_card_refs( + card->action_chain.conditions[cond_index].card_ref, + target_card->get_card_ref(), + attacker_card_ref, + card->action_chain.conditions[cond_index].card_definition_effect_index, + attacker_sc_attack_medium)) { + continue; + } + + auto cond_type = card->action_chain.conditions[cond_index].type; + if (((cond_type == ConditionType::UNKNOWN_5F) && + (target_card_ref == card->action_chain.conditions[cond_index].condition_giver_card_ref)) || + ((cond_type == ConditionType::UNKNOWN_60) && + (target_card_ref == card->action_chain.conditions[cond_index].card_ref))) { + *out_value = card->action_chain.conditions[cond_index].value8; + } + } + }; + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (ps) { + for (size_t set_index = 0; set_index < 8; set_index++) { + check_card(ps->get_set_card(set_index)); + } + check_card(ps->get_sc_card()); + } + } + + if (attacker_card && + attacker_card->get_attack_condition_value(ConditionType::UNKNOWN_7D, 0xFFFF,0xFF,0xFFFF, nullptr)) { + *out_value = *out_value * 1.5f; + } + if (target_card && + target_card->get_attack_condition_value(ConditionType::UNKNOWN_7D, 0xFFFF, 0xFF, 0xFFFF, nullptr)) { + *out_value = 0; + } +} + +CardSpecial::AttackEnvStats CardSpecial::compute_attack_env_stats( + const ActionState& pa, + shared_ptr card, + const DiceRoll& dice_roll, + uint16_t target_card_ref, + uint16_t condition_giver_card_ref) { + this->action_state = pa; + auto attacker_card = this->server()->card_for_set_card_ref(pa.attacker_card_ref); + if (!attacker_card && (pa.original_attacker_card_ref != 0xFFFF)) { + attacker_card = this->server()->card_for_set_card_ref(pa.original_attacker_card_ref); + } + + AttackEnvStats ast; + + auto ps = card->player_state(); + ast.num_set_cards = ps->count_set_cards(); + auto condition_giver_card = this->server()->card_for_set_card_ref(condition_giver_card_ref); + auto target_card = this->server()->card_for_set_card_ref(target_card_ref); + if (!target_card) { + target_card = condition_giver_card; + } + + size_t ps_num_set_cards = 0; + for (size_t z = 0; z < 4; z++) { + auto other_ps = this->server()->get_player_state(z); + if (other_ps) { + ps_num_set_cards += other_ps->count_set_cards(); + } + } + ast.total_num_set_cards = ps_num_set_cards; + + uint8_t target_card_team_id = target_card + ? target_card->player_state()->get_team_id() : 0xFF; + + size_t target_team_num_set_cards = 0; + size_t condition_giver_team_num_set_cards = 0; + for (size_t z = 0; z < 4; z++) { + auto other_ps = this->server()->get_player_state(z); + if (other_ps) { + if (target_card_team_id == other_ps->get_team_id()) { + target_team_num_set_cards += other_ps->count_set_cards(); + } else { + condition_giver_team_num_set_cards += other_ps->count_set_cards(); + } + } + } + ast.target_team_num_set_cards = target_team_num_set_cards; + ast.condition_giver_team_num_set_cards = condition_giver_team_num_set_cards; + + ast.num_native_creatures = this->get_all_set_cards_by_team_and_class( + CardClass::NATIVE_CREATURE, 0xFF, true).size(); + ast.num_a_beast_creatures = this->get_all_set_cards_by_team_and_class( + CardClass::A_BEAST_CREATURE, 0xFF, true).size(); + ast.num_machine_creatures = this->get_all_set_cards_by_team_and_class( + CardClass::MACHINE_CREATURE, 0xFF, true).size(); + ast.num_dark_creatures = this->get_all_set_cards_by_team_and_class( + CardClass::DARK_CREATURE, 0xFF, true).size(); + ast.num_sword_type_items = this->get_all_set_cards_by_team_and_class( + CardClass::SWORD_ITEM, 0xFF, true).size(); + ast.num_gun_type_items = this->get_all_set_cards_by_team_and_class( + CardClass::GUN_ITEM, 0xFF, true).size(); + ast.num_cane_type_items = this->get_all_set_cards_by_team_and_class( + CardClass::CANE_ITEM, 0xFF, true).size(); + ast.num_sword_type_items_on_team = card + ? this->get_all_set_cards_by_team_and_class(CardClass::SWORD_ITEM, card->get_team_id(), true).size() + : 0; + + size_t num_item_or_creature_cards_in_hand = 0; + for (size_t z = 0; z < 6; z++) { + uint16_t card_ref = ps->card_ref_for_hand_index(z); + if (card_ref == 0xFFFF) { + continue; + } + auto ce = this->server()->definition_for_card_id(card_ref); + if (ce && ((ce->def.type == CardType::ITEM) || (ce->def.type == CardType::CREATURE))) { + num_item_or_creature_cards_in_hand++; + } + } + ast.num_item_or_creature_cards_in_hand = num_item_or_creature_cards_in_hand; + + ast.num_destroyed_ally_fcs = card->num_destroyed_ally_fcs; + // Note: The original implementation has dice_roll as optional, but since it's + // provided at all callsites, we require it (and hence don't check for nullptr + // here) + ast.dice_roll_value1 = dice_roll.value; + ast.dice_roll_value2 = dice_roll.value; + ast.effective_ap = card->action_chain.chain.effective_ap; + ast.effective_tp = card->action_chain.chain.effective_tp; + ast.current_hp = card->get_current_hp(); + ast.max_hp = card->get_max_hp(); + ast.team_dice_boost = card ? this->server()->team_dice_boost[card->get_team_id()] : 0; + + ast.effective_ap_if_not_tech = (!attacker_card || (attacker_card->action_chain.chain.attack_medium == AttackMedium::TECH)) + ? 0 : attacker_card->action_chain.chain.damage; + ast.effective_ap_if_not_tech2 = (!attacker_card || (attacker_card->action_chain.chain.attack_medium == AttackMedium::TECH)) + ? 0 : attacker_card->action_chain.chain.damage; + ast.effective_ap_if_not_physical = (!attacker_card || (attacker_card->action_chain.chain.attack_medium == AttackMedium::PHYSICAL)) + ? 0 : attacker_card->action_chain.chain.damage; + ast.sc_effective_ap = attacker_card ? attacker_card->action_chain.chain.damage : 0; + ast.attack_bonus = card->action_metadata.attack_bonus; + ast.last_attack_preliminary_damage = card->last_attack_preliminary_damage; + ast.last_attack_damage = card->last_attack_final_damage; + + int32_t total_last_attack_damage; + size_t last_attack_damage_count; + this->sum_last_attack_damage(nullptr, &total_last_attack_damage, &last_attack_damage_count); + ast.total_last_attack_damage = total_last_attack_damage; + ast.last_attack_damage_count = last_attack_damage_count; + + if (!target_card) { + ast.target_attack_bonus = 0; + ast.target_current_hp = 0; + } else { + ast.target_attack_bonus = target_card->action_metadata.attack_bonus; + ast.target_current_hp = target_card->get_current_hp(); + } + ast.player_num_destroyed_fcs = ps->num_destroyed_fcs; + ast.player_num_atk_points = ps->get_atk_points(); + + auto ce = card->get_definition(); + ast.card_cost = ce->def.self_cost; + ast.defined_max_hp = ast.max_hp; + + size_t z; + // Note: The (z < 9) conditions in these two loops are not present in the + // original code. + for (z = 0; + ((target_card_ref != pa.attacker_card_ref) && (z < 9) && (pa.action_card_refs[z] != 0xFFFF)); + z++) { } + ast.action_cards_ap = 0; + ast.action_cards_tp = 0; + for (; (z < 9) && (pa.action_card_refs[z] != 0xFFFF); z++) { + this->unknown_a2 = pa.action_card_refs[z]; + auto ce = this->server()->definition_for_card_ref(pa.action_card_refs[z]); + if (ce) { + if (ce->def.ap.type != CardDefinition::Stat::Type::MINUS_STAT) { + ast.action_cards_ap += ce->def.ap.stat; + } + if (ce->def.tp.type != CardDefinition::Stat::Type::MINUS_STAT) { + ast.action_cards_tp += ce->def.tp.stat; + } + } + } + + return ast; +} + +shared_ptr CardSpecial::compute_replaced_target_based_on_conditions( + uint16_t target_card_ref, + int unknown_p3, + int unknown_p4, + uint16_t attacker_card_ref, + uint16_t set_card_ref, + int unknown_p7, + uint32_t* unknown_p9, + uint8_t def_effect_index, + uint32_t* unknown_p11, + uint16_t sc_card_ref) { + auto attacker_card = this->server()->card_for_set_card_ref(attacker_card_ref); + auto target_card = this->server()->card_for_set_card_ref(target_card_ref); + uint8_t target_client_id = client_id_for_card_ref(target_card_ref); + uint8_t target_team_id = 0xFF; + if (unknown_p9) { + *unknown_p9 = 0; + } + if (target_card) { + target_team_id = target_card->get_team_id(); + } + if (unknown_p11) { + *unknown_p11 = 0; + } + + Location target_card_loc; + if (!target_card) { + target_card_loc.x = 0; + target_card_loc.y = 0; + target_card_loc.direction = Direction::RIGHT; + } else { + this->get_card1_loc_with_card2_opposite_direction( + &target_card_loc, target_card, attacker_card); + } + + auto attack_medium = attacker_card ? attacker_card->action_chain.chain.attack_medium : AttackMedium::INVALID_FF; + + if ((this->server()->get_battle_phase() != BattlePhase::ACTION) || + (this->server()->get_current_action_subphase() == ActionSubphase::ATTACK)) { + return nullptr; + } + if (target_card_ref == attacker_card_ref) { + return nullptr; + } + if (target_card_ref == set_card_ref) { + return nullptr; + } + + bool has_pierce = ((target_client_id != 0xFF) && + attacker_card && + (attacker_card->action_chain.check_flag(0x00002000 << target_client_id))); + + // Handle Parry if present + if (target_card && !(target_card->card_flags & 3)) { + for (size_t x = 0; x < 9; x++) { + auto& cond = target_card->action_chain.conditions[x]; + if ((unknown_p7 == 0) && this->card_ref_has_ability_trap(cond)) { + continue; + } + if (cond.type == ConditionType::NONE) { + continue; + } + if (!this->server()->ruler_server->check_usability_or_apply_condition_for_card_refs( + target_card->action_chain.conditions[x].card_ref, + target_card->get_card_ref(), + attacker_card_ref, + target_card->action_chain.conditions[x].card_definition_effect_index, + attack_medium)) { + continue; + } + if (target_card->action_chain.conditions[x].type != ConditionType::PARRY) { + continue; + } + auto target_ps = target_card->player_state(); + if (has_pierce || (unknown_p7 != 0) || !target_ps) { + continue; + } + + // Parry forwards the attack to a random FC within one tile of the + // original target. Note that Sega's implementation (used here) hardcodes + // the Gifoie card's ID (00D9) for compute_effective_range. + // TODO: We should fix this so it doesn't rely on a fixed card definition. + parray range; + compute_effective_range(range, this->server()->base()->data_index, 0x00D9, target_card_loc, this->server()->base()->map_and_rules1); + auto card_refs_in_parry_range = target_ps->get_all_cards_within_range( + range, target_card_loc, 0xFF); + + // Filter out the attacker card ref, the set card ref, the original + // target, and any SCs within the range + vector candidate_card_refs; + for (uint16_t card_ref : card_refs_in_parry_range) { + if (attacker_card_ref == card_ref) { + continue; + } + if (set_card_ref == card_ref) { + continue; + } + if (target_card_ref == card_ref) { + continue; + } + auto ce = this->server()->definition_for_card_ref(card_ref); + if (ce && ((ce->def.type == CardType::HUNTERS_SC) || (ce->def.type == CardType::ARKZ_SC))) { + continue; + } + candidate_card_refs.emplace_back(card_ref); + } + + size_t num_candidates = candidate_card_refs.size(); + if (num_candidates > 0) { + uint8_t a = target_ps->roll_dice_with_effects(2); + uint8_t b = target_ps->roll_dice_with_effects(1); + return this->server()->card_for_set_card_ref( + candidate_card_refs[(a + b) - ((a + b) / num_candidates) * num_candidates]); + } + } + } + + // Note: Some vestigial functionality was removed here. The original code has + // a parallel array of booleans that seem to specify a priority: if any of the + // candidate cards has true in the priority array, then the first candidate + // card with a true value is returned instead of a random entry from the + // entire array. The original code only puts false values into the priority + // array, effectively rendering it unused, so we've omitted it entirely. + vector> candidate_cards; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->get_player_state(client_id); + if (!other_ps) { + continue; + } + + for (size_t set_index = 0; set_index < 8; set_index++) { + auto other_set_card = other_ps->get_set_card(set_index); + if (!other_set_card || (other_set_card->card_flags & 3)) { + continue; + } + + for (size_t z = 0; (z < 9) && (candidate_cards.size() < 36); z++) { + auto& cond = other_set_card->action_chain.conditions[z]; + if ((unknown_p7 == 0) && this->card_ref_has_ability_trap(cond)) { + continue; + } + if (cond.type == ConditionType::NONE) { + continue; + } + if (!this->server()->ruler_server->check_usability_or_apply_condition_for_card_refs( + other_set_card->action_chain.conditions[z].card_ref, + other_set_card->get_card_ref(), + attacker_card_ref, + other_set_card->action_chain.conditions[z].card_definition_effect_index, + attack_medium)) { + continue; + } + + switch (other_set_card->action_chain.conditions[z].type) { + case ConditionType::GUARD_CREATURE: + if (!has_pierce && + (unknown_p7 != 0) && + ((unknown_p3 != 0) || (unknown_p4 != 0)) && + (target_client_id == client_id) && + target_card && + target_card->get_definition()->def.is_sc()) { + candidate_cards.emplace_back(other_set_card); + } + break; + case ConditionType::DEFENDER: + if (!has_pierce && + (unknown_p7 == 0) && + (unknown_p4 != 0) && + (target_card_ref == other_set_card->action_chain.conditions[z].condition_giver_card_ref)) { + candidate_cards.emplace_back(other_set_card); + if (unknown_p11 &&(def_effect_index != 0xFF) && (set_card_ref != 0xFFFF) && + !this->server()->ruler_server->check_usability_or_apply_condition_for_card_refs( + set_card_ref, sc_card_ref, other_set_card->get_card_ref(), def_effect_index, attack_medium)) { + *unknown_p11 = 1; + } + } + break; + case ConditionType::UNKNOWN_39: + if (!has_pierce && + (unknown_p7 == 0) && + (unknown_p3 != 0) && + (target_card_ref == other_set_card->action_chain.conditions[z].condition_giver_card_ref)) { + candidate_cards.emplace_back(other_set_card); + } + break; + case ConditionType::SURVIVAL_DECOYS: + if (!has_pierce && + (unknown_p7 == 0) && + attacker_card && + (attacker_card->action_chain.chain.target_card_ref_count > 1) && + (unknown_p3 != 0) && + (other_set_card->get_team_id() == target_team_id)) { + candidate_cards.emplace_back(other_set_card); + } + break; + case ConditionType::REFLECT: + if ((unknown_p7 == 0) && (unknown_p3 != 0)) { + if (target_card_ref == other_set_card->action_chain.conditions[z].condition_giver_card_ref) { + if (unknown_p9) { + *unknown_p9 = 0; + } + return other_set_card; + } else if (unknown_p9) { + *unknown_p9 = 1; + } + } + break; + default: + break; + } + } + } + + auto other_sc = other_ps->get_sc_card(); + if (other_sc && !(other_sc->card_flags & 3)) { + for (size_t z = 0; (z < 9) && (candidate_cards.size() < 36); z++) { + auto& cond = other_sc->action_chain.conditions[z]; + if ((unknown_p7 == 0) && this->card_ref_has_ability_trap(cond)) { + continue; + } + if (cond.type == ConditionType::NONE) { + continue; + } + if (!this->server()->ruler_server->check_usability_or_apply_condition_for_card_refs( + cond.card_ref, + other_sc->get_card_ref(), + attacker_card_ref, + cond.card_definition_effect_index, + attack_medium)) { + continue; + } + + switch (cond.type) { + case ConditionType::GUARD_CREATURE: + if (!has_pierce && + (unknown_p7 != 0) && + ((unknown_p3 != 0) || (unknown_p4 != 0)) && + (target_client_id == client_id) && + target_card && + target_card->get_definition()->def.is_sc()) { + candidate_cards.emplace_back(other_sc); + } + break; + case ConditionType::DEFENDER: + if (!has_pierce && + (unknown_p7 == 0) && + (unknown_p4 != 0) && + (target_card_ref == cond.condition_giver_card_ref)) { + candidate_cards.emplace_back(other_sc); + if (unknown_p11 && (def_effect_index != 0xFF) && (set_card_ref != 0xFFFF) && + !this->server()->ruler_server->check_usability_or_apply_condition_for_card_refs( + set_card_ref, sc_card_ref, other_sc->get_card_ref(), def_effect_index, attack_medium)) { + *unknown_p11 = 1; + } + } + break; + case ConditionType::UNKNOWN_39: + if (!has_pierce && + (unknown_p7 == 0) && + (unknown_p3 != 0) && + (target_card_ref == cond.condition_giver_card_ref)) { + candidate_cards.emplace_back(other_sc); + } + break; + case ConditionType::SURVIVAL_DECOYS: + if (!has_pierce && + (unknown_p7 == 0) && + attacker_card && + (attacker_card->action_chain.chain.target_card_ref_count > 1) && + (unknown_p3 != 0) && + (other_sc->get_team_id() == target_team_id)) { + candidate_cards.emplace_back(other_sc); + } + break; + case ConditionType::REFLECT: + if ((unknown_p7 == 0) && (unknown_p3 != 0)) { + if (target_card_ref == cond.condition_giver_card_ref) { + if (unknown_p9) { + *unknown_p9 = 0; + } + return other_sc; + } else if (unknown_p9) { + *unknown_p9 = 1; + } + } + break; + default: + break; + } + } + } + } + + if (candidate_cards.empty()) { + return nullptr; + } + + // If the set card is a candidate (or the attacker is, if there's no set + // card), don't redirect the attack at all + for (size_t z = 0; z < candidate_cards.size(); z++) { + auto candidate_card = candidate_cards[z]; + uint16_t candidate_card_ref = candidate_card->get_card_ref(); + if ((set_card_ref == candidate_card_ref) || + ((set_card_ref == 0xFFFF) && (attacker_card_ref == candidate_card_ref))) { + return nullptr; + } + } + + if (candidate_cards.size() == 1) { + return candidate_cards[0]; + } + + uint8_t index = 0; + auto target_ps = target_card->player_state(); + if (target_ps && (unknown_p7 == 0)) { + uint8_t a = target_ps->roll_dice_with_effects(2); + uint8_t b = target_ps->roll_dice_with_effects(1); + index = (a + b) - ((a + b) / candidate_cards.size()) * candidate_cards.size(); + } + return candidate_cards[index]; +} + +StatSwapType CardSpecial::compute_stat_swap_type(shared_ptr card) const { + if (!card) { + return StatSwapType::NONE; + } + + StatSwapType ret = StatSwapType::NONE; + for (size_t cond_index = 0; cond_index < 9; cond_index++) { + auto& cond = card->action_chain.conditions[cond_index]; + if (cond.type != ConditionType::NONE) { + if (!this->card_ref_has_ability_trap(cond)) { + if (cond.type == ConditionType::UNKNOWN_75) { + if (ret == StatSwapType::A_H_SWAP) { + ret = StatSwapType::NONE; + } else { + ret = StatSwapType::A_H_SWAP; + } + } else if (cond.type == ConditionType::A_T_SWAP) { + if (ret == StatSwapType::A_T_SWAP) { + ret = StatSwapType::NONE; + } else { + ret = StatSwapType::A_T_SWAP; + } + } + } + } + } + return ret; +} + +void CardSpecial::compute_team_dice_boost(uint8_t team_id) { + uint8_t value = this->server()->team_exp[team_id] / (this->server()->team_client_count[team_id] * 12); + this->adjust_dice_boost_if_team_has_condition_52(team_id, &value, 0); + this->server()->team_dice_boost[team_id] = min(value, 8); +} + +bool CardSpecial::condition_has_when_20_or_21(const Condition& cond) const { + auto ce = this->server()->definition_for_card_ref(cond.card_ref); + if (!ce) { + return false; + } + uint8_t when = ce->def.effects[cond.card_definition_effect_index].when; + return ((when == 0x20) || (when == 0x21)); +} + +size_t CardSpecial::count_action_cards_with_condition_for_all_current_attacks( + ConditionType cond_type, uint16_t card_ref) const { + size_t ret = 0; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (ps) { + ret += this->count_action_cards_with_condition_for_current_attack( + ps->get_sc_card(), cond_type, card_ref); + for (size_t set_index = 0; set_index < 8; set_index++) { + ret += this->count_action_cards_with_condition_for_current_attack( + ps->get_set_card(set_index), cond_type, card_ref); + } + } + } + return ret; +} + +size_t CardSpecial::count_action_cards_with_condition_for_current_attack( + shared_ptr card, ConditionType cond_type, uint16_t card_ref) const { + if (!card) { + return 0; + } + + size_t ret = 0; + + auto check_card_ref = [&](uint16_t other_card_ref) { + if (other_card_ref == card_ref) { + return; + } + auto ce = this->server()->definition_for_card_ref(other_card_ref); + if (!ce) { + return; + } + for (size_t cond_index = 0; cond_index < 3; cond_index++) { + if (ce->def.effects[cond_index].type == ConditionType::NONE) { + break; + } + if (ce->def.effects[cond_index].type == cond_type) { + ret++; + break; + } + } + }; + + for (size_t z = 0; z < card->action_chain.chain.attack_action_card_ref_count; z++) { + check_card_ref(card->action_chain.chain.attack_action_card_refs[z]); + } + for (size_t z = 0; z < card->action_metadata.defense_card_ref_count; z++) { + check_card_ref(card->action_metadata.defense_card_refs[z]); + } + + return ret; +} + +size_t CardSpecial::count_cards_with_card_id_set_by_player_except_card_ref( + uint16_t card_id, uint16_t card_ref) const { + size_t ret = 0; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (!ps) { + continue; + } + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = ps->get_set_card(set_index); + if (card && + (card->get_card_ref() != card_ref) && + (card->get_definition()->def.card_id == card_id)) { + ret++; + } + } + } + return ret; +} + +vector> CardSpecial::get_all_set_cards_by_team_and_class( + CardClass card_class, uint8_t team_id, bool exclude_destroyed_cards) const { + vector> ret; + auto check_card = [&](shared_ptr card) -> void { + if (card && + (!exclude_destroyed_cards || !(card->card_flags & 2)) && + (card->get_definition()->def.card_class() == card_class) && + ((team_id == 0xFF) || (card->get_team_id() == team_id))) { + ret.emplace_back(card); + } + }; + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (!ps) { + continue; + } + check_card(ps->get_sc_card()); + for (size_t set_index = 0; set_index < 8; set_index++) { + check_card(ps->get_set_card(set_index)); + } + } + + return ret; +} + +ActionState CardSpecial::create_attack_state_from_card_action_chain( + shared_ptr attacker_card) const { + ActionState ret; + if (attacker_card) { + ret.attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid( + attacker_card->get_card_ref(), 4); + for (size_t z = 0; z < attacker_card->action_chain.chain.attack_action_card_ref_count; z++) { + ret.action_card_refs[z] = this->send_6xB4x06_if_card_ref_invalid( + attacker_card->action_chain.chain.attack_action_card_refs[z], 5); + } + for (size_t z = 0; z < attacker_card->action_chain.chain.target_card_ref_count; z++) { + ret.target_card_refs[z] = this->send_6xB4x06_if_card_ref_invalid( + attacker_card->action_chain.chain.target_card_refs[z], 6); + } + } + return ret; +} + +ActionState CardSpecial::create_defense_state_for_card_pair_action_chains( + shared_ptr attacker_card, + shared_ptr defender_card) const { + ActionState ret; + if (defender_card && attacker_card) { + size_t count = 0; + for (size_t z = 0; z < defender_card->action_metadata.defense_card_ref_count; z++) { + if ((defender_card->action_metadata.defense_card_refs[z] != 0xFFFF) && + (defender_card->action_metadata.original_attacker_card_refs[z] == attacker_card->get_card_ref())) { + ret.action_card_refs[count++] = this->send_6xB4x06_if_card_ref_invalid( + defender_card->action_metadata.defense_card_refs[z], 7); + } + } + } + if (defender_card) { + ret.target_card_refs[0] = this->send_6xB4x06_if_card_ref_invalid( + defender_card->get_card_ref(), 8); + } + if (attacker_card) { + ret.original_attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid( + attacker_card->get_card_ref(), 9); + } + return ret; +} + +void CardSpecial::destroy_card_if_hp_zero( + shared_ptr card, uint16_t attacker_card_ref) { + if (card && (card->get_current_hp() <= 0)) { + card->destroy_set_card(this->server()->card_for_set_card_ref(attacker_card_ref)); + } +} + +bool CardSpecial::evaluate_effect_arg2_condition( + const ActionState& as, + shared_ptr card, + const char* arg2_text, + DiceRoll& dice_roll, + uint16_t set_card_ref, + uint16_t sc_card_ref, + uint8_t random_percent, + uint8_t when) const { + // Note: In the original code, as and dice_roll were optional pointers, but + // they are non-null at all callsites, so we've replaced them with references + // (and eliminated the null checks within this function). + + uint16_t attacker_card_ref = as.attacker_card_ref; + if (attacker_card_ref == 0xFFFF) { + attacker_card_ref = as.original_attacker_card_ref; + } + + auto set_card = this->server()->card_for_set_card_ref(set_card_ref); + bool set_card_has_ability_trap = (set_card && + (this->card_has_condition_with_ref(set_card, ConditionType::ABILITY_TRAP, 0xFFFF, 0xFFFF))); + + switch (arg2_text[0]) { + case 'C': + card = this->server()->card_for_set_card_ref(set_card_ref); + if (!card) { + card = this->server()->card_for_set_card_ref(sc_card_ref); + } + if (!card) { + return false; + } + [[fallthrough]]; + case 'c': { + uint8_t ch1 = arg2_text[1] - '0'; + uint8_t ch2 = arg2_text[2] - '0'; + if ((ch1 > 9) || (ch2 > 9)) { + return false; + } + auto ps = this->server()->get_player_state(client_id_for_card_ref(card->get_card_ref())); + if (!ps) { + return false; + } + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = ps->get_set_card(set_index); + if (!card) { + continue; + } + auto ce = card->get_definition(); + if (!ce) { + continue; + } + for (size_t cond_index = 0; cond_index < 3; cond_index++) { + if (ce->def.effects[cond_index].type == ConditionType::NONE) { + break; + } + uint8_t arg2_command = ce->def.effects[cond_index].arg2[0]; + if ((arg2_command == 'c') || (arg2_command == 'C')) { + uint8_t other_ch1 = ce->def.effects[cond_index].arg2[1] - 0x30; + if ((other_ch1 > 9)) { + return false; + } + if (other_ch1 == ch2) { + return true; + } + } + } + } + return false; + } + + case 'b': { + auto attacker_card = this->server()->card_for_set_card_ref(attacker_card_ref); + return (attacker_card && (attacker_card->action_chain.chain.damage <= atoi(arg2_text + 1))); + } + + case 'd': { + if (set_card_has_ability_trap) { + return false; + } + uint8_t low = arg2_text[1] - '0'; + uint8_t high = arg2_text[2] - '0'; + if ((low < 10) && (high < 10)) { + if (high < low) { + uint8_t t = high; + high = low; + low = t; + } + dice_roll.value_used_in_expr = true; + return ((low <= dice_roll.value) && (dice_roll.value <= high)); + } + return false; + } + + case 'h': + return (atoi(arg2_text + 1) <= card->get_current_hp()); + + case 'i': + return (atoi(arg2_text + 1) >= card->get_current_hp()); + + case 'm': { + auto attacker_card = this->server()->card_for_set_card_ref(attacker_card_ref); + return (attacker_card && (attacker_card->action_chain.chain.damage >= atoi(arg2_text + 1))); + } + + case 'n': + switch (atoi(arg2_text + 1)) { + case 0: + return true; + case 1: + return (!card || (card->get_definition()->def.type == CardType::HUNTERS_SC)); + case 2: + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto target_card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (target_card && target_card->check_card_flag(2)) { + return true; + } + } + return false; + case 3: + for (size_t z = 0; z < 8; z++) { + uint16_t action_card_ref = as.action_card_refs[z]; + if (action_card_ref != 0xFFFF) { + auto ce = this->server()->definition_for_card_ref(action_card_ref); + if (card_class_is_tech_like(ce->def.card_class())) { + return true; + } + } + } + return false; + case 4: + return card->action_chain.check_flag(0x0001E000); + case 5: + return card->action_chain.check_flag(0x00001E00); + case 6: + return (card->get_definition()->def.card_class() == CardClass::NATIVE_CREATURE); + case 7: + return (card->get_definition()->def.card_class() == CardClass::A_BEAST_CREATURE); + case 8: + return (card->get_definition()->def.card_class() == CardClass::MACHINE_CREATURE); + case 9: + return (card->get_definition()->def.card_class() == CardClass::DARK_CREATURE); + case 10: + return (card->get_definition()->def.card_class() == CardClass::SWORD_ITEM); + case 11: + return (card->get_definition()->def.card_class() == CardClass::GUN_ITEM); + case 12: + return (card->get_definition()->def.card_class() == CardClass::CANE_ITEM); + case 13: { + auto ce = card->get_definition(); + return ((ce->def.card_class() == CardClass::GUARD_ITEM) || + (ce->def.card_class() == CardClass::MAG_ITEM) || + this->server()->ruler_server->find_condition_on_card_ref( + card->get_card_ref(), ConditionType::GUARD_CREATURE, 0, 0, 0)); + } + case 14: + return card->get_definition()->def.is_sc(); + case 15: + return ((card->action_chain.chain.attack_action_card_ref_count == 0) && + (card->action_metadata.defense_card_ref_count == 0)); + case 16: + return this->server()->ruler_server->card_ref_is_aerial(card->get_card_ref()); + case 17: { + auto sc_card = this->server()->card_for_set_card_ref(sc_card_ref); + int16_t this_ap = card->ap; + int16_t other_ap = -1; + if (!sc_card) { + auto ce = this->server()->definition_for_card_ref(sc_card_ref); + if (ce) { + other_ap = ce->def.ap.stat; + } + } else { + other_ap = sc_card->ap; + } + return (other_ap == this_ap); + } + case 18: + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto target_card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (target_card && target_card->get_definition()->def.is_sc()) { + return true; + } + } + return false; + case 19: + return this->server()->ruler_server->find_condition_on_card_ref( + card->get_card_ref(), ConditionType::PARALYZE, 0, 0, 0); + case 20: + return this->server()->ruler_server->find_condition_on_card_ref( + card->get_card_ref(), ConditionType::FREEZE, 0, 0, 0); + case 21: { + uint8_t client_id = client_id_for_card_ref(sc_card_ref); + if (client_id != 0xFF) { + return card->action_chain.check_flag(0x00002000 << client_id); + } + return false; + } + case 22: { + uint8_t client_id = client_id_for_card_ref(sc_card_ref); + if (client_id != 0xFF) { + return card->action_chain.check_flag(0x00000200 << client_id); + } + return false; + } + default: + return false; + } + throw logic_error("this should be impossible"); + + case 'o': { + uint8_t v = atoi(arg2_text + 1); + if ((v / 10) == 1) { + auto new_card = this->server()->card_for_set_card_ref(set_card_ref); + if (!new_card) { + new_card = this->server()->card_for_set_card_ref(sc_card_ref); + } + if (new_card) { + card = new_card; + } + } + return (this->find_condition_with_parameters( + card, ConditionType::ANY, set_card_ref, ((v % 10) == 0) ? 0xFF : (v % 10)) != nullptr); + } + case 'r': + return !set_card_has_ability_trap && (random_percent < atoi(arg2_text + 1)); + case 's': { + auto ce = card->get_definition(); + return ((ce->def.self_cost >= arg2_text[1] - '0') && + (ce->def.self_cost <= arg2_text[2] - '0')); + } + case 't': { + auto set_card = this->server()->card_for_set_card_ref(set_card_ref); + if (!set_card) { + return false; + } + uint8_t v = atoi(arg2_text + 1); + // TODO: Figure out what this logic actually does and rename the variables + // or comment it appropriately. + if (when == 4) { + uint32_t y = set_card->unknown_a9 & 0xFFFFFFFE; + if ((set_card->unknown_a9 > 0) && + (y == (y / (v & 0xFFFFFFFE)) * (v & 0xFFFFFFFE))) { + return true; + } + } else { + uint32_t y = set_card->unknown_a9; + if ((set_card->unknown_a9 > 0) && + (y == (y / (v + 1)) * (v + 1))) { + return true; + } + } + return false; + } + default: + return false; + } + throw logic_error("this should be impossible"); +} + +int32_t CardSpecial::evaluate_effect_expr( + const AttackEnvStats& ast, + const char* expr, + DiceRoll& dice_roll) const { + // Note: This implementation is not based on the original code because the + // original code was hard to follow - it used a look-behind approach with lots + // of local variables instead of the look-ahead approach that this + // implementation uses. Hopefully this implementation is easier to follow. + vector> tokens; + while (expr) { + ExpressionTokenType type; + int32_t value = 0; + expr = this->get_next_expr_token(expr, &type, &value); + if (expr) { + if (type == ExpressionTokenType::SPACE) { + throw runtime_error("expression contains space token"); + } + // Turn references into numbers, so only numbers and operators can appear + // in the tokens vector + if (type == ExpressionTokenType::REFERENCE) { + if ((value == 1) || (value == 11)) { + dice_roll.value_used_in_expr = true; + } + tokens.emplace_back(make_pair(ExpressionTokenType::NUMBER, ast.at(value))); + } else { + tokens.emplace_back(make_pair(type, value)); + } + } + } + + // Operators are evaluated left-to-right - there are no operator precedence + // rules + int32_t value = 0; + for (size_t token_index = 0; token_index < tokens.size(); token_index++) { + auto token_type = tokens[token_index].first; + int32_t token_value = tokens[token_index].second; + if ((token_type == ExpressionTokenType::SPACE) || (token_type == ExpressionTokenType::REFERENCE)) { + throw logic_error("space or reference token present in expr evaluation phase 2"); + } + if (token_type == ExpressionTokenType::NUMBER) { + value = token_value; + } else { + if (token_index >= tokens.size() - 1) { + throw runtime_error("no token on right side of binary operator"); + } + token_index++; + auto right_token_type = tokens[token_index].first; + auto right_value = tokens[token_index].second; + if (right_token_type != ExpressionTokenType::NUMBER) { + throw runtime_error("non-number, non-reference token on right side of operator"); + } + switch (token_type) { + case ExpressionTokenType::ROUND_DIVIDE: + value = lround(value / right_value); + break; + case ExpressionTokenType::SUBTRACT: + value -= right_value; + break; + case ExpressionTokenType::ADD: + value += right_value; + break; + case ExpressionTokenType::MULTIPLY: + value *= right_value; + break; + case ExpressionTokenType::FLOOR_DIVIDE: + value = floor(value / right_value); + break; + default: + throw logic_error("invalid binary operator"); + } + } + } + + return value; +} + +bool CardSpecial::execute_effect( + Condition& cond, + shared_ptr card, + int16_t expr_value, + int16_t unknown_p5, + ConditionType cond_type, + uint unknown_p7, + uint16_t attacker_card_ref) { + int16_t clamped_expr_value = clamp(expr_value, -99, 99); + int16_t clamped_unknown_p5 = clamp(unknown_p5, -99, 99); + + cond.value8 = clamped_expr_value; + if (this->card_ref_has_ability_trap(cond)) { + return false; + } + if (card->card_flags & 1) { + return false; + } + + if ((card->card_flags & 3) || + (card->action_metadata.check_flag(0x10) && + (cond.card_ref != card->get_card_ref()) && + (cond.condition_giver_card_ref != card->get_card_ref()))) { + unknown_p7 = unknown_p7 & 0xFFFFFFFB; + } + if (unknown_p7 == 0) { + return false; + } + + int16_t positive_expr_value = max(0, clamped_expr_value); + clamped_unknown_p5 = max(0, clamped_unknown_p5); + auto attacker_sc = this->server()->card_for_set_card_ref(attacker_card_ref); + auto attack_medium = attacker_sc ? attacker_sc->action_chain.chain.attack_medium : AttackMedium::UNKNOWN; + + switch (cond_type) { + case ConditionType::RAMPAGE: + case ConditionType::IMMOBILE: + case ConditionType::HOLD: + case ConditionType::UNKNOWN_07: + case ConditionType::GUOM: + case ConditionType::PARALYZE: + case ConditionType::PIERCE: + case ConditionType::UNKNOWN_0F: + case ConditionType::UNKNOWN_12: + case ConditionType::UNKNOWN_13: + case ConditionType::ACID: + case ConditionType::UNKNOWN_15: + case ConditionType::ABILITY_TRAP: + case ConditionType::FREEZE: + case ConditionType::MAJOR_PIERCE: + case ConditionType::HEAVY_PIERCE: + case ConditionType::MAJOR_RAMPAGE: + case ConditionType::HEAVY_RAMPAGE: + case ConditionType::DEF_DISABLE_BY_COST: + default: + return false; + + case ConditionType::UNKNOWN_39: + case ConditionType::DEFENDER: + case ConditionType::SURVIVAL_DECOYS: + case ConditionType::EXP_DECOY: + case ConditionType::SET_MV: + case ConditionType::MV_BONUS: + return true; + + case ConditionType::AP_BOOST: + if (unknown_p7 & 1) { + card->action_chain.chain.ap_effect_bonus = clamp( + card->action_chain.chain.ap_effect_bonus + positive_expr_value, -99, 99); + } + return true; + + case ConditionType::MULTI_STRIKE: + if (unknown_p7 & 1) { + card->action_chain.chain.strike_count = positive_expr_value; + } + return true; + + case ConditionType::DAMAGE_MOD_1: + if (unknown_p7 & 2) { + card->action_chain.chain.damage = positive_expr_value; + } + return true; + + case ConditionType::TP_BOOST: + if (unknown_p7 & 1) { + card->action_chain.chain.tp_effect_bonus = clamp( + card->action_chain.chain.tp_effect_bonus + positive_expr_value, -99, 99); + } + return true; + + case ConditionType::GIVE_DAMAGE: + if ((unknown_p7 & 4) != 0) { + int16_t current_hp = clamp(card->get_current_hp(), -99, 99); + int16_t new_hp = clamp(current_hp - positive_expr_value, -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x20, -positive_expr_value, 0, 1); + new_hp = max(new_hp, 0); + if (new_hp != current_hp) { + card->set_current_hp(new_hp); + this->destroy_card_if_hp_zero(card, attacker_card_ref); + } + } + return true; + + case ConditionType::UNKNOWN_0C: + case ConditionType::A_T_SWAP_PERM: + if (unknown_p7 & 4) { + int16_t ap = clamp(card->ap, -99, 99); + int16_t tp = clamp(card->tp, -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0xA0, tp - ap, 0, 0); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x80, ap - tp, 0, 0); + card->ap = tp; + card->tp = ap; + cond.flags |= 2; + } + return true; + + case ConditionType::A_H_SWAP: + case ConditionType::A_H_SWAP_PERM: + if (unknown_p7 & 4) { + int16_t ap = clamp(card->ap, -99, 99); + int16_t hp = clamp(card->get_current_hp(), -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0xA0, hp - ap, 0, 0); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x20, ap - hp, 1, 0); + cond.flags |= 2; + if (ap != hp) { + card->set_current_hp(ap); + card->ap = hp; + this->destroy_card_if_hp_zero(card, attacker_card_ref); + } + } + return true; + + case ConditionType::HEAL: + if (unknown_p7 & 4) { + int16_t hp = clamp(card->get_current_hp(), -99, 99); + int16_t new_hp = clamp(hp + positive_expr_value, -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x20, new_hp - hp, 1, 1); + if (new_hp != hp) { + card->set_current_hp(new_hp); + this->destroy_card_if_hp_zero(card, attacker_card_ref); + } + } + return true; + + case ConditionType::RETURN_TO_HAND: + if (unknown_p7 & 4) { + uint8_t client_id = client_id_for_card_ref(card->get_card_ref()); + if (client_id == 0xFF) { + return false; + } + auto ps = this->server()->player_states[client_id]; + if (!ps) { + return false; + } + if ((card->card_flags & 2) || this->card_is_destroyed(card)) { + return true; + } + this->send_6xB4x06_for_card_destroyed(card, attacker_card_ref); + card->unknown_802380C0(); + if (!ps->return_set_card_to_hand1(card->get_card_ref())) { + return ps->discard_card_or_add_to_draw_pile(card->get_card_ref(), false); + } + } + return false; + + case ConditionType::MIGHTY_KNUCKLE: { + auto ps = card->player_state(); + uint8_t atk = ps->get_atk_points(); + if (unknown_p7 & 1) { + card->action_chain.chain.ap_effect_bonus = clamp( + card->action_chain.chain.ap_effect_bonus + clamped_unknown_p5, -99, 99); + } + if (unknown_p7 & 4) { + ps->subtract_atk_points(atk); + } + return true; + } + + case ConditionType::UNIT_BLOW: + if (unknown_p7 & 1) { + int16_t count = clamp(this->count_action_cards_with_condition_for_all_current_attacks(ConditionType::UNIT_BLOW, 0xFFFF), -99, 99); + card->action_chain.chain.ap_effect_bonus = clamp(card->action_chain.chain.ap_effect_bonus + count * positive_expr_value, -99, 99); + } + return false; + + case ConditionType::CURSE: + if (unknown_p7 & 4) { + for (size_t z = 0; z < card->action_chain.chain.target_card_ref_count; z++) { + auto target_card = this->server()->card_for_set_card_ref( + card->action_chain.chain.target_card_refs[z]); + if (target_card) { + CardShortStatus stat = target_card->get_short_status(); + if (stat.card_flags & 2) { + int16_t hp = clamp(card->get_current_hp(), -99, 99); + int16_t new_hp = max(0, hp - 1); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x20, -1, 0, 1); + if (hp != new_hp) { + card->set_current_hp(new_hp); + this->destroy_card_if_hp_zero(card, attacker_card_ref); + } + } + } + } + } + return true; + + case ConditionType::COMBO_AP: + if (unknown_p7 & 1) { + int16_t count = clamp(this->count_action_cards_with_condition_for_all_current_attacks(ConditionType::COMBO_AP, 0xFFFF), -99, 99); + card->action_chain.chain.ap_effect_bonus = clamp( + card->action_chain.chain.ap_effect_bonus + count * count, -99, 99); + } + return false; + + case ConditionType::PIERCE_RAMPAGE_BLOCK: + if (unknown_p7 & 4) { + card->action_chain.set_flags(0x40); + } + if (unknown_p7 & 3) { + card->action_chain.reset(); + } + return true; + + case ConditionType::ANTI_ABNORMALITY_1: + if (unknown_p7 & 4) { + for (ssize_t z = 8; z >= 0; z--) { + auto& cond = card->action_chain.conditions[z]; + if ((cond.type == ConditionType::IMMOBILE) || + (cond.type == ConditionType::HOLD) || + (cond.type == ConditionType::UNKNOWN_07) || + (cond.type == ConditionType::GUOM) || + (cond.type == ConditionType::PARALYZE) || + (cond.type == ConditionType::UNKNOWN_13) || + (cond.type == ConditionType::ACID) || + (cond.type == ConditionType::UNKNOWN_15) || + (cond.type == ConditionType::CURSE) || + (cond.type == ConditionType::PIERCE_RAMPAGE_BLOCK) || + (cond.type == ConditionType::FREEZE) || + (cond.type == ConditionType::UNKNOWN_1E) || + (cond.type == ConditionType::DROP)) { + 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, 0x0C); + cmd.effect.target_card_ref = card->get_card_ref(); + cmd.effect.value = 0; + cmd.effect.operation = -static_cast(cond.type); + cmd.effect.condition_index = z; + this->server()->send(cmd); + this->apply_stat_deltas_to_card_from_condition_and_clear_cond( + cond, card); + card->send_6xB4x4E_4C_4D_if_needed(); + } + } + } + return false; + + case ConditionType::UNKNOWN_1E: + if (unknown_p7 & 4) { + auto sc_card = this->server()->card_for_set_card_ref(attacker_card_ref); + if (!sc_card || (sc_card->action_chain.chain.attack_medium == AttackMedium::PHYSICAL)) { + int16_t hp = clamp(card->get_current_hp(), -99, 99); + int16_t new_hp = lround(hp * 0.5f); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x20, new_hp - hp, 0, 1); + if (new_hp != hp) { + card->set_current_hp(new_hp); + this->destroy_card_if_hp_zero(card, attacker_card_ref); + } + } + } + return true; + + case ConditionType::EXPLOSION: + if (unknown_p7 & 0x40) { + int16_t count = clamp(this->count_action_cards_with_condition_for_all_current_attacks(ConditionType::EXPLOSION, 0xFFFF), -99, 99); + card->action_metadata.attack_bonus = clamp(count * count, -99, 99); + } + return false; + + case ConditionType::UNKNOWN_22: + if (unknown_p7 & 4) { + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x80, positive_expr_value - card->tp, 0, 1); + card->tp = positive_expr_value; + } + return true; + + case ConditionType::RETURN_TO_DECK: { + if (!(unknown_p7 & 4)) { + return true; + } + uint8_t client_id = client_id_for_card_ref(card->get_card_ref()); + if (client_id == 0xFF) { + return false; + } + auto ps = this->server()->player_states[client_id]; + if (!ps) { + return false; + } + card->unknown_802380C0(); + return ps->discard_card_or_add_to_draw_pile(card->get_card_ref(), true); + } + + case ConditionType::AP_LOSS: + if (unknown_p7 & 1) { + card->action_chain.chain.ap_effect_bonus = clamp( + card->action_chain.chain.ap_effect_bonus - positive_expr_value, -99, 99); + } + return true; + + case ConditionType::BONUS_FROM_LEADER: + if (unknown_p7 & 1) { + clamped_unknown_p5 = this->count_cards_with_card_id_set_by_player_except_card_ref(expr_value, 0xFFFF) + + (card->action_chain).chain.ap_effect_bonus; + (card->action_chain).chain.ap_effect_bonus = clamp(clamped_unknown_p5, -99, 99); + } + return true; + + case ConditionType::FILIAL: { + if (unknown_p7 & 4) { + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x20, positive_expr_value, 0, 1); + if (positive_expr_value != 0) { + int16_t hp = clamp(card->get_current_hp(), -99, 99); + int16_t new_hp = clamp(hp + positive_expr_value, -99, 99); + card->set_current_hp(new_hp, true, false); + this->destroy_card_if_hp_zero(card, attacker_card_ref); + } + } + return true; + } + + case ConditionType::SNATCH: + if (unknown_p7 & 4) { + uint8_t attacker_client_id = client_id_for_card_ref(cond.card_ref); + uint8_t target_client_id = client_id_for_card_ref(card->get_card_ref()); + if ((attacker_client_id != 0xFF) && (target_client_id != 0xFF)) { + auto attacker_ps = this->server()->player_states[attacker_client_id]; + auto target_ps = this->server()->player_states[target_client_id]; + if (attacker_ps && target_ps) { + uint8_t attacker_team_id = attacker_ps->get_team_id(); + uint8_t target_team_id = target_ps->get_team_id(); + if (positive_expr_value < this->server()->team_exp[target_team_id]) { + this->server()->team_exp[attacker_team_id] += positive_expr_value; + this->server()->team_exp[target_team_id] -= positive_expr_value; + } else { + positive_expr_value = this->server()->team_exp[target_team_id]; + this->server()->team_exp[attacker_team_id] += this->server()->team_exp[target_team_id]; + this->server()->team_exp[target_team_id] = 0; + } + this->compute_team_dice_boost(attacker_team_id); + this->compute_team_dice_boost(target_team_id); + this->send_6xB4x06_for_exp_change(card, attacker_card_ref, -positive_expr_value, 1); + this->server()->update_battle_state_flags_and_send_6xB4x03_if_needed(); + } + } + } + return true; + + case ConditionType::HAND_DISRUPTER: { + if (unknown_p7 & 4) { + auto ps = card->player_state(); + for (; positive_expr_value > 0; positive_expr_value--) { + size_t hand_size = ps->get_hand_size(); + if (hand_size > 0) { + uint8_t a = ps->roll_dice_with_effects(2); + uint8_t b = ps->roll_dice_with_effects(1); + uint16_t card_ref = ps->card_ref_for_hand_index( + (a + b) - ((a + b) / hand_size) * hand_size); + if (card_ref != 0xFFFF) { + ps->discard_ref_from_hand(card_ref); + } + } else { + break; + } + } + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + return true; + } + + case ConditionType::DROP: + if (unknown_p7 & 4) { + auto ps = card->player_state(); + if (ps) { + uint8_t team_id = ps->get_team_id(); + int16_t delta = 0; + if (this->server()->team_exp[team_id] < 4) { + this->server()->team_exp[team_id] = 0; + } else { + delta = -3; + this->server()->team_exp[team_id] -= 3; + } + this->compute_team_dice_boost(team_id); + this->send_6xB4x06_for_exp_change(card, attacker_card_ref, delta, 1); + } + } + return true; + + case ConditionType::ACTION_DISRUPTER: + if (unknown_p7 & 4) { + for (size_t z = 0; z < card->action_chain.chain.attack_action_card_ref_count; z++) { + this->apply_stat_deltas_to_all_cards_from_all_conditions_with_card_ref(card->action_chain.chain.attack_action_card_refs[z]); + } + card->action_chain.chain.attack_action_card_ref_count = 0; + } + return true; + + case ConditionType::SET_HP: { + if ((unknown_p7 & 4) && (card->action_metadata.defense_power < 99)) { + int16_t hp = card->get_current_hp(); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x20, positive_expr_value - hp, 0, 1); + if (hp != positive_expr_value) { + card->set_current_hp(positive_expr_value, true, false); + this->destroy_card_if_hp_zero(card, attacker_card_ref); + } + } + return true; + } + + case ConditionType::NATIVE_SHIELD: + return this->apply_attribute_guard_if_possible( + unknown_p7, CardClass::NATIVE_CREATURE, card, cond.condition_giver_card_ref, attacker_card_ref); + + case ConditionType::A_BEAST_SHIELD: + return this->apply_attribute_guard_if_possible( + unknown_p7, CardClass::A_BEAST_CREATURE, card, cond.condition_giver_card_ref, attacker_card_ref); + + case ConditionType::MACHINE_SHIELD: + return this->apply_attribute_guard_if_possible( + unknown_p7, CardClass::MACHINE_CREATURE, card, cond.condition_giver_card_ref, attacker_card_ref); + + case ConditionType::DARK_SHIELD: + return this->apply_attribute_guard_if_possible( + unknown_p7, CardClass::DARK_CREATURE, card, cond.condition_giver_card_ref, attacker_card_ref); + + case ConditionType::SWORD_SHIELD: + return this->apply_attribute_guard_if_possible( + unknown_p7, CardClass::SWORD_ITEM, card, cond.condition_giver_card_ref, attacker_card_ref); + + case ConditionType::GUN_SHIELD: + return this->apply_attribute_guard_if_possible( + unknown_p7, CardClass::GUN_ITEM, card, cond.condition_giver_card_ref, attacker_card_ref); + + case ConditionType::CANE_SHIELD: + return this->apply_attribute_guard_if_possible( + unknown_p7, CardClass::CANE_ITEM, card, cond.condition_giver_card_ref, attacker_card_ref); + + case ConditionType::UNKNOWN_38: { + auto ps = card->player_state(); + if (ps && (unknown_p7 & 4)) { + ps->subtract_def_points(ps->get_def_points() - positive_expr_value); + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + return true; + } + + case ConditionType::GIVE_OR_TAKE_EXP: + if (unknown_p7 & 4) { + uint8_t client_id = client_id_for_card_ref(card->get_card_ref()); + if ((client_id != 0xFF) && this->server()->player_states[client_id]) { + uint8_t team_id = this->server()->player_states[client_id]->get_team_id(); + int32_t existing_exp = this->server()->team_exp[team_id]; + if ((clamped_expr_value + existing_exp) < 0) { + clamped_expr_value = -existing_exp; + this->server()->team_exp[team_id] = 0; + } else { + this->server()->team_exp[team_id] = existing_exp + clamped_expr_value; + } + this->send_6xB4x06_for_exp_change(card, attacker_card_ref, clamped_expr_value, 1); + this->compute_team_dice_boost(team_id); + this->server()->update_battle_state_flags_and_send_6xB4x03_if_needed(); + } + } + return true; + + case ConditionType::UNKNOWN_3D: + if (unknown_p7 & 4) { + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0xA0, positive_expr_value - card->ap, 0, 1); + card->ap = positive_expr_value; + } + return true; + + case ConditionType::DEATH_COMPANION: + if (attacker_sc && (unknown_p7 & 4)) { + vector card_refs; + card_refs.emplace_back(attacker_sc->get_card_ref()); + if (attacker_sc != card) { + card_refs.emplace_back(card->get_card_ref()); + } + + for (uint16_t card_ref : card_refs) { + auto sc_card = this->server()->card_for_set_card_ref(card_ref); + if (sc_card && (sc_card->get_current_hp() > 0)) { + if (this->server()->ruler_server->check_usability_or_apply_condition_for_card_refs( + cond.card_ref, cond.condition_giver_card_ref, + sc_card->get_card_ref(), cond.card_definition_effect_index, + attack_medium)) { + this->send_6xB4x06_for_stat_delta(sc_card, attacker_card_ref, 0x20, -sc_card->get_current_hp(), 0, 1); + sc_card->set_current_hp(0); + this->destroy_card_if_hp_zero(sc_card, attacker_card_ref); + } + } + } + } + return false; + + case ConditionType::GROUP: + if (unknown_p7 & 1) { + auto ce = card->get_definition(); + if (ce) { + int16_t count = clamp( + this->count_cards_with_card_id_set_by_player_except_card_ref(ce->def.card_id, card->get_card_ref()), -99, 99); + card->action_chain.chain.ap_effect_bonus = clamp( + card->action_chain.chain.ap_effect_bonus + count * positive_expr_value, -99, 99); + } + } + return true; + + case ConditionType::BERSERK: + if (unknown_p7 & 4) { + int16_t hp = clamp(card->get_current_hp(), -99, 99); + int16_t new_hp = clamp(hp - this->max_all_attack_bonuses(nullptr), -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x20, new_hp - hp, 0, 1); + new_hp = max(new_hp, 0); + if (new_hp != hp) { + card->set_current_hp(new_hp); + this->destroy_card_if_hp_zero(card, attacker_card_ref); + } + } + return true; + + case ConditionType::UNKNOWN_49: + if (unknown_p7 & 4) { + auto attacker_card = this->server()->card_for_set_card_ref(attacker_card_ref); + if (attacker_card && (attacker_card != card)) { + for (ssize_t z = 8; z >= 0; z--) { + this->apply_stat_deltas_to_card_from_condition_and_clear_cond( + attacker_card->action_chain.conditions[z], attacker_card); + } + for (size_t z = 0; z < 9; z++) { + attacker_card->action_chain.conditions[z] = card->action_chain.conditions[z]; + } + for (size_t z = 0; z < 9; z++) { + auto& cond = attacker_card->action_chain.conditions[z]; + if (cond.type != ConditionType::UNKNOWN_49) { + this->execute_effect( + cond, attacker_card, positive_expr_value, clamped_unknown_p5, cond.type, unknown_p7, attacker_card_ref); + } + } + } + } + return true; + + case ConditionType::AP_GROWTH: + if (unknown_p7 & 4) { + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0xA0, positive_expr_value, 0, 1); + card->ap = clamp(card->ap + positive_expr_value, -99, 99); + } + return true; + + case ConditionType::TP_GROWTH: + if (unknown_p7 & 4) { + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x80, positive_expr_value, 0, 1); + card->tp = clamp(card->tp + positive_expr_value, -99, 99); + } + return true; + + case ConditionType::COPY: + if (unknown_p7 & 4) { + auto attacker_card = this->server()->card_for_set_card_ref(attacker_card_ref); + if (attacker_card && (attacker_card != card)) { + int16_t new_ap = clamp((positive_expr_value < 51) ? (card->ap / 2) : card->ap, -99, 99); + int16_t new_tp = clamp((positive_expr_value < 51) ? (card->tp / 2) : card->tp, -99, 99); + this->send_6xB4x06_for_stat_delta( + attacker_card, attacker_card_ref, 0xA0, new_ap - attacker_card->ap, 0, 0); + this->send_6xB4x06_for_stat_delta( + attacker_card, attacker_card_ref, 0x80, new_tp - attacker_card->tp, 0, 0); + attacker_card->ap = new_ap; + attacker_card->tp = new_tp; + } + } + return true; + + case ConditionType::MISC_GUARDS: + if (unknown_p7 & 8) { + card->action_metadata.defense_bonus = clamp( + positive_expr_value + card->action_metadata.defense_bonus, -99, 99); + } + return true; + + case ConditionType::AP_OVERRIDE: + if ((unknown_p7 & 4) && !(cond.flags & 2)) { + cond.value = clamp(positive_expr_value - card->ap, -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0xA0, cond.value, 0, 0); + card->ap = positive_expr_value; + cond.flags |= 2; + } + return true; + + case ConditionType::TP_OVERRIDE: + if ((unknown_p7 & 4) && !(cond.flags & 2)) { + cond.value = clamp(positive_expr_value - card->tp, -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x80, cond.value, 0, 0); + card->tp = positive_expr_value; + cond.flags |= 2; + } + return true; + + case ConditionType::SLAYERS_ASSASSINS: + case ConditionType::UNKNOWN_64: + case ConditionType::FORWARD_DAMAGE: + if (unknown_p7 & 0x20) { + card->action_metadata.attack_bonus = clamp( + positive_expr_value + card->action_metadata.attack_bonus, -99, 99); + } + return true; + + case ConditionType::BLOCK_ATTACK: + if (unknown_p7 & 4) { + card->action_metadata.set_flags(0x10); + } + return true; + + case ConditionType::COMBO_TP: + if (unknown_p7 & 1) { + ssize_t count = this->count_cards_with_card_id_set_by_player_except_card_ref( + expr_value, 0xFFFF); + card->action_chain.chain.tp_effect_bonus = clamp( + count + card->action_chain.chain.tp_effect_bonus, -99, 99); + } + return true; + + case ConditionType::MISC_AP_BONUSES: + if ((unknown_p7 & 4) && !(cond.flags & 2)) { + int16_t orig_ap = clamp(card->ap, -99, 99); + card->ap = clamp(positive_expr_value + card->ap, 0, 99); + cond.value = clamp(card->ap - orig_ap, -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0xA0, cond.value, 0, 0); + cond.flags |= 2; + } + return false; + + case ConditionType::MISC_TP_BONUSES: + if ((unknown_p7 & 4) && !(cond.flags & 2)) { + int16_t orig_tp = clamp(card->tp, -99, 99); + card->tp = clamp(positive_expr_value + card->tp, 0, 99); + cond.value = clamp(card->tp - orig_tp, -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x80, cond.value, 0, 0); + cond.flags |= 2; + } + return false; + + case ConditionType::MISC_DEFENSE_BONUSES: + case ConditionType::WEAK_SPOT_INFLUENCE: + if (unknown_p7 & 0x20) { + card->action_metadata.attack_bonus = clamp( + card->action_metadata.attack_bonus - positive_expr_value, 0, 99); + } + return true; + + case ConditionType::MOSTLY_HALFGUARDS: + case ConditionType::DAMAGE_MODIFIER_2: + if (unknown_p7 & 0x40) { + card->action_metadata.attack_bonus = positive_expr_value; + } + return true; + + case ConditionType::PERIODIC_FIELD: + if ((unknown_p7 & 0x40) && + (static_cast(attack_medium) == ((this->server()->get_round_num() >> 1) & 1) + 1)) { + card->action_metadata.attack_bonus = 0; + } + return true; + + case ConditionType::AP_SILENCE: + if ((unknown_p7 & 4) && !(cond.flags & 2)) { + int16_t prev_ap = clamp(card->ap, -99, 99); + card->ap = clamp(card->ap - positive_expr_value, 0, 99); + cond.value = clamp(prev_ap - card->ap, -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0xA0, -cond.value, 0, 0); + cond.flags |= 2; + } + return false; + + case ConditionType::TP_SILENCE: + if ((unknown_p7 & 4) && !(cond.flags & 2)) { + int16_t prev_ap = clamp(card->tp, -99, 99); + card->tp = clamp(card->tp - positive_expr_value, 0, 99); + cond.value = clamp(prev_ap - card->tp, -99, 99); + this->send_6xB4x06_for_stat_delta(card, attacker_card_ref, 0x80, -cond.value, 0, 0); + cond.flags |= 2; + } + return false; + + case ConditionType::RAMPAGE_AP_LOSS: + if (unknown_p7 & 1) { + card->action_chain.chain.tp_effect_bonus = clamp( + card->action_chain.chain.tp_effect_bonus - positive_expr_value, -99, 99); + } + return true; + + case ConditionType::UNKNOWN_77: + if (attacker_sc && (unknown_p7 & 4)) { + vector card_refs; + card_refs.emplace_back(attacker_sc->get_card_ref()); + for (size_t z = 0; z < attacker_sc->action_chain.chain.target_card_ref_count; z++) { + card_refs.emplace_back(attacker_sc->action_chain.chain.target_card_refs[z]); + } + + for (uint16_t card_ref : card_refs) { + auto set_card = this->server()->card_for_set_card_ref(card_ref); + if (set_card && (set_card->get_current_hp() > 0)) { + if (this->server()->ruler_server->check_usability_or_apply_condition_for_card_refs( + cond.card_ref, + cond.condition_giver_card_ref, + set_card->get_card_ref(), + cond.card_definition_effect_index, + attack_medium)) { + this->send_6xB4x06_for_stat_delta( + set_card, attacker_card_ref, 0x20, -set_card->get_current_hp(), 0, 1); + set_card->set_current_hp(0); + this->destroy_card_if_hp_zero(set_card, attacker_card_ref); + } + } + } + } + return false; + } +} + +const Condition* CardSpecial::find_condition_with_parameters( + shared_ptr card, + ConditionType cond_type, + uint16_t set_card_ref, + uint8_t def_effect_index) const { + const Condition* ret = nullptr; + uint8_t max_order = 9; + for (size_t z = 0; z < 9; z++) { + if (card->action_chain.conditions[z].type == ConditionType::NONE) { + continue; + } + auto& cond = card->action_chain.conditions[z]; + auto orig_eff = this->original_definition_for_condition(cond); + if (!this->card_ref_has_ability_trap(cond) && + ((cond_type == ConditionType::ANY) || (cond.type == cond_type)) && + ((set_card_ref == 0xFFFF) || (cond.card_ref == set_card_ref)) && + ((def_effect_index == 0xFF) || (orig_eff && (orig_eff->effect_num == def_effect_index))) && + (!ret || (max_order < cond.order))) { + max_order = cond.order; + ret = &cond; + } + } + return ret; +} + +Condition* CardSpecial::find_condition_with_parameters( + shared_ptr card, + ConditionType cond_type, + uint16_t set_card_ref, + uint8_t def_effect_index) const { + return const_cast(this->find_condition_with_parameters( + static_cast>(card), cond_type, set_card_ref, def_effect_index)); +} + +void CardSpecial::get_card1_loc_with_card2_opposite_direction( + Location* out_loc, + shared_ptr card1, + shared_ptr card2) { + if (card1) { + if (!card2 || (static_cast(card2->facing_direction) & 0x80)) { + *out_loc = card1->loc; + } else if ((card2->loc.x == card1->loc.x) && (card2->loc.y == card1->loc.y)) { + *out_loc = card1->loc; + out_loc->direction = card2->facing_direction; + } else { + *out_loc = card1->loc; + out_loc->direction = turn_around(card2->facing_direction); + } + } +} + +uint16_t CardSpecial::get_card_id_with_effective_range( + shared_ptr card1, uint16_t default_card_id, shared_ptr card2) const { + if (card2 && !(static_cast(card2->facing_direction) & 0x80)) { + return this->server()->ruler_server->get_card_id_with_effective_range( + card1 ? card1->get_card_ref() : 0xFFFF, default_card_id, 0); + } + return default_card_id; +} + +void CardSpecial::get_effective_ap_tp( + StatSwapType type, + int16_t* effective_ap, + int16_t* effective_tp, + int16_t hp, + int16_t ap, + int16_t tp) { + switch (type) { + case StatSwapType::NONE: + *effective_ap = ap; + *effective_tp = tp; + break; + case StatSwapType::A_T_SWAP: + *effective_ap = tp; + *effective_tp = ap; + break; + case StatSwapType::A_H_SWAP: + *effective_ap = hp; + *effective_tp = tp; + break; + default: + throw logic_error("invalid stat swap state"); + } +} + +const char* CardSpecial::get_next_expr_token( + const char *expr, ExpressionTokenType* out_type, int32_t* out_value) const { + switch (*expr) { + case '\0': + *out_type = ExpressionTokenType::SPACE; + return nullptr; + case ' ': + *out_type = ExpressionTokenType::SPACE; + return expr + 1; + case '+': + *out_type = ExpressionTokenType::ADD; + return expr + 1; + case '-': + *out_type = ExpressionTokenType::SUBTRACT; + return expr + 1; + case '*': + *out_type = ExpressionTokenType::MULTIPLY; + return expr + 1; + case '/': + if (expr[1] == '/') { + *out_type = ExpressionTokenType::FLOOR_DIVIDE; + return expr + 2; + } else { + *out_type = ExpressionTokenType::ROUND_DIVIDE; + return expr + 1; + } + } + + if ((*expr >= 'a') && (*expr <= 'z')) { + string token_buf; + for (; ('a' <= *expr) && (*expr < 'z'); expr++) { + token_buf.push_back(*expr); + } + + *out_type = ExpressionTokenType::SPACE; + *out_value = 0x27; + + static const vector tokens({ + "f", "d", "ap", "tp", "hp", "mhp", "dm", "tdm", "tf", "ac", "php", + "dc", "cs", "a", "kap", "ktp", "dn", "hf", "df", "ff", "ef", "bi", + "ab", "mc", "dk", "sa", "gn", "wd", "tt", "lv", "adm", "ddm", "sat", + "edm", "ldm", "rdm", "fdm", "ndm", "ehp"}); + for (size_t z = 0; z < tokens.size(); z++) { + if (token_buf == tokens[z]) { + *out_type = ExpressionTokenType::REFERENCE; + *out_value = z; + return expr; + } + } + return expr; + } + + if ((*expr >= '0') && (*expr <= '9')) { + *out_type = ExpressionTokenType::NUMBER; + *out_value = strtol(expr, const_cast(&expr), 10); + return expr; + } + + throw runtime_error("invalid card effect expression"); +} + +vector> CardSpecial::get_targeted_cards_for_condition( + uint16_t card_ref, + uint8_t def_effect_index, + uint16_t setter_card_ref, + const ActionState& as, + int16_t p_target_type, + bool apply_usability_filters) const { + vector> ret; + + uint8_t client_id = client_id_for_card_ref(card_ref); + auto card1 = this->server()->card_for_set_card_ref(card_ref); + if (!card1) { + card1 = this->server()->card_for_set_card_ref(setter_card_ref); + } + + auto card2 = this->server()->card_for_set_card_ref((as.attacker_card_ref == 0xFFFF) + ? as.original_attacker_card_ref : as.attacker_card_ref); + + Location card1_loc; + if (!card1) { + card1_loc.x = 0; + card1_loc.y = 0; + card1_loc.direction = Direction::RIGHT; + } else { + this->get_card1_loc_with_card2_opposite_direction(&card1_loc, card1, card2); + } + + AttackMedium attack_medium = card2 + ? card2->action_chain.chain.attack_medium + : AttackMedium::UNKNOWN; + + auto add_card_refs = [&](const vector& result_card_refs) -> void { + for (uint16_t result_card_ref : result_card_refs) { + auto result_card = this->server()->card_for_set_card_ref(result_card_ref); + if (result_card) { + ret.emplace_back(result_card); + } + } + }; + + switch (p_target_type) { + case 1: + case 5: { + auto result_card = this->server()->card_for_set_card_ref(setter_card_ref); + if (result_card) { + ret.emplace_back(result_card); + } + break; + } + case 2: + if (as.original_attacker_card_ref == 0xFFFF) { + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto result_card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (result_card) { + ret.emplace_back(result_card); + } + } + } else if (card2) { + ret.emplace_back(card2); + } + break; + case 3: + if (card1) { + auto ce = this->server()->definition_for_card_ref(card_ref); + auto ps = card1->player_state(); + if (ce && ps) { + uint16_t range_card_id = this->get_card_id_with_effective_range(card1, ce->def.card_id, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, range_card_id, card1_loc, this->server()->base()->map_and_rules1); + add_card_refs(ps->get_card_refs_within_range_from_all_players(range, card1_loc, CardType::ITEM)); + } + } + break; + case 4: + size_t z; + for (z = 0; (z < 9) && (as.action_card_refs[z] != 0xFFFF) && (as.action_card_refs[z] != card_ref); z++) { } + for (; (z < 9) && (as.action_card_refs[z] != 0xFFFF); z++) { + auto result_card = this->server()->card_for_set_card_ref(as.action_card_refs[z]); + if (result_card) { + ret.emplace_back(result_card); + } + } + break; + case 6: + ret = this->get_attacker_card_and_sc_if_item(as); + break; + case 7: { + auto card = this->get_attacker_card(as); + if (card) { + ret.emplace_back(card); + } + break; + } + case 8: { + auto card = this->sc_card_for_client_id(client_id); + if (card) { + ret.emplace_back(card); + } + break; + } + case 9: + if (card1) { + auto ce = this->server()->definition_for_card_ref(card_ref); + auto ps = card1->player_state(); + if (ce && ps) { + uint16_t range_card_id = this->get_card_id_with_effective_range(card1, ce->def.card_id, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, range_card_id, card1_loc, this->server()->base()->map_and_rules1); + add_card_refs(ps->get_all_cards_within_range(range, card1_loc, card1->get_team_id())); + } + } + break; + case 10: + ret = this->find_all_cards_on_same_or_other_team(client_id, true); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 11: + ret = this->find_all_set_cards_on_client_team(client_id); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 12: + ret = this->find_all_cards_by_aerial_attribute(false); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 13: + ret = this->find_cards_by_condition_inc_exc(ConditionType::FREEZE); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 14: + ret = this->find_cards_in_hp_range(-1000, 3); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 15: + ret = this->get_all_set_cards(); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 16: + ret = this->find_cards_in_hp_range(8, 1000); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 17: { + auto result_card = this->server()->card_for_set_card_ref(card_ref); + if (result_card) { + ret.emplace_back(result_card); + } + break; + } + case 18: { + auto card = this->sc_card_for_client_id(client_id); + if (card) { + ret.emplace_back(card); + } + break; + } + case 19: + ret = this->find_all_sc_cards_of_class(CardClass::HU_SC); + break; + case 20: + ret = this->find_all_sc_cards_of_class(CardClass::RA_SC); + break; + case 21: + ret = this->find_all_sc_cards_of_class(CardClass::FO_SC); + break; + case 22: + if (card1) { + auto def = this->server()->definition_for_card_ref(card_ref); + auto ps = card1->player_state(); + if (def && ps) { + // TODO: Again, Sega hardcodes the Gifoie card's ID here... we + // should fix this eventually. + uint16_t range_card_id = this->get_card_id_with_effective_range(card1, 0x00D9, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, range_card_id, card1_loc, this->server()->base()->map_and_rules1); + auto result_card_refs = ps->get_all_cards_within_range(range, card1_loc, card1->get_team_id()); + for (uint16_t result_card_ref : result_card_refs) { + auto result_card = this->server()->card_for_set_card_ref(result_card_ref); + if (result_card && + (result_card->get_definition()->def.type != CardType::ITEM) && + (card1 != result_card)) { + ret.emplace_back(result_card); + } + } + if (card1) { + ret.emplace_back(card1); + } + } + } + break; + case 23: + if (card1) { + auto def = this->server()->definition_for_card_ref(card_ref); + auto ps = card1->player_state(); + if (def && ps) { + // TODO: Again with the Gifoie hardcoding... + uint16_t range_card_id = this->get_card_id_with_effective_range(card1, 0x00D9, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, range_card_id, card1_loc, this->server()->base()->map_and_rules1); + auto result_card_refs = ps->get_all_cards_within_range(range, card1_loc, 0xFF); + for (uint16_t result_card_ref : result_card_refs) { + auto result_card = this->server()->card_for_set_card_ref(result_card_ref); + if (result_card && + (result_card->get_definition()->def.type != CardType::ITEM)) { + ret.emplace_back(result_card); + } + } + } + } + break; + case 24: + ret = this->find_cards_by_condition_inc_exc(ConditionType::PARALYZE); + break; + case 25: + ret = this->find_all_cards_by_aerial_attribute(true); + break; + case 26: + ret = this->find_cards_damaged_by_at_least(1); + break; + case 27: + ret = this->get_all_set_cards_by_team_and_class(CardClass::NATIVE_CREATURE, 0xFF, false); + break; + case 28: + ret = this->get_all_set_cards_by_team_and_class(CardClass::A_BEAST_CREATURE, 0xFF, false); + break; + case 29: + ret = this->get_all_set_cards_by_team_and_class(CardClass::MACHINE_CREATURE, 0xFF, false); + break; + case 30: + ret = this->get_all_set_cards_by_team_and_class(CardClass::DARK_CREATURE, 0xFF, false); + break; + case 31: + ret = this->get_all_set_cards_by_team_and_class(CardClass::SWORD_ITEM, 0xFF, false); + break; + case 32: + ret = this->get_all_set_cards_by_team_and_class(CardClass::GUN_ITEM, 0xFF, false); + break; + case 33: + ret = this->get_all_set_cards_by_team_and_class(CardClass::CANE_ITEM, 0xFF, false); + break; + case 34: + if (as.original_attacker_card_ref == 0xFFFF) { + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto result_card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (result_card && + result_card->get_definition() && + !result_card->get_definition()->def.is_sc()) { + ret.emplace_back(result_card); + } + } + } else if (card2 && + card2->get_definition() && + !card2->get_definition()->def.is_sc()) { + ret.emplace_back(card2); + } + break; + case 35: + if (card1) { + auto def = this->server()->definition_for_card_ref(card_ref); + auto ps = card1->player_state(); + if (def && ps) { + // TODO: Again with the Gifoie hardcoding... + uint16_t range_card_id = this->get_card_id_with_effective_range(card1, 0x00D9, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, range_card_id, card1_loc, this->server()->base()->map_and_rules1); + auto result_card_refs = ps->get_all_cards_within_range(range, card1_loc, 0xFF); + for (uint16_t result_card_ref : result_card_refs) { + auto result_card = this->server()->card_for_set_card_ref(result_card_ref); + if (result_card) { + auto ce = result_card->get_definition(); + if (ce->def.type == CardType::HUNTERS_SC) { + bool should_add = true; + for (uint16_t other_result_card_ref : result_card_refs) { + if ((other_result_card_ref != result_card_ref) && + (client_id_for_card_ref(other_result_card_ref) == client_id_for_card_ref(result_card_ref))) { + should_add = false; + break; + } + } + if (should_add) { + ret.emplace_back(result_card); + } + } else { + ret.emplace_back(result_card); + } + } + } + } + } + break; + case 36: + if (as.original_attacker_card_ref == 0xFFFF) { + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto result_card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (result_card && + result_card->get_definition() && + result_card->get_definition()->def.is_sc()) { + ret.emplace_back(result_card); + } + } + } else if (card2 && + card2->get_definition() && + card2->get_definition()->def.is_sc()) { + ret.emplace_back(card2); + } + break; + case 37: + ret = this->find_all_cards_on_same_or_other_team(client_id, false); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 38: + if (card1) { + auto def = this->server()->definition_for_card_ref(card_ref); + auto ps = card1->player_state(); + if (def && ps) { + // TODO: Yet another Gifoie hardcode location :( + uint16_t range_card_id = this->get_card_id_with_effective_range(card1, 0x00D9, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, range_card_id, card1_loc, this->server()->base()->map_and_rules1); + auto result_card_refs = ps->get_all_cards_within_range(range, card1_loc, card1->get_team_id()); + for (uint16_t result_card_ref : result_card_refs) { + auto result_card = this->server()->card_for_set_card_ref(result_card_ref); + if (result_card && + (result_card->get_definition()->def.type != CardType::ITEM) && + (result_card->get_card_ref() != card_ref)) { + ret.emplace_back(result_card); + } + } + } + } + break; + case 39: + ret = this->find_all_set_cards_with_cost_in_range(4, 99); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 40: + ret = this->find_all_set_cards_with_cost_in_range(0, 3); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 41: { + auto ps = card1->player_state(); + if (card1 && ps) { + // TODO: Sigh. Gifoie again. + uint16_t range_card_id = this->get_card_id_with_effective_range(card1, 0x00D9, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, range_card_id, card1_loc, this->server()->base()->map_and_rules1); + auto result_card_refs = ps->get_all_cards_within_range(range, card1_loc, 0xFF); + for (uint16_t result_card_ref : result_card_refs) { + auto result_card = this->server()->card_for_set_card_ref(result_card_ref); + if (result_card && + (result_card != card1) && + (result_card->get_card_ref() != card_ref) && + (result_card->get_definition()->def.is_fc())) { + ret.emplace_back(result_card); + } + } + + for (size_t z = 0; z < 8; z++) { + auto result_card = ps->get_set_card(z); + if (result_card && (card1 != result_card) && + (result_card->get_definition()->def.type == CardType::ITEM)) { + bool already_in_ret = false; + for (auto c : ret) { + if (c == result_card) { + already_in_ret = true; + break; + } + } + if (!already_in_ret) { + ret.emplace_back(result_card); + } + } + } + } + break; + } + case 42: { + auto check_card = [&](shared_ptr result_card) -> void { + if (result_card) { + ret.emplace_back(result_card); + auto ce = result_card->get_definition(); + auto ps = result_card->player_state(); + if ((ce->def.type == CardType::ITEM) && ps) { + result_card = ps->get_sc_card(); + if (result_card) { + ret.emplace_back(result_card); + } + } + } + }; + if (as.original_attacker_card_ref == 0xFFFF) { + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + check_card(this->server()->card_for_set_card_ref(as.target_card_refs[z])); + } + } else if (card2) { + check_card(card2); + } + break; + } + case 43: + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto result_card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (!result_card) { + continue; + } + auto ce = result_card->get_definition(); + auto ps = result_card->player_state(); + if (ce && !ce->def.is_sc() && result_card->check_card_flag(2) && ps) { + auto result_sc_card = ps->get_sc_card(); + if (result_sc_card) { + ret.emplace_back(result_sc_card); + } + } + } + break; + case 44: { + auto ps = this->server()->get_player_state(client_id); + if (ps) { + for (size_t z = 0; z < 8; z++) { + auto result_card = ps->get_set_card(z); + if (result_card) { + ret.emplace_back(result_card); + } + } + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + } + break; + } + case 45: + this->sum_last_attack_damage(&ret, 0, 0); + ret = this->filter_cards_by_range(ret, card1, card1_loc, card2); + break; + case 46: + if (card1) { + auto def = this->server()->definition_for_card_ref(card_ref); + auto ps = card1->player_state(); + if (def && ps) { + // TODO: Yet another hardcoded card ID... but this time it's Cross + // Slay instead of Gifoie + uint16_t range_card_id = this->get_card_id_with_effective_range(card1, 0x009C, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, range_card_id, card1_loc, this->server()->base()->map_and_rules1); + auto result_card_refs = ps->get_all_cards_within_range(range, card1_loc, 0xFF); + for (uint16_t result_card_ref : result_card_refs) { + auto result_card = this->server()->card_for_set_card_ref(result_card_ref); + if (result_card && (result_card->get_definition()->def.type != CardType::ITEM)) { + ret.emplace_back(result_card); + } + } + } + } + break; + case 47: { + uint8_t client_id = client_id_for_card_ref(as.original_attacker_card_ref); + if (client_id != 0xFF) { + auto card = this->sc_card_for_client_id(client_id); + if (card) { + ret.emplace_back(card); + } + } + break; + } + case 48: + if (card1) { + auto ce = this->server()->definition_for_card_ref(card_ref); + auto ps = card1->player_state(); + if (ce && ps) { + // TODO: Sigh. Gifoie. Sigh. + uint16_t range_card_id = this->get_card_id_with_effective_range(card1, 0x00D9, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, range_card_id, card1_loc, this->server()->base()->map_and_rules1); + auto result_card_refs = ps->get_all_cards_within_range(range, card1_loc, 0xFF); + for (uint16_t result_card_ref : result_card_refs) { + auto result_card = this->server()->card_for_set_card_ref(result_card_ref); + if (result_card) { + auto def = result_card->get_definition(); + if (ce->def.type == CardType::HUNTERS_SC) { + bool should_add = true; + for (uint16_t other_result_card_ref : result_card_refs) { + if (other_result_card_ref != result_card_ref) { + if (client_id_for_card_ref(other_result_card_ref) == client_id_for_card_ref(result_card_ref)) { + should_add = false; + break; + } + } + } + if (should_add) { + ret.emplace_back(result_card); + } + } else { + ret.emplace_back(result_card); + } + } + } + } + auto result_card = this->server()->card_for_set_card_ref(setter_card_ref); + if (result_card) { + ret.emplace_back(result_card); + } + } + break; + case 49: + if (card1) { + auto ps = card1->player_state(); + if (ps) { + // TODO: One more Gifoie here. + uint16_t range_card_id = this->get_card_id_with_effective_range(card1, 0x00D9, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, range_card_id, card1_loc, this->server()->base()->map_and_rules1); + auto result_card_refs = ps->get_all_cards_within_range(range, card1_loc, card1->get_team_id()); + for (uint16_t result_card_ref : result_card_refs) { + auto result_card = this->server()->card_for_set_card_ref(result_card_ref); + if (result_card && (result_card != card1) && + (result_card->get_card_ref() != card_ref) && + result_card->get_definition()->def.is_fc()) { + ret.emplace_back(result_card); + } + } + + for (size_t set_index = 0; set_index < 8; set_index++) { + auto result_card = ps->get_set_card(set_index); + if (result_card && (card1 != result_card) && + (result_card->get_definition()->def.type == CardType::ITEM)) { + bool should_add = true; + for (auto c : ret) { + if (c == result_card) { + should_add = false; + break; + } + } + if (should_add) { + ret.emplace_back(result_card); + } + } + } + } + } + } + + if (apply_usability_filters) { + vector> filtered_ret; + for (auto c : ret) { + if (this->server()->ruler_server->check_usability_or_apply_condition_for_card_refs( + card_ref, setter_card_ref, c->get_card_ref(), def_effect_index, attack_medium)) { + filtered_ret.emplace_back(c); + } + } + return filtered_ret; + } else { + return ret; + } +} + +vector> CardSpecial::get_targeted_cards_for_condition( + uint16_t card_ref, + uint8_t def_effect_index, + uint16_t setter_card_ref, + const ActionState& as, + int16_t p_target_type, + bool apply_usability_filters) { + return this->server()->const_cast_set_cards_v(as_const(*this).get_targeted_cards_for_condition( + card_ref, def_effect_index, setter_card_ref, as, p_target_type, apply_usability_filters)); +} + +bool CardSpecial::is_card_targeted_by_condition( + const Condition& cond, + const ActionState& as, + shared_ptr card) const { + auto ce = this->server()->definition_for_card_ref(cond.card_ref); + auto sc_card = this->server()->card_for_set_card_ref(cond.card_ref); + if (cond.type != ConditionType::NONE) { + if ((!sc_card || ((sc_card != card) && (sc_card->card_flags & 2))) && + ce && + ((ce->def.type == CardType::ITEM) || ce->def.is_sc()) && + (cond.remaining_turns != 100) && + (client_id_for_card_ref(card->get_card_ref()) == client_id_for_card_ref(cond.card_ref))) { + return false; + } + if (cond.remaining_turns == 102) { + if (sc_card && ((sc_card == card) || !(sc_card->card_flags & 2))) { + auto target_cards = this->get_targeted_cards_for_condition( + cond.card_ref, + cond.card_definition_effect_index, + cond.condition_giver_card_ref, + as, + atoi(&ce->def.effects[cond.card_definition_effect_index].arg3[1]), + 0); + for (auto c : target_cards) { + if (c == card) { + return true; + } + } + } + return false; + } else { + return true; + } + } + return true; +} + +void CardSpecial::on_card_set(shared_ptr ps, uint16_t card_ref) { + auto sc_card = ps->get_sc_card(); + uint16_t sc_card_ref = sc_card ? sc_card->get_card_ref() : 0xFFFF; + + ActionState as; + this->unknown_8024C2B0(1, card_ref, as, sc_card_ref); +} + +const CardDefinition::Effect* CardSpecial::original_definition_for_condition( + const Condition& cond) const { + auto ce = this->server()->definition_for_card_ref(cond.card_ref); + if (!ce) { + return nullptr; + } + const auto* eff = &ce->def.effects[cond.card_definition_effect_index]; + return (eff->type == ConditionType::NONE) ? nullptr : eff; +} + +bool CardSpecial::card_ref_has_ability_trap(const Condition& cond) const { + auto card = this->server()->card_for_set_card_ref(cond.card_ref); + if (!card) { + return false; + } else { + return this->card_has_condition_with_ref( + card, ConditionType::ABILITY_TRAP, 0xFFFF, 0xFFFF); + } +} + +void CardSpecial::send_6xB4x06_for_exp_change( + shared_ptr card, + uint16_t attacker_card_ref, + uint8_t dice_roll_value, + bool unknown_p5) const { + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd; + cmd.effect.flags = 0x02; + cmd.effect.attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid(attacker_card_ref, 10); + cmd.effect.target_card_ref = card->get_card_ref(); + cmd.effect.value = 0; + cmd.effect.dice_roll_value = dice_roll_value; + cmd.effect.ap = clamp(card->ap, 0, 99); + cmd.effect.current_hp = clamp(card->get_current_hp(), 0, 99); + if (unknown_p5 == 0) { + cmd.effect.current_hp |= 0x80; + } + // NOTE: The original code appears to have a copy/paste error here: if + // card->tp > 99, then it sets cmd.effect.ap = 99 instead of cmd.effect.tp. + // We implement the presumably intended behavior here instead. + cmd.effect.tp = clamp(card->tp, 0, 99); + this->server()->send(cmd); +} + +void CardSpecial::send_6xB4x06_for_card_destroyed( + shared_ptr destroyed_card, uint16_t attacker_card_ref) const { + 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, 0x13); + cmd.effect.target_card_ref = destroyed_card->get_card_ref(); + cmd.effect.value = 0; + cmd.effect.operation = 0x7E; + this->server()->send(cmd); +} + +uint16_t CardSpecial::send_6xB4x06_if_card_ref_invalid( + uint16_t card_ref, int16_t value) const { + if (!this->server()->card_ref_is_empty_or_has_valid_card_id(card_ref)) { + if (value != 0) { + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd; + cmd.effect.flags = 0x04; + cmd.effect.attacker_card_ref = 0xFFFF; + cmd.effect.target_card_ref = 0xFFFF; + cmd.effect.value = value; + cmd.effect.operation = 0x7E; + this->server()->send(cmd); + } + card_ref = 0xFFFF; + } + return card_ref; +} + +void CardSpecial::send_6xB4x06_for_stat_delta( + shared_ptr card, + uint16_t attacker_card_ref, + uint32_t flags, + int16_t hp_delta, + bool unknown_p6, + bool unknown_p7) const { + if (((hp_delta > 50) || (hp_delta < -50)) && (flags == 0x20)) { + if (hp_delta < 0) { + hp_delta = -card->get_current_hp(); + } else { + hp_delta = card->get_max_hp() - card->get_current_hp(); + } + } + + if (unknown_p6) { + hp_delta = min(hp_delta + card->get_current_hp(), card->get_max_hp()) - card->get_current_hp(); + if (hp_delta == 0) { + return; + } + } + + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd; + cmd.effect.flags = flags | 2; + cmd.effect.attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid(attacker_card_ref, 10); + cmd.effect.target_card_ref = card->get_card_ref(); + cmd.effect.value = -hp_delta; + cmd.effect.ap = clamp(card->ap, 0, 99); + cmd.effect.current_hp = clamp(card->get_current_hp(), 0, 99); + cmd.effect.tp = clamp(card->tp, 0, 99); + if (!unknown_p7) { + cmd.effect.current_hp |= 0x80; + } + this->server()->send(cmd); +} + +bool CardSpecial::should_cancel_condition_due_to_anti_abnormality( + const CardDefinition::Effect& eff, + shared_ptr card, + uint16_t target_card_ref, + uint16_t sc_card_ref) const { + if (!card) { + return false; + } + if ((card->card_flags & 3) || + (card->action_metadata.check_flag(0x10) && + (card->get_card_ref() != target_card_ref) && + (card->get_card_ref() != sc_card_ref))) { + return true; + } + auto ce = card->get_definition(); + if (ce->def.is_sc() && (eff.type == ConditionType::FREEZE)) { + return true; + } + switch (eff.type) { + case ConditionType::IMMOBILE: + case ConditionType::HOLD: + case ConditionType::GUOM: + case ConditionType::PARALYZE: + case ConditionType::ACID: + case ConditionType::CURSE: + case ConditionType::FREEZE: + case ConditionType::DROP: { + const auto* cond = this->find_condition_with_parameters(card, ConditionType::ANTI_ABNORMALITY_2, 0xFFFF, 0xFF); + return (cond != nullptr) || + this->server()->ruler_server->card_ref_is_boss_sc(card->get_card_ref()); + } + default: + return false; + } +} + +bool CardSpecial::should_return_card_ref_to_hand_on_destruction( + uint16_t card_ref) const { + if (card_ref == 0xFFFF) { + return false; + } + uint8_t client_id = client_id_for_card_ref(card_ref); + if (client_id == 0xFF) { + return false; + } + auto ce = this->server()->definition_for_card_ref(card_ref); + if (!ce) { + return false; + } + auto ps = (client_id == 0xFF) ? nullptr : this->server()->get_player_state(client_id); + if (!ps) { + return false; + } + + auto check_card = [&](shared_ptr card) -> bool { + if (!card) { + return false; + } + for (size_t cond_index = 0; cond_index < 9; cond_index++) { + if (this->card_ref_has_ability_trap(card->action_chain.conditions[cond_index])) { + continue; + } + auto cond_type = card->action_chain.conditions[cond_index].type; + if ((cond_type == ConditionType::RETURN) && + !(card->card_flags & 1) && + (card->get_card_ref() == card_ref)) { + return true; + } else if ((cond_type == ConditionType::REBORN) && + !(card->card_flags & 3) && + (ce->def.card_id == static_cast(card->action_chain.conditions[cond_index].value))) { + return true; + } + } + return false; + }; + + for (size_t set_index = 0; set_index < 8; set_index++) { + if (check_card(ps->get_set_card(set_index))) { + return true; + } + } + return check_card(ps->get_sc_card()); +} + +size_t CardSpecial::sum_last_attack_damage( + vector>* out_cards, + int32_t* out_damage_sum, + size_t* out_damage_count) const { + size_t damage_count = 0; + auto check_card = [&](shared_ptr c) -> void { + if (c && (c->last_attack_final_damage > 0)) { + if (out_damage_sum) { + *out_damage_sum += c->last_attack_final_damage; + } + if (out_cards) { + out_cards->emplace_back(c); + } + damage_count++; + } + }; + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (!ps) { + continue; + } + check_card(ps->get_sc_card()); + for (size_t set_index = 0; set_index < 8; set_index++) { + check_card(ps->get_set_card(set_index)); + } + } + + if (out_damage_count) { + *out_damage_count += damage_count; + } + return damage_count; +} + +void CardSpecial::update_condition_orders(shared_ptr card) { + vector cond_indexes; + for (size_t z = 0; z < 9; z++) { + if (card->action_chain.conditions[z].type != ConditionType::NONE) { + cond_indexes.emplace_back(z); + } + } + + bool modified = true; + while (modified) { + modified = false; + for (size_t index_offset = 0; index_offset < cond_indexes.size() - 1; index_offset++) { + size_t this_index = cond_indexes[index_offset]; + size_t next_index = cond_indexes[index_offset + 1]; + uint8_t this_cond_order = card->action_chain.conditions[this_index].order; + uint8_t next_cond_order = card->action_chain.conditions[next_index].order; + if (next_cond_order < this_cond_order) { + card->action_chain.conditions[this_index].order = next_cond_order; + card->action_chain.conditions[next_index].order = this_cond_order; + modified = true; + } + } + } + + size_t cond_order = 0; + for (size_t index : cond_indexes) { + card->action_chain.conditions[index].order = cond_order++; + } +} + +int16_t CardSpecial::max_all_attack_bonuses(size_t* out_count) const { + int16_t max_attack_bonus = 0; + size_t num_attack_bonuses = 0; + auto check_card = [&](shared_ptr c) { + if (!c) { + return; + } + if (c->action_metadata.attack_bonus > max_attack_bonus) { + max_attack_bonus = c->action_metadata.attack_bonus; + } + if (c->action_metadata.attack_bonus > 0) { + num_attack_bonuses++; + } + }; + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (ps) { + check_card(ps->get_sc_card()); + for (size_t set_index = 0; set_index < 8; set_index++) { + check_card(ps->get_set_card(set_index)); + } + } + } + + if (out_count) { + *out_count = num_attack_bonuses; + } + return max_attack_bonus; +} + +void CardSpecial::unknown_80244AA8(shared_ptr card) { + ActionState as = this->create_attack_state_from_card_action_chain(card); + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->player_states[client_id]; + if (ps) { + auto other_card = ps->get_sc_card(); + if (other_card) { + this->clear_invalid_conditions_on_card(other_card, as); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + auto other_card = ps->get_set_card(set_index); + if (other_card) { + this->clear_invalid_conditions_on_card(other_card, as); + } + } + } + } + + this->apply_defense_conditions(as, 0x27, card, 4); + this->unknown_8024C2B0(0x27, card->get_card_ref(), as, 0xFFFF); + this->apply_defense_conditions(as, 0x13, card, 4); + this->unknown_8024C2B0(0x13, card->get_card_ref(), as, 0xFFFF); +} + +void CardSpecial::unknown_80244E20( + shared_ptr attacker_card, + shared_ptr target_card, + int16_t* inout_unknown_p4) { + if (!inout_unknown_p4) { + return; + } + if (target_card->get_current_hp() > *inout_unknown_p4) { + return; + } + + uint16_t ally_sc_card_ref = this->server()->ruler_server->get_ally_sc_card_ref( + target_card->get_card_ref()); + if (ally_sc_card_ref == 0xFFFF) { + return; + } + + auto ally_sc = this->server()->card_for_set_card_ref(ally_sc_card_ref); + if (!ally_sc || (ally_sc->card_flags & 2)) { + return; + } + + uint8_t target_ally_client_id = client_id_for_card_ref(ally_sc_card_ref); + if (target_ally_client_id == 0xFF) { + return; + } + + uint8_t target_client_id = client_id_for_card_ref(target_card->get_card_ref()); + if (target_client_id == 0xFF) { + return; + } + + auto ally_hes = this->server()->ruler_server->get_hand_and_equip_state_for_client_id(target_ally_client_id); + if (!ally_hes || !ally_hes->is_cpu_player) { + return; + } + + uint16_t target_card_id = this->server()->card_id_for_card_ref(target_card->get_card_ref()); + if (target_card_id == 0xFFFF) { + return; + } + + uint16_t ally_sc_card_id = this->server()->card_id_for_card_ref(ally_sc_card_ref); + if (ally_sc_card_id == 0xFFFF) { + return; + } + + auto target_ps = target_card->player_state(); + if (!target_ps) { + return; + } + 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) { + return; + } + + target_ps->unknown_a17++; + + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd; + cmd.effect.flags = 0x04; + cmd.effect.attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid(attacker_card->get_card_ref(), 0x12); + cmd.effect.target_card_ref = target_card->get_card_ref(); + cmd.effect.value = 0; + cmd.effect.operation = 0x7D; + this->server()->send(cmd); + if (inout_unknown_p4) { + *inout_unknown_p4 = 0; + target_card->action_metadata.set_flags(0x10); + } +} + +void CardSpecial::unknown_8024C2B0( + uint32_t when, + uint16_t set_card_ref, + const ActionState& as, + uint16_t sc_card_ref, + bool apply_defense_condition_to_all_cards, + uint16_t apply_defense_condition_to_card_ref) { + set_card_ref = this->send_6xB4x06_if_card_ref_invalid(set_card_ref, 1); + auto ce = this->server()->definition_for_card_ref(set_card_ref); + if (!ce) { + return; + } + + uint16_t as_attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid(as.attacker_card_ref, 2); + if (as_attacker_card_ref == 0xFFFF) { + as_attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid(as.original_attacker_card_ref, 3); + } + + G_ApplyConditionEffect_GC_Ep3_6xB4x06 dice_cmd; + dice_cmd.effect.target_card_ref = set_card_ref; + bool as_action_card_refs_contains_set_card_ref = false; + bool as_action_card_refs_contains_duplicate_of_set_card = false; + for (size_t z = 0; (z < 8) && (as.action_card_refs[z] != 0xFFFF); z++) { + if (as.action_card_refs[z] == dice_cmd.effect.target_card_ref) { + as_action_card_refs_contains_set_card_ref = true; + break; + } + auto action_ce = this->server()->definition_for_card_ref(as.action_card_refs[z]); + if (action_ce && (action_ce->def.card_id == action_ce->def.card_id)) { + as_action_card_refs_contains_duplicate_of_set_card = true; + } + } + + bool unknown_v1 = as_action_card_refs_contains_duplicate_of_set_card && as_action_card_refs_contains_set_card_ref; + + uint8_t random_percent = this->server() ? this->server()->get_random(99) : 0; + bool any_expr_used_dice_roll = false; + + DiceRoll dice_roll; + uint8_t client_id = client_id_for_card_ref(dice_cmd.effect.target_card_ref); + auto set_card_ps = (client_id == 0xFF) ? nullptr : this->server()->player_states[client_id]; + + dice_roll.value = 1; + if (set_card_ps) { + dice_roll.value = set_card_ps->roll_dice_with_effects(1); + } + dice_roll.client_id = client_id; + dice_roll.unknown_a2 = 3; + dice_roll.value_used_in_expr = false; + + for (size_t def_effect_index = 0; (def_effect_index < 3) && !unknown_v1 && (ce->def.effects[def_effect_index].type != ConditionType::NONE); def_effect_index++) { + const auto& card_effect = ce->def.effects[def_effect_index]; + if (card_effect.when != when) { + continue; + } + + 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); + bool all_targets_matched = false; + if (!targeted_cards.empty() && + ((card_effect.type == ConditionType::UNKNOWN_64) || + (card_effect.type == ConditionType::MISC_DEFENSE_BONUSES) || + (card_effect.type == ConditionType::MOSTLY_HALFGUARDS))) { + size_t count = 0; + for (size_t z = 0; z < targeted_cards.size(); z++) { + dice_roll.value_used_in_expr = false; + string arg2_text = card_effect.arg2; + if (this->evaluate_effect_arg2_condition( + as, targeted_cards[z], arg2_text.c_str(), dice_roll, + set_card_ref, sc_card_ref, random_percent, when)) { + count++; + } + if (dice_roll.value_used_in_expr) { + any_expr_used_dice_roll = true; + } + } + if (count == targeted_cards.size()) { + auto set_card = this->server()->card_for_set_card_ref(set_card_ref); + if (!set_card) { + set_card = this->server()->card_for_set_card_ref(sc_card_ref); + } + targeted_cards.clear(); + if (set_card != nullptr) { + targeted_cards.emplace_back(set_card); + } + all_targets_matched = true; + } else { + targeted_cards.clear(); + } + } + + for (size_t z = 0; z < targeted_cards.size(); z++) { + dice_roll.value_used_in_expr = false; + string arg2_str = card_effect.arg2; + if (all_targets_matched || + this->evaluate_effect_arg2_condition( + as, targeted_cards[z], arg2_str.c_str(), dice_roll, set_card_ref, sc_card_ref, random_percent, when)) { + auto env_stats = this->compute_attack_env_stats( + as, targeted_cards[z], dice_roll, set_card_ref, sc_card_ref); + string expr_str = card_effect.expr; + int16_t value = this->evaluate_effect_expr(env_stats, expr_str.c_str(), dice_roll); + + uint32_t unknown_v1 = 0; + auto target_card = this->compute_replaced_target_based_on_conditions( + targeted_cards[z]->get_card_ref(), + 0, + 1, + as_attacker_card_ref, + set_card_ref, + 0, + nullptr, + def_effect_index, + &unknown_v1, + sc_card_ref); + if (!target_card) { + target_card = targeted_cards[z]; + } + + ssize_t applied_cond_index = -1; + if ((unknown_v1 == 0) && !this->should_cancel_condition_due_to_anti_abnormality( + 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(when, 4, &env_stats, "!set_abnormal..", target_card, card_effect.type); + } + + if (applied_cond_index >= 0) { + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd; + cmd.effect.flags = 0x04; + cmd.effect.attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid(as_attacker_card_ref, 0x14); + cmd.effect.target_card_ref = target_card->get_card_ref(); + cmd.effect.value = (target_card->action_chain).conditions[applied_cond_index].remaining_turns; + cmd.effect.operation = static_cast(card_effect.type); + this->server()->send(cmd); + } + + if (dice_roll.value_used_in_expr) { + target_card->action_chain.conditions[applied_cond_index].flags |= 1; + } + if ((applied_cond_index >= 0) && + (apply_defense_condition_to_all_cards || (apply_defense_condition_to_card_ref == targeted_cards[z]->get_card_ref()))) { + this->apply_defense_condition( + when, &target_card->action_chain.conditions[applied_cond_index], applied_cond_index, as, target_card, 4, 1); + } + target_card->send_6xB4x4E_4C_4D_if_needed(0); + } + if (dice_roll.value_used_in_expr) { + any_expr_used_dice_roll = true; + } + } + } + + if (any_expr_used_dice_roll) { + dice_cmd.effect.flags = 0x08; + dice_cmd.effect.attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid( + as_attacker_card_ref, 0x15); + dice_cmd.effect.dice_roll_value = dice_roll.value; + this->server()->send(dice_cmd); + } +} + +vector> CardSpecial::get_all_set_cards() const { + vector> ret; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (ps) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto set_card = ps->get_set_card(set_index); + if (set_card) { + ret.emplace_back(set_card); + } + } + } + } + return ret; +} + +vector> CardSpecial::find_cards_by_condition_inc_exc( + ConditionType include_cond, + ConditionType exclude_cond, + AssistEffect include_eff, + AssistEffect exclude_eff) const { + vector> ret; + auto check_card = [&](uint8_t client_id, shared_ptr c) -> void { + if (c) { + bool should_include = false; + bool should_exclude = false; + for (size_t z = 0; z < 9; z++) { + auto type = c->action_chain.conditions[z].type; + if ((type == include_cond) || (include_cond == ConditionType::ANY_FF)) { + should_include = true; + } + if ((type == exclude_cond) && (exclude_cond != ConditionType::NONE)) { + should_exclude = true; + } + } + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->server()->assist_server->get_active_assist_by_index(z); + if ((exclude_eff != AssistEffect::NONE) && + ((include_eff == AssistEffect::ANY) || (include_eff == eff))) { + should_include = true; + } + if ((exclude_eff != AssistEffect::NONE) && (exclude_eff == eff)) { + should_exclude = true; + } + } + if (should_include && !should_exclude) { + ret.emplace_back(c); + } + } + }; + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (!ps) { + continue; + } + for (size_t set_index = 0; set_index < 8; set_index++) { + check_card(client_id, ps->get_set_card(set_index)); + } + check_card(client_id, ps->get_sc_card()); + } + + return ret; +} + +void CardSpecial::clear_invalid_conditions_on_card( + shared_ptr card, const ActionState& as) { + for (size_t cond_index = 0; cond_index < 9; cond_index++) { + auto& cond = card->action_chain.conditions[cond_index]; + if (cond.type != ConditionType::NONE) { + if (!this->is_card_targeted_by_condition(cond, as, card)) { + if (cond.type != ConditionType::NONE) { + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd; + cmd.effect.flags = 0x04; + cmd.effect.attacker_card_ref = 0xFFFF; + cmd.effect.target_card_ref = card->get_card_ref(); + cmd.effect.value = 0; + cmd.effect.operation = -static_cast(cond.type); + this->server()->send(cmd); + } + this->apply_stat_deltas_to_card_from_condition_and_clear_cond(cond, card); + card->send_6xB4x4E_4C_4D_if_needed(); + } + } + } +} + +const UnknownMatrixEntry* unknown_8024DAFC( + uint16_t row_card_id, + uint16_t column_card_id, + bool use_entry_v1, + size_t* out_entry_index) { + static const UnknownMatrixEntry entries[] = { + {0x0004, 0xFF, 0xFF}, + {0x0002, 0x04, 0x00}, + {0x0002, 0x00, 0x0F}, + {0x0003, 0x03, 0x00}, + {0x0003, 0x00, 0x0A}, + {0x0006, 0x01, 0x00}, + {0x0006, 0x00, 0x05}, + {0x0111, 0x01, 0x00}, + {0x0111, 0x00, 0x05}, + {0x0001, 0x03, 0x00}, + {0x0001, 0x00, 0x0A}, + {0x0002, 0xFF, 0xFF}, + {0x0004, 0x04, 0x00}, + {0x0004, 0x00, 0x0F}, + {0x0003, 0x06, 0x00}, + {0x0003, 0x00, 0x14}, + {0x0006, 0x04, 0x00}, + {0x0006, 0x00, 0x0F}, + {0x0003, 0xFF, 0xFF}, + {0x0004, 0x04, 0x00}, + {0x0004, 0x00, 0x0F}, + {0x0002, 0x04, 0x00}, + {0x0002, 0x00, 0x0F}, + {0x0006, 0xFF, 0xFF}, + {0x0002, 0x06, 0x00}, + {0x0002, 0x00, 0x14}, + {0x0111, 0xFF, 0xFF}, + {0x0004, 0x01, 0x00}, + {0x0004, 0x00, 0x05}, + {0x0001, 0x06, 0x00}, + {0x0001, 0x00, 0x14}, + {0x0001, 0xFF, 0xFF}, + {0x0111, 0x04, 0x00}, + {0x0111, 0x00, 0x0F}, + {0x0112, 0xFF, 0xFF}, + {0x0113, 0x06, 0x00}, + {0x0113, 0x00, 0x14}, + {0x0110, 0x06, 0x00}, + {0x0110, 0x00, 0x14}, + {0x0114, 0x01, 0x00}, + {0x0114, 0x00, 0x05}, + {0x011D, 0x02, 0x00}, + {0x011D, 0x00, 0x07}, + {0x0113, 0xFF, 0xFF}, + {0x0003, 0x03, 0x00}, + {0x0003, 0x00, 0x0A}, + {0x0112, 0x03, 0x00}, + {0x0112, 0x00, 0x0A}, + {0x0110, 0xFF, 0xFF}, + {0x0005, 0x03, 0x00}, + {0x0005, 0x00, 0x0A}, + {0x0112, 0x04, 0x00}, + {0x0112, 0x00, 0x0F}, + {0x0005, 0xFF, 0xFF}, + {0x0110, 0x03, 0x00}, + {0x0110, 0x00, 0x0A}, + {0x0114, 0xFF, 0xFF}, + {0x0005, 0x03, 0x00}, + {0x0005, 0x00, 0x0A}, + {0x0110, 0x01, 0x00}, + {0x0110, 0x00, 0x05}, + {0x0115, 0x06, 0x00}, + {0x0115, 0x00, 0x14}, + {0x0115, 0xFF, 0xFF}, + {0x0004, 0x01, 0x00}, + {0x0004, 0x00, 0x05}, + {0x0003, 0x01, 0x00}, + {0x0003, 0x00, 0x05}, + {0x0006, 0x01, 0x00}, + {0x0006, 0x00, 0x05}, + {0x0112, 0x01, 0x00}, + {0x0112, 0x00, 0x05}, + {0x0110, 0x01, 0x00}, + {0x0110, 0x00, 0x05}, + {0x0114, 0x04, 0x00}, + {0x0114, 0x00, 0x0F}, + {0x0008, 0xFF, 0xFF}, + {0x0007, 0x06, 0x00}, + {0x0007, 0x00, 0x14}, + {0x0116, 0x01, 0x00}, + {0x0116, 0x00, 0x05}, + {0x011E, 0x03, 0x00}, + {0x011E, 0x00, 0x0A}, + {0x0118, 0x06, 0x00}, + {0x0118, 0x00, 0x14}, + {0x0007, 0xFF, 0xFF}, + {0x0008, 0x06, 0x00}, + {0x0008, 0x00, 0x14}, + {0x0118, 0x01, 0x00}, + {0x0118, 0x00, 0x05}, + {0x011B, 0x03, 0x00}, + {0x011B, 0x00, 0x0A}, + {0x0116, 0xFF, 0xFF}, + {0x0008, 0x01, 0x00}, + {0x0008, 0x00, 0x05}, + {0x011C, 0x03, 0x00}, + {0x011C, 0x00, 0x0A}, + {0x011A, 0xFF, 0xFF}, + {0x0119, 0x04, 0x00}, + {0x0119, 0x00, 0x0F}, + {0x011D, 0x04, 0x00}, + {0x011D, 0x00, 0x0F}, + {0x0119, 0xFF, 0xFF}, + {0x011A, 0x04, 0x00}, + {0x011A, 0x00, 0x0F}, + {0x011D, 0x04, 0x00}, + {0x011D, 0x00, 0x0F}, + {0x011D, 0xFF, 0xFF}, + {0x0119, 0x04, 0x00}, + {0x0119, 0x00, 0x0F}, + {0x011A, 0x04, 0x00}, + {0x011A, 0x00, 0x0F}, + {0x0112, 0x01, 0x00}, + {0x0112, 0x00, 0x07}, + {0x011E, 0xFF, 0xFF}, + {0x0008, 0x03, 0x00}, + {0x0008, 0x00, 0x0A}, + {0x0118, 0x06, 0x00}, + {0x0118, 0x00, 0x14}, + {0x011C, 0xFF, 0xFF}, + {0x0116, 0x04, 0x00}, + {0x0116, 0x00, 0x0F}, + {0x011E, 0x01, 0x00}, + {0x011E, 0x00, 0x05}, + {0x0118, 0xFF, 0xFF}, + {0x011E, 0x06, 0x00}, + {0x011E, 0x00, 0x14}, + {0x011B, 0xFF, 0xFF}, + {0x0007, 0x03, 0x00}, + {0x0007, 0x00, 0x0A}, + {0x0117, 0x03, 0x00}, + {0x0117, 0x00, 0x0A}, + {0x011F, 0x06, 0x00}, + {0x011F, 0x00, 0x14}, + {0x0117, 0xFF, 0xFF}, + {0x011F, 0x03, 0x00}, + {0x011F, 0x00, 0x0A}, + {0x011B, 0x04, 0x00}, + {0x011B, 0x00, 0x0F}, + {0x011F, 0xFF, 0xFF}, + {0x0007, 0x01, 0x00}, + {0x0007, 0x00, 0x05}, + {0x011B, 0x06, 0x00}, + {0x011B, 0x00, 0x14}, + {0x0117, 0x04, 0x00}, + {0x0117, 0x00, 0x0F}, + }; + constexpr size_t num_entries = sizeof(entries) / sizeof(entries[0]); + + const UnknownMatrixEntry* 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 ((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; + if (current_max <= v) { + ret_entry = &entry; + current_max = v; + if (out_entry_index) { + *out_entry_index = logical_index; + } + } + } + logical_index++; + } else { + current_row_card_id = current_column_card_id; + } + } + + return ret_entry; +} + +void CardSpecial::on_card_destroyed( + shared_ptr attacker_card, shared_ptr destroyed_card) { + ActionState attack_as = this->create_attack_state_from_card_action_chain(attacker_card); + ActionState defense_as = this->create_defense_state_for_card_pair_action_chains( + attacker_card, destroyed_card); + + uint16_t destroyed_card_ref = destroyed_card->get_card_ref(); + this->unknown_8024C2B0(5, destroyed_card_ref, defense_as, 0xFFFF); + for (size_t z = 0; (z < 8) && (defense_as.action_card_refs[z] != 0xFFFF); z++) { + this->unknown_8024C2B0( + 5, defense_as.action_card_refs[z], defense_as, destroyed_card->get_card_ref()); + } + + if (attacker_card) { + for (size_t cond_index = 0; cond_index < 9; cond_index++) { + auto& cond = attacker_card->action_chain.conditions[cond_index]; + if (cond.type == ConditionType::CURSE) { + this->execute_effect(cond, attacker_card, 0, 0, ConditionType::CURSE, 4, 0xFFFF); + } + } + } + this->send_6xB4x06_for_card_destroyed(destroyed_card, attack_as.attacker_card_ref); +} + +vector> CardSpecial::find_cards_in_hp_range( + int16_t min, int16_t max) const { + vector> ret; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (ps) { + auto card = ps->get_sc_card(); + if (card) { + int16_t hp = card->get_current_hp(); + if ((min <= hp) && (hp <= max)) { + ret.emplace_back(card); + } + } + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = ps->get_set_card(set_index); + if (card) { + int16_t hp = card->get_current_hp(); + if ((min <= hp) && (hp <= max)) { + ret.emplace_back(card); + } + } + } + } + } + return ret; +} + +vector> CardSpecial::find_all_cards_by_aerial_attribute(bool is_aerial) const { + vector> ret; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (ps) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = ps->get_set_card(set_index); + if (card && (this->server()->ruler_server->card_ref_is_aerial(card->get_card_ref()) == is_aerial)) { + ret.emplace_back(card); + } + } + auto card = ps->get_sc_card(); + if (card && (this->server()->ruler_server->card_ref_is_aerial(card->get_card_ref()) == is_aerial)) { + ret.emplace_back(card); + } + } + } + return ret; +} + +vector> CardSpecial::find_cards_damaged_by_at_least(int16_t damage) const { + vector> ret; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (ps) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = ps->get_set_card(set_index); + if (card && (damage + card->get_current_hp() <= card->get_max_hp())) { + ret.emplace_back(card); + } + } + auto card = ps->get_sc_card(); + if (card) { + if (damage + card->get_current_hp() <= card->get_max_hp()) { + ret.emplace_back(card); + } + } + } + } + return ret; +} + +vector> CardSpecial::find_all_set_cards_on_client_team(uint8_t client_id) const { + vector> ret; + auto ps = this->server()->get_player_state(client_id); + if (!ps) { + return ret; + } + for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) { + auto other_ps = this->server()->get_player_state(other_client_id); + if (other_ps && (other_ps->get_team_id() == ps->get_team_id())) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = other_ps->get_set_card(set_index); + if (card) { + ret.emplace_back(card); + } + } + } + } + return ret; +} + +vector> CardSpecial::find_all_cards_on_same_or_other_team(uint8_t client_id, bool same_team) const { + vector> ret; + auto ps = this->server()->get_player_state(client_id); + if (!ps) { + return ret; + } + + for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) { + auto other_ps = this->server()->get_player_state(other_client_id); + if (other_ps) { + bool should_collect = false; + if (!same_team) { + if ((other_ps->get_team_id() != 0xFF) && (ps->get_team_id() != other_ps->get_team_id())) { + should_collect = true; + } + } else { + if (ps->get_team_id() == other_ps->get_team_id()) { + should_collect = true; + } + } + if (should_collect) { + auto card = other_ps->get_sc_card(); + if (card) { + ret.emplace_back(card); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = other_ps->get_set_card(set_index); + if (card) { + ret.emplace_back(card); + } + } + } + } + } + return ret; +} + +shared_ptr CardSpecial::sc_card_for_client_id(uint8_t client_id) const { + auto ps = this->server()->get_player_state(client_id); + return ps ? ps->get_sc_card() : nullptr; +} + +shared_ptr CardSpecial::get_attacker_card(const ActionState& as) const { + uint32_t card_ref = as.attacker_card_ref; + if (card_ref == 0xFFFF) { + card_ref = as.original_attacker_card_ref; + } + + auto card = this->server()->card_for_set_card_ref(card_ref); + if (card) { + auto ce = card->get_definition(); + if ((ce->def.type == CardType::ITEM) || (ce->def.type == CardType::CREATURE)) { + return card; + } + } + return nullptr; +} + +vector> CardSpecial::get_attacker_card_and_sc_if_item(const ActionState& as) const { + vector> ret; + uint16_t card_ref = as.attacker_card_ref; + if (card_ref == 0xFFFF) { + card_ref = as.original_attacker_card_ref; + } + auto card = this->server()->card_for_set_card_ref(card_ref); + if (card) { + auto ce = card->get_definition(); + if (ce->def.type == CardType::ITEM) { + auto ps = card->player_state(); + if (ps) { + ret.emplace_back(ps->get_sc_card()); + } + ret.emplace_back(card); + } else { + if ((ce->def.type == CardType::HUNTERS_SC) || + (ce->def.type == CardType::ARKZ_SC) || + (ce->def.type == CardType::CREATURE)) { + ret.emplace_back(card); + } + } + } + return ret; +} + +vector> CardSpecial::find_all_set_cards_with_cost_in_range(uint8_t min_cost, uint8_t max_cost) const { + vector> ret; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->server()->get_player_state(client_id); + if (ps) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = ps->get_set_card(set_index); + if (card) { + auto ce = card->get_definition(); + if (ce && (min_cost <= ce->def.self_cost) && (ce->def.self_cost <= max_cost)) { + ret.emplace_back(card); + } + } + } + } + } + return ret; +} + +vector> CardSpecial::filter_cards_by_range( + const vector>& cards, + shared_ptr card1, + const Location& card1_loc, + shared_ptr card2) const { + vector> ret; + if (!card1 || cards.empty()) { + return ret; + } + + auto ps = card1->player_state(); + if (!ps) { + return ret; + } + + // TODO: Remove hardcoded card ID here (Earthquake) + uint16_t card_id = this->get_card_id_with_effective_range(card1, 0x00ED, card2); + parray range; + compute_effective_range(range, this->server()->base()->data_index, card_id, card1_loc, this->server()->base()->map_and_rules1); + auto card_refs_in_range = ps->get_card_refs_within_range_from_all_players(range, card1_loc, CardType::ITEM); + + for (auto card : cards) { + if (!card || (card->get_card_ref() == 0xFFFF)) { + continue; + } + for (uint16_t card_ref_in_range : card_refs_in_range) { + if (card_ref_in_range == card->get_card_ref()) { + ret.emplace_back(card); + break; + } + } + } + return ret; +} + +void CardSpecial::unknown_8024AAB8(const ActionState& as) { + this->unknown_action_state_a1 = as; + + for (size_t z = 0; (z < 8) && (as.action_card_refs[z] != 0xFFFF); z++) { + uint16_t card_ref = this->send_6xB4x06_if_card_ref_invalid( + as.action_card_refs[z], 0x1E); + if (card_ref == 0xFFFF) { + break; + } + + if (this->send_6xB4x06_if_card_ref_invalid(as.original_attacker_card_ref, 0x1F) == 0xFFFF) { + this->unknown_8024C2B0( + 1, + as.action_card_refs[z], + as, + this->send_6xB4x06_if_card_ref_invalid(as.attacker_card_ref, 0x21)); + this->unknown_8024C2B0( + 0xb, + as.action_card_refs[z], + as, + this->send_6xB4x06_if_card_ref_invalid(as.attacker_card_ref, 0x22)); + } else { + uint16_t card_ref = this->send_6xB4x06_if_card_ref_invalid(as.target_card_refs[0], 0x20); + if (card_ref != 0xFFFF) { + this->unknown_8024C2B0(1, as.action_card_refs[z], as, card_ref); + this->unknown_8024C2B0(0x15, as.action_card_refs[z], as, card_ref); + } + } + } + + if (as.original_attacker_card_ref == 0xffff) { + uint16_t card_ref1 = this->send_6xB4x06_if_card_ref_invalid(as.attacker_card_ref, 0x23); + uint16_t card_ref2 = this->send_6xB4x06_if_card_ref_invalid(as.attacker_card_ref, 0x25); + this->unknown_8024C2B0(0x33, card_ref2, as, card_ref1); + card_ref1 = this->send_6xB4x06_if_card_ref_invalid(as.attacker_card_ref, 0x24); + card_ref2 = this->send_6xB4x06_if_card_ref_invalid(as.attacker_card_ref, 0x26); + this->unknown_8024C2B0(0x34, card_ref2, as, card_ref1); + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + uint16_t card_ref = this->send_6xB4x06_if_card_ref_invalid( + as.action_card_refs[z], 0x27); + if (card_ref == 0xFFFF) { + break; + } + this->unknown_8024C2B0(0x35, as.target_card_refs[z], as, as.attacker_card_ref); + } + } +} + +void CardSpecial::unknown_80244BE4(shared_ptr card) { + ActionState as = this->create_attack_state_from_card_action_chain(card); + this->apply_defense_conditions(as, 9, card, 4); + this->unknown_8024C2B0(9, card->get_card_ref(), as, 0xFFFF); + this->apply_defense_conditions(as, 0x27, card, 4); + this->unknown_8024C2B0(0x27, card->get_card_ref(), as, 0xFFFF); +} + +void CardSpecial::unknown_80244CA8(shared_ptr card) { + ActionState as; + auto ps = card->player_state(); + as.attacker_card_ref = card->get_card_ref(); + as.action_card_refs = card->action_chain.chain.attack_action_card_refs; + as.target_card_refs = card->action_chain.chain.target_card_refs; + + uint16_t sc_card_ref = 0xFFFF; + if (ps) { + auto sc_card = ps->get_sc_card(); + if (sc_card) { + sc_card_ref = sc_card->get_card_ref(); + } + } + + this->apply_defense_conditions(as, 0x46, card, 4); + this->unknown_8024C2B0(0x46, card->get_card_ref(), as, sc_card_ref); + if (ps->is_team_turn()) { + this->apply_defense_conditions(as, 4, card, 4); + this->unknown_8024C2B0(4, card->get_card_ref(), as, sc_card_ref); + } +} + +template +void CardSpecial::unknown1_t( + shared_ptr unknown_p2, const ActionState* existing_as) { + ActionState as; + if (!existing_as) { + as = this->create_attack_state_from_card_action_chain(unknown_p2); + } else { + as = *existing_as; + } + this->apply_defense_conditions(as, When1, unknown_p2, 4); + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (card) { + ActionState target_as = this->create_defense_state_for_card_pair_action_chains(unknown_p2, card); + this->apply_defense_conditions(target_as, When1, card, 4); + } + } + auto card = this->sc_card_for_card(unknown_p2); + this->unknown_8024C2B0(When1, unknown_p2->get_card_ref(), as, card ? card->get_card_ref() : 0xFFFF); + for (size_t z = 0; (z < 8) && (as.action_card_refs[z] != 0xFFFF); z++) { + this->unknown_8024C2B0(When1, as.action_card_refs[z], as, unknown_p2->get_card_ref()); + } + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (card) { + ActionState target_as = this->create_defense_state_for_card_pair_action_chains( + unknown_p2, card); + this->unknown_8024C2B0(When2, as.target_card_refs[z], target_as, unknown_p2->get_card_ref()); + for (size_t w = 0; (w < 8) && (target_as.action_card_refs[w] != 0xFFFF); w++) { + this->unknown_8024C2B0(When1, target_as.action_card_refs[w], target_as, card->get_card_ref()); + } + } + } +} + +void CardSpecial::unknown_80249060(shared_ptr unknown_p2) { + this->unknown1_t<0x0F, 0x0A>(unknown_p2); +} + +void CardSpecial::unknown_80249254(shared_ptr unknown_p2) { + if (unknown_p2->player_state()->is_team_turn()) { + this->unknown1_t<0x0E, 0x0A>(unknown_p2); + } +} + +void CardSpecial::unknown_8024945C(shared_ptr unknown_p2, const ActionState& existing_as) { + this->unknown1_t<0x0A, 0x0A>(unknown_p2, &existing_as); +} + +void CardSpecial::unknown_8024966C(shared_ptr unknown_p2, const ActionState* existing_as) { + ActionState as; + if (!existing_as) { + as = this->create_attack_state_from_card_action_chain(unknown_p2); + } else { + as = *existing_as; + } + + auto card = this->sc_card_for_card(unknown_p2); + uint16_t card_ref = card ? card->get_card_ref() : 0xFFFF; + + auto ce = unknown_p2->get_definition(); + auto defender_card = (ce && (ce->def.type == CardType::ITEM) && card) ? card : unknown_p2; + + this->apply_defense_conditions(as, 0x3D, unknown_p2, 4); + this->apply_defense_conditions(as, 0x3E, unknown_p2, 4); + if (defender_card) { + this->apply_defense_conditions(as, 0x22, defender_card, 4); + } + + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (card) { + ActionState defense_as = this->create_defense_state_for_card_pair_action_chains(unknown_p2, card); + this->apply_defense_conditions(defense_as, 0x3D, card, 4); + this->apply_defense_conditions(defense_as, 0x3F, card, 4); + } + } + + this->unknown_8024C2B0(0x3D, unknown_p2->get_card_ref(), as, card_ref); + this->unknown_8024C2B0(0x3E, unknown_p2->get_card_ref(), as, card_ref); + if (defender_card) { + this->unknown_8024C2B0(0x22, defender_card->get_card_ref(), as, card_ref); + } + + for (size_t z = 0; (z < 8) && (as.action_card_refs[z] != 0xFFFF); z++) { + this->unknown_8024C2B0(0x3D, as.action_card_refs[z], as, unknown_p2->get_card_ref()); + this->unknown_8024C2B0(0x3E, as.action_card_refs[z], as, unknown_p2->get_card_ref()); + } + + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (card) { + ActionState defense_as = this->create_defense_state_for_card_pair_action_chains(unknown_p2, card); + this->unknown_8024C2B0(0x3D, card->get_card_ref(), defense_as, unknown_p2->get_card_ref()); + this->unknown_8024C2B0(0x3F, card->get_card_ref(), defense_as, unknown_p2->get_card_ref()); + } + } +} + +shared_ptr CardSpecial::sc_card_for_card(shared_ptr unknown_p2) { + auto ps = unknown_p2->player_state(); + return ps ? ps->get_sc_card() : nullptr; +} + +void CardSpecial::unknown_8024A9D8(const ActionState& pa, uint16_t action_card_ref) { + for (size_t z = 0; (z < 8) && (pa.action_card_refs[z] != 0xFFFF); z++) { + if ((action_card_ref == 0xFFFF) || (action_card_ref == pa.action_card_refs[z])) { + if (pa.original_attacker_card_ref == 0xFFFF) { + this->unknown_8024C2B0(0x29, pa.action_card_refs[z], pa, pa.attacker_card_ref); + this->unknown_8024C2B0(0x2A, pa.action_card_refs[z], pa, pa.attacker_card_ref); + } else { + this->unknown_8024C2B0(0x29, pa.action_card_refs[z], pa, pa.target_card_refs[0]); + this->unknown_8024C2B0(0x2B, pa.action_card_refs[z], pa, pa.target_card_refs[0]); + } + } + } +} + +void CardSpecial::unknown_8024504C(shared_ptr unknown_p2) { + if (unknown_p2->action_chain.chain.damage <= 0) { + return; + } + + uint16_t ally_sc_card_ref = this->server()->ruler_server->get_ally_sc_card_ref( + unknown_p2->get_card_ref()); + if (ally_sc_card_ref == 0xFFFF) { + return; + } + + uint8_t ally_client_id = client_id_for_card_ref(ally_sc_card_ref); + if (ally_client_id == 0xFF) { + return; + } + auto ally_sc_card = this->server()->card_for_set_card_ref(ally_sc_card_ref); + if (!ally_sc_card || (ally_sc_card->card_flags & 2)) { + return; + } + + uint8_t client_id = client_id_for_card_ref(unknown_p2->get_card_ref()); + if (client_id == 0xFF) { + return; + } + + auto ally_hes = this->server()->ruler_server->get_hand_and_equip_state_for_client_id(ally_client_id); + if (!ally_hes || !ally_hes->is_cpu_player) { + return; + } + + this->server()->ruler_server->get_hand_and_equip_state_for_client_id(client_id); + auto ps = unknown_p2->player_state(); + if (!ps || (ps->unknown_a16 >= 1)) { + return; + } + + uint16_t card_ref = unknown_p2->get_card_ref(); + if ((unknown_p2->get_definition()->def.type == CardType::ITEM) && ps->get_sc_card()) { + card_ref = ps->get_sc_card()->get_card_ref(); + } + + uint16_t row_card_id = this->server()->card_id_for_card_ref(card_ref); + if (row_card_id == 0xFFFF) { + return; + } + + uint16_t ally_sc_card_id = this->server()->card_id_for_card_ref(ally_sc_card_ref); + if (ally_sc_card_id == 0xFFFF) { + return; + } + + const auto* entry = unknown_8024DAFC(row_card_id, ally_sc_card_id, true); + if (!entry || (this->server()->get_random(99) >= entry->unknown_v1)) { + return; + } + + ps->unknown_a16++; + unknown_p2->action_chain.set_flags(0x100); + + G_ApplyConditionEffect_GC_Ep3_6xB4x06 cmd; + cmd.effect.flags = 0x04; + cmd.effect.attacker_card_ref = this->send_6xB4x06_if_card_ref_invalid( + unknown_p2->get_card_ref(), 0x11); + cmd.effect.target_card_ref = unknown_p2->get_card_ref(); + cmd.effect.value = 0; + cmd.effect.operation = 0x7D; + this->server()->send(cmd); +} + +template +void CardSpecial::unknown_t2(shared_ptr unknown_p2) { + ActionState as = this->create_attack_state_from_card_action_chain(unknown_p2); + + auto sc_card = this->sc_card_for_card(unknown_p2); + uint16_t sc_card_ref = 0xFFFF; + if (sc_card) { + sc_card_ref = sc_card->get_card_ref(); + } + + auto defender_card = unknown_p2; + if (unknown_p2->get_definition() && + (unknown_p2->get_definition()->def.type == CardType::ITEM) && + sc_card) { + defender_card = sc_card; + } + + this->apply_defense_conditions(as, When1, unknown_p2, 4); + this->apply_defense_conditions(as, When2, unknown_p2, 4); + if (defender_card) { + this->apply_defense_conditions(as, When3, defender_card, 4); + } + + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto set_card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (set_card) { + ActionState target_as = this->create_defense_state_for_card_pair_action_chains( + unknown_p2, set_card); + this->apply_defense_conditions(target_as, When1, set_card, 4); + this->apply_defense_conditions(target_as, When4, set_card, 4); + } + } + + this->unknown_8024C2B0(When1, unknown_p2->get_card_ref(), as, sc_card_ref); + this->unknown_8024C2B0(When2, unknown_p2->get_card_ref(), as, sc_card_ref); + if (defender_card) { + this->unknown_8024C2B0(When3, defender_card->get_card_ref(), as, sc_card_ref); + } + for (size_t z = 0; (z < 8) && (as.action_card_refs[z] != 0xFFFF); z++) { + this->unknown_8024C2B0(When1, as.action_card_refs[z], as, unknown_p2->get_card_ref()); + this->unknown_8024C2B0(When2, as.action_card_refs[z], as, unknown_p2->get_card_ref()); + } + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + auto set_card = this->server()->card_for_set_card_ref(as.target_card_refs[z]); + if (set_card) { + ActionState target_as = this->create_defense_state_for_card_pair_action_chains(unknown_p2, set_card); + this->unknown_8024C2B0(When1, set_card->get_card_ref(), target_as, unknown_p2->get_card_ref()); + this->unknown_8024C2B0(When4, set_card->get_card_ref(), target_as, unknown_p2->get_card_ref()); + for (size_t z = 0; (z < 8) && (as.action_card_refs[z] != 0xFFFF); z++) { + this->unknown_8024C2B0(When1, as.action_card_refs[z], target_as, set_card->get_card_ref()); + this->unknown_8024C2B0(When4, as.action_card_refs[z], target_as, set_card->get_card_ref()); + } + } + } +} + +void CardSpecial::unknown_8024997C(shared_ptr card) { + return this->unknown_t2<0x03, 0x0D, 0x21, 0x17>(card); +} + +void CardSpecial::unknown_8024A394(shared_ptr card) { + return this->unknown_t2<0x02, 0x0C, 0x20, 0x16>(card); +} + +bool CardSpecial::client_has_atk_dice_boost_condition(uint8_t client_id) { + auto ps = this->server()->get_player_state(client_id); + if (ps) { + auto card = ps->get_sc_card(); + if (card) { + for (size_t z = 0; z < 9; z++) { + if (!this->card_ref_has_ability_trap(card->action_chain.conditions[z]) && + (card->action_chain.conditions[z].type == ConditionType::ATK_DICE_BOOST)) { + return true; + } + } + } + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = ps->get_set_card(set_index); + if (card) { + for (size_t z = 0; z < 9; z++) { + if (!this->card_ref_has_ability_trap(card->action_chain.conditions[z]) && + (card->action_chain.conditions[z].type == ConditionType::ATK_DICE_BOOST)) { + return true; + } + } + } + } + } + return false; +} + +void CardSpecial::unknown_8024A6DC( + shared_ptr unknown_p2, shared_ptr unknown_p3) { + ActionState as = this->create_defense_state_for_card_pair_action_chains( + unknown_p2, unknown_p3); + for (size_t z = 0; (z < 8) && (as.action_card_refs[z] != 0xFFFF); z++) { + this->unknown_8024C2B0(1, as.action_card_refs[z], as, unknown_p3->get_card_ref()); + this->unknown_8024C2B0(0x15, as.action_card_refs[z], as, unknown_p3->get_card_ref()); + } +} + +vector> CardSpecial::find_all_sc_cards_of_class( + CardClass card_class) const { + vector> ret; + for (size_t z = 0; z < 4; z++) { + auto ps = this->server()->get_player_state(z); + if (ps) { + auto sc_card = ps->get_sc_card(); + if (sc_card && (sc_card->get_definition()->def.card_class() == card_class)) { + ret.emplace_back(sc_card); + } + } + } + return ret; +} + + + +} // namespace Episode3 diff --git a/src/Episode3/CardSpecial.hh b/src/Episode3/CardSpecial.hh new file mode 100644 index 00000000..a8841b0e --- /dev/null +++ b/src/Episode3/CardSpecial.hh @@ -0,0 +1,347 @@ +#pragma once + +#include + +#include + +#include "../Text.hh" +#include "DataIndex.hh" + +namespace Episode3 { + + + +struct UnknownMatrixEntry { + uint16_t card_id; + uint8_t unknown_v1; + uint8_t unknown_v2; +}; + +const UnknownMatrixEntry* unknown_8024DAFC( + uint16_t row_card_id, + uint16_t column_card_id, + bool use_entry_v1, + size_t* out_entry_index = nullptr); + + + +class CardSpecial { +public: + enum class ExpressionTokenType { + SPACE = 0, // Also used for end of string (get_next_expr_token returns null) + REFERENCE = 1, // Reference to a value from the env stats (e.g. hp) + NUMBER = 2, // Constant value (e.g. 2) + SUBTRACT = 3, // "-" in input string + ADD = 4, // "+" in input string + ROUND_DIVIDE = 5, // "/" in input string + FLOOR_DIVIDE = 6, // "//" in input string + MULTIPLY = 7, // "*" in input string + }; + + struct DiceRoll { + uint8_t client_id; + uint8_t unknown_a2; + uint8_t value; + bool value_used_in_expr; + uint16_t unknown_a5; + + DiceRoll(); + void clear(); + }; + + struct AttackEnvStats { + uint32_t num_set_cards; // "f" in expr + uint32_t dice_roll_value1; // "d" in expr + uint32_t effective_ap; // "ap" in expr + uint32_t effective_tp; // "tp" in expr + uint32_t current_hp; // "hp" in expr + uint32_t max_hp; // "mhp" in expr + uint32_t effective_ap_if_not_tech; // "dm" in expr + uint32_t effective_ap_if_not_physical; // "tdm" in expr + uint32_t player_num_destroyed_fcs; // "tf" in expr + uint32_t player_num_atk_points; // "ac" in expr + uint32_t defined_max_hp; // "php" in expr + uint32_t dice_roll_value2; // "dc" in expr + uint32_t card_cost; // "cs" in expr + uint32_t total_num_set_cards; // "a" in expr + uint32_t action_cards_ap; // "kap" in expr + uint32_t action_cards_tp; // "ktp" in expr + uint32_t unknown_a1; // "dn" in expr + uint32_t num_item_or_creature_cards_in_hand; // "hf" in expr + uint32_t num_destroyed_ally_fcs; // "df" in expr + uint32_t target_team_num_set_cards; // "ff" in expr + uint32_t condition_giver_team_num_set_cards; // "ef" in expr + uint32_t num_native_creatures; // "bi" in expr + uint32_t num_a_beast_creatures; // "ab" in expr + uint32_t num_machine_creatures; // "mc" in expr + uint32_t num_dark_creatures; // "dk" in expr + uint32_t num_sword_type_items; // "sa" in expr + uint32_t num_gun_type_items; // "gn" in expr + uint32_t num_cane_type_items; // "wd" in expr + uint32_t effective_ap_if_not_tech2; // "tt" in expr + uint32_t team_dice_boost; // "lv" in expr + uint32_t sc_effective_ap; // "adm" in expr + uint32_t attack_bonus; // "ddm" in expr + uint32_t num_sword_type_items_on_team; // "sat" in expr + uint32_t target_attack_bonus; // "edm" in expr + uint32_t last_attack_preliminary_damage; // "ldm" in expr + uint32_t last_attack_damage; // "rdm" in expr + uint32_t total_last_attack_damage; // "fdm" in expr + uint32_t last_attack_damage_count; // "ndm" in expr + uint32_t target_current_hp; // "ehp" in expr + + AttackEnvStats(); + void clear(); + + uint32_t at(size_t offset) const; + } __attribute__((packed)); + + CardSpecial(std::shared_ptr server); + std::shared_ptr server(); + std::shared_ptr server() const; + + void adjust_attack_damage_due_to_conditions( + std::shared_ptr target_card, int16_t* inout_damage, uint16_t attacker_card_ref); + void adjust_dice_boost_if_team_has_condition_52( + uint8_t team_id, uint8_t* inout_dice_boost, std::shared_ptr card); + void apply_action_conditions( + uint8_t when, + std::shared_ptr attacker_card, + std::shared_ptr defender_card, + uint32_t flags, + const ActionState* as); + bool apply_attribute_guard_if_possible( + uint32_t flags, + CardClass card_class, + std::shared_ptr card, + uint16_t condition_giver_card_ref, + uint16_t attacker_card_ref); + bool apply_defense_condition( + uint8_t when, + Condition* defender_cond, + uint8_t cond_index, + const ActionState& defense_state, + std::shared_ptr defender_card, + uint32_t flags, + bool unknown_p8); + bool apply_defense_conditions( + const ActionState& as, + uint8_t when, + std::shared_ptr defender_card, + uint32_t flags); + bool apply_stat_deltas_to_all_cards_from_all_conditions_with_card_ref( + uint16_t card_ref); + bool apply_stat_deltas_to_card_from_condition_and_clear_cond( + Condition& cond, std::shared_ptr card); + bool apply_stats_deltas_to_card_from_all_conditions_with_card_ref( + uint16_t card_ref, std::shared_ptr card); + bool card_has_condition_with_ref( + std::shared_ptr card, + ConditionType cond_type, + uint16_t card_ref, + uint16_t match_card_ref) const; + bool card_is_destroyed(std::shared_ptr card) const; + void compute_attack_ap( + std::shared_ptr target_card, + int16_t* out_value, + uint16_t attacker_card_ref); + AttackEnvStats compute_attack_env_stats( + const ActionState& pa, + std::shared_ptr card, + const DiceRoll& dice_roll, + uint16_t target_card_ref, + uint16_t condition_giver_card_ref); + std::shared_ptr compute_replaced_target_based_on_conditions( + uint16_t target_card_ref, + int unknown_p3, + int unknown_p4, + uint16_t attacker_card_ref, + uint16_t set_card_ref, + int unknown_p7, + uint32_t* unknown_p9, + uint8_t def_effect_index, + uint32_t* unknown_p11, + uint16_t sc_card_ref); + StatSwapType compute_stat_swap_type(std::shared_ptr card) const; + void compute_team_dice_boost(uint8_t team_id); + bool condition_has_when_20_or_21(const Condition& cond) const; + size_t count_action_cards_with_condition_for_all_current_attacks( + ConditionType cond_type, uint16_t card_ref) const; + size_t count_action_cards_with_condition_for_current_attack( + std::shared_ptr card, ConditionType cond_type, uint16_t card_ref) const; + size_t count_cards_with_card_id_set_by_player_except_card_ref( + uint16_t card_id, uint16_t card_ref) const; + std::vector> get_all_set_cards_by_team_and_class( + CardClass card_class, uint8_t team_id, bool exclude_destroyed_cards) const; + ActionState create_attack_state_from_card_action_chain( + std::shared_ptr attacker_card) const; + ActionState create_defense_state_for_card_pair_action_chains( + std::shared_ptr attacker_card, + std::shared_ptr defender_card) const; + void destroy_card_if_hp_zero( + std::shared_ptr card, uint16_t attacker_card_ref); + bool evaluate_effect_arg2_condition( + const ActionState& as, + std::shared_ptr card, + const char* arg2_text, + DiceRoll& dice_roll, + uint16_t set_card_ref, + uint16_t sc_card_ref, + uint8_t random_percent, + uint8_t when) const; + int32_t evaluate_effect_expr( + const AttackEnvStats& ast, + const char* expr, + DiceRoll& dice_roll) const; + bool execute_effect( + Condition& cond, + std::shared_ptr card, + int16_t expr_value, + int16_t unknown_p5, + ConditionType cond_type, + uint unknown_p7, + uint16_t attacker_card_ref); + const Condition* find_condition_with_parameters( + std::shared_ptr card, + ConditionType cond_type, + uint16_t set_card_ref, + uint8_t def_effect_index) const; + Condition* find_condition_with_parameters( + std::shared_ptr card, + ConditionType cond_type, + uint16_t set_card_ref, + uint8_t def_effect_index) const; + static void get_card1_loc_with_card2_opposite_direction( + Location* out_loc, + std::shared_ptr card1, + std::shared_ptr card2); + uint16_t get_card_id_with_effective_range( + std::shared_ptr card1, uint16_t default_card_id, std::shared_ptr card2) const; + static void get_effective_ap_tp( + StatSwapType type, + int16_t* effective_ap, + int16_t* effective_tp, + int16_t hp, + int16_t ap, + int16_t tp); + const char* get_next_expr_token( + const char *expr, ExpressionTokenType* out_type, int32_t* out_value) const; + std::vector> get_targeted_cards_for_condition( + uint16_t card_ref, + uint8_t def_effect_index, + uint16_t setter_card_ref, + const ActionState& as, + int16_t p_target_type, + bool apply_usability_filters) const; + std::vector> get_targeted_cards_for_condition( + uint16_t card_ref, + uint8_t def_effect_index, + uint16_t setter_card_ref, + const ActionState& as, + int16_t p_target_type, + bool apply_usability_filters); + bool is_card_targeted_by_condition( + const Condition& cond, const ActionState& as, std::shared_ptr card) const; + void on_card_set(std::shared_ptr ps, uint16_t card_ref); + const CardDefinition::Effect* original_definition_for_condition( + const Condition& cond) const; + bool card_ref_has_ability_trap(const Condition& eff) const; + void send_6xB4x06_for_exp_change( + std::shared_ptr card, + uint16_t attacker_card_ref, + uint8_t dice_roll_value, + bool unknown_p5) const; + void send_6xB4x06_for_card_destroyed( + std::shared_ptr destroyed_card, uint16_t attacker_card_ref) const; + uint16_t send_6xB4x06_if_card_ref_invalid( + uint16_t card_ref, int16_t value) const; + void send_6xB4x06_for_stat_delta( + std::shared_ptr card, + uint16_t attacker_card_ref, + uint32_t flags, + int16_t hp_delta, + bool unknown_p6, + bool unknown_p7) const; + bool should_cancel_condition_due_to_anti_abnormality( + const CardDefinition::Effect& eff, + std::shared_ptr card, + uint16_t target_card_ref, + uint16_t sc_card_ref) const; + bool should_return_card_ref_to_hand_on_destruction( + uint16_t card_ref) const; + size_t sum_last_attack_damage( + std::vector>* out_cards, + int32_t* out_damage_sum, + size_t* out_damage_count) const; + void update_condition_orders(std::shared_ptr card); + int16_t max_all_attack_bonuses(size_t* out_count) const; + void unknown_80244AA8(std::shared_ptr card); + void unknown_80244E20( + std::shared_ptr attacker_card, + std::shared_ptr target_card, + int16_t* inout_unknown_p4); + void unknown_8024C2B0( + uint32_t when, + uint16_t set_card_ref, + const ActionState& as, + uint16_t sc_card_ref, + bool apply_defense_condition_to_all_cards = true, + uint16_t apply_defense_condition_to_card_ref = 0xFFFF); + std::vector> get_all_set_cards() const; + std::vector> find_cards_by_condition_inc_exc( + ConditionType include_cond, + ConditionType exclude_cond = ConditionType::NONE, + AssistEffect include_eff = AssistEffect::NONE, + AssistEffect exclude_eff = AssistEffect::NONE) const; + void clear_invalid_conditions_on_card( + std::shared_ptr card, const ActionState& as); + void on_card_destroyed( + std::shared_ptr attacker_card, std::shared_ptr destroyed_card); + std::vector> find_cards_in_hp_range( + int16_t min, int16_t max) const; + std::vector> find_all_cards_by_aerial_attribute(bool is_aerial) const; + std::vector> find_cards_damaged_by_at_least(int16_t damage) const; + std::vector> find_all_set_cards_on_client_team(uint8_t client_id) const; + std::vector> find_all_cards_on_same_or_other_team(uint8_t client_id, bool same_team) const; + std::shared_ptr sc_card_for_client_id(uint8_t client_id) const; + std::shared_ptr get_attacker_card(const ActionState& as) const; + std::vector> get_attacker_card_and_sc_if_item(const ActionState& as) const; + std::vector> find_all_set_cards_with_cost_in_range(uint8_t min_cost, uint8_t max_cost) const; + std::vector> filter_cards_by_range( + const std::vector>& cards, + std::shared_ptr card1, + const Location& card1_loc, + std::shared_ptr card2) const; + void unknown_8024AAB8(const ActionState& as); + void unknown_80244BE4(std::shared_ptr unknown_p2); + void unknown_80244CA8(std::shared_ptr card); + template + void unknown1_t( + std::shared_ptr unknown_p2, const ActionState* existing_as = nullptr); + void unknown_80249060(std::shared_ptr unknown_p2); + void unknown_80249254(std::shared_ptr unknown_p2); + void unknown_8024945C(std::shared_ptr unknown_p2, const ActionState& existing_as); + void unknown_8024966C(std::shared_ptr unknown_p2, const ActionState* existing_as); + static std::shared_ptr sc_card_for_card(std::shared_ptr unknown_p2); + void unknown_8024A9D8(const ActionState& pa, uint16_t action_card_ref); + void unknown_8024504C(std::shared_ptr unknown_p2); + template + void unknown_t2(std::shared_ptr unknown_p2); + void unknown_8024997C(std::shared_ptr card); + void unknown_8024A394(std::shared_ptr card); + bool client_has_atk_dice_boost_condition(uint8_t client_id); + void unknown_8024A6DC( + std::shared_ptr unknown_p2, std::shared_ptr unknown_p3); + std::vector> find_all_sc_cards_of_class( + CardClass card_class) const; + +private: + std::weak_ptr w_server; + ActionState unknown_action_state_a1; + ActionState action_state; + uint16_t unknown_a2; +}; + + + +} // namespace Episode3 diff --git a/src/Episode3/DataIndex.cc b/src/Episode3/DataIndex.cc new file mode 100644 index 00000000..2c42f287 --- /dev/null +++ b/src/Episode3/DataIndex.cc @@ -0,0 +1,1403 @@ +#include "DataIndex.hh" + +#include + +#include +#include +#include + +#include "../Loggers.hh" +#include "../Compression.hh" +#include "../Text.hh" + +using namespace std; + +namespace Episode3 { + + + +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) + : x(x), y(y), direction(direction), unused(0) { } + +void Location::clear() { + this->x = 0; + this->y = 0; + this->direction = Direction::RIGHT; + this->unused = 0; +} + +void Location::clear_FF() { + this->x = 0xFF; + this->y = 0xFF; + this->direction = Direction::INVALID_FF; + this->unused = 0xFF; +} + + + +Direction turn_left(Direction d) { + switch (d) { + case Direction::RIGHT: + return Direction::UP; + case Direction::UP: + return Direction::LEFT; + case Direction::LEFT: + return Direction::DOWN; + case Direction::DOWN: + return Direction::RIGHT; + default: + return Direction::INVALID_FF; + } +} + +Direction turn_right(Direction d) { + switch (d) { + case Direction::RIGHT: + return Direction::DOWN; + case Direction::UP: + return Direction::RIGHT; + case Direction::LEFT: + return Direction::UP; + case Direction::DOWN: + return Direction::LEFT; + default: + return Direction::INVALID_FF; + } +} + +Direction turn_around(Direction d) { + switch (d) { + case Direction::RIGHT: + return Direction::LEFT; + case Direction::UP: + return Direction::DOWN; + case Direction::LEFT: + return Direction::RIGHT; + case Direction::DOWN: + return Direction::UP; + default: + return Direction::INVALID_FF; + } +} + +const char* name_for_direction(Direction d) { + switch (d) { + case Direction::RIGHT: + return "LEFT"; + case Direction::UP: + return "DOWN"; + case Direction::LEFT: + return "RIGHT"; + case Direction::DOWN: + return "UP"; + case Direction::INVALID_FF: + return "INVALID_FF"; + default: + return "__unknown__"; + } +} + + + +bool card_class_is_tech_like(CardClass cc) { + return (cc == CardClass::TECH) || + (cc == CardClass::PHOTON_BLAST) || + (cc == CardClass::BOSS_TECH); +} + + + +static const vector name_for_card_type({ + "HunterSC", + "ArkzSC", + "Item", + "Creature", + "Action", + "Assist", +}); + +static const unordered_map description_for_expr_token({ + {"f", "Number of FCs controlled by current SC"}, + {"d", "Die roll"}, + {"ap", "Attacker effective AP"}, + {"tp", "Attacker effective TP"}, + {"hp", "Current HP"}, + {"mhp", "Maximum HP"}, + {"dm", "Physical damage"}, + {"tdm", "Technique damage"}, + {"tf", "Number of SC\'s destroyed FCs"}, + {"ac", "Remaining ATK points"}, + {"php", "Maximum HP"}, + {"dc", "Die roll"}, + {"cs", "Card set cost"}, + {"a", "Number of FCs on all teams"}, + {"kap", "Action cards AP"}, + {"ktp", "Action cards TP"}, + {"dn", "Unknown: dn"}, + {"hf", "Number of item or creature cards in hand"}, + {"df", "Number of destroyed ally FCs (including SC\'s own)"}, + {"ff", "Number of ally FCs (including SC\'s own)"}, + {"ef", "Number of enemy FCs"}, + {"bi", "Number of Native FCs on either team"}, + {"ab", "Number of A.Beast FCs on either team"}, + {"mc", "Number of Machine FCs on either team"}, + {"dk", "Number of Dark FCs on either team"}, + {"sa", "Number of Sword-type items on either team"}, + {"gn", "Number of Gun-type items on either team"}, + {"wd", "Number of Cane-type items on either team"}, + {"tt", "Physical damage"}, + {"lv", "Dice boost"}, + {"adm", "SC attack damage"}, + {"ddm", "Defending damage"}, + {"sat", "Number of Sword-type items on SC\'s team"}, + {"edm", "Defending damage"}, // TODO: How is this different from ddm? + {"ldm", "Unknown: ldm"}, // Unused + {"rdm", "Defending damage"}, // TODO: How is this different from ddm/edm? + {"fdm", "Final damage (after defense)"}, + {"ndm", "Unknown: ndm"}, // Unused + {"ehp", "Attacker HP"}, +}); + +// Arguments are encoded as 3-character null-terminated strings (why?!), and are +// used for adding conditions to effects (e.g. making them only trigger in +// certain situations) or otherwise customizing their results. The arguments are +// heterogeneous based on their position; that is, the first argument always has +// the same meaning, and meaning letters that are valid in arg1 are not +// necessarily valid in arg2, etc. +// Argument meanings: +// a01 = ??? +// e00 = effect lasts while equipped? (in contrast to tXX) +// pXX = who to target (see description_for_p_target) +// In arg2: +// bXX = require attack doing not more than XX damage +// cXY/CXY = linked items (require item with cYX/CYX to be equipped as well) +// dXY = roll one die; require result between X and Y inclusive +// hXX = require HP >= XX +// iXX = require HP <= XX +// mXX = require attack doing at least XX damage +// nXX = require condition (see description_for_n_condition below) +// oXX = seems to be "require previous random-condition effect to have happened" +// TODO: this is used as both o01 (recovery) and o11 (reflection) +// conditions - why the difference? +// rXX = randomly pass with XX% chance (if XX == 00, 100% chance?) +// sXY = require card cost between X and Y ATK points (inclusive) +// tXX = lasts XX turns, or activate after XX turns + +static const vector description_for_n_condition({ + /* n00 */ "Always true", + /* n01 */ "Card is Hunters-side SC", + /* n02 */ "Destroyed with a single attack", + /* n03 */ "Technique or PB action card was used", + /* n04 */ "Attack has Pierce", + /* n05 */ "Attack has Rampage", + /* n06 */ "Native attribute", + /* n07 */ "A.Beast attribute", + /* n08 */ "Machine attribute", + /* n09 */ "Dark attribute", + /* n10 */ "Sword-type item", + /* n11 */ "Gun-type item", + /* n12 */ "Cane-type item", + /* n13 */ "Guard item or MAG", + /* n14 */ "Story Character", + /* n15 */ "Attacker does not use action cards", + /* n16 */ "Aerial attribute", + /* n17 */ "Same AP as opponent", + /* n18 */ "Any target is an SC", + /* n19 */ "Has Paralyzed condition", + /* n20 */ "Has Frozen condition", + /* n21 */ "???", // TODO: This appears related to Pierce/Rampage + /* n22 */ "???", // TODO: This appears related to Pierce/Rampage +}); + +static const vector description_for_p_target({ + /* p00 */ "Unknown: p00", // Unused; probably invalid + /* p01 */ "SC / FC who set the card", + /* p02 */ "Attacking SC / FC", + /* p03 */ "Unknown: p03", // Unused + /* p04 */ "Unknown: p04", // Unused + /* p05 */ "SC / FC who set the card", // Identical to p01 + /* p06 */ "??? (TODO)", + /* p07 */ "??? (TODO; Weakness)", + /* p08 */ "FC\'s owner SC", + /* p09 */ "Unknown: p09", // Unused + /* p10 */ "All ally FCs", + /* p11 */ "All ally FCs", // TODO: how is this different from p10? + /* p12 */ "All non-aerial FCs on both teams", + /* p13 */ "All FCs on both teams that are Frozen", + /* p14 */ "All FCs on both teams that have <= 3 HP", + /* p15 */ "All FCs except SCs", // TODO: used during attacks only? + /* p16 */ "All FCs except SCs", // TODO: used during attacks only? how is this different from p15? + /* p17 */ "This card", + /* p18 */ "SC who equipped this card", + /* p19 */ "Unknown: p19", // Unused + /* p20 */ "Unknown: p20", // Unused + /* p21 */ "Unknown: p21", // Unused + /* p22 */ "All characters (SCs & FCs) including this card", // TODO: But why does Shifta apply only to allies then? + /* p23 */ "All characters (SCs & FCs) except this card", + /* p24 */ "All FCs on both teams that have Paralysis", + /* p25 */ "Unknown: p25", // Unused + /* p26 */ "Unknown: p26", // Unused + /* p27 */ "Unknown: p27", // Unused + /* p28 */ "Unknown: p28", // Unused + /* p29 */ "Unknown: p29", // Unused + /* p30 */ "Unknown: p30", // Unused + /* p31 */ "Unknown: p31", // Unused + /* p32 */ "Unknown: p32", // Unused + /* p33 */ "Unknown: p33", // Unused + /* p34 */ "Unknown: p34", // Unused + /* p35 */ "All characters (SCs & FCs) within range", // Used for Explosion effect + /* p36 */ "All ally SCs within range, but not the caster", // Resta + /* p37 */ "All FCs or all opponent FCs (TODO)", // TODO: when to use which selector? is a3 involved here somehow? + /* p38 */ "All allies except items within range (and not this card)", + /* p39 */ "All FCs that cost 4 or more points", + /* p40 */ "All FCs that cost 3 or fewer points", + /* p41 */ "Unknown: p41", // Unused + /* p42 */ "Attacker during defense phase", // Only used by TP Defense + /* p43 */ "Owner SC of defending FC during attack", + /* p44 */ "SC\'s own creature FCs within range", + /* p45 */ "Both attacker and defender", // Used for Snatch, which moves EXP from one to the other + /* p46 */ "All SCs & FCs one space left or right of this card", + /* p47 */ "FC\'s owner Boss SC", // Only used for Gibbles+ which explicitly mentions Boss SC, so it looks like this is p08 but for bosses + /* p48 */ "Everything within range, including this card\'s user", // Madness + /* p49 */ "All ally FCs within range except this card", +}); + +struct ConditionDescription { + bool has_expr; + const char* name; + const char* description; +}; + +static const vector 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}, +}); + +void CardDefinition::Stat::decode_code() { + this->type = static_cast(this->code / 1000); + int16_t value = this->code - (this->type * 1000); + if (value != 999) { + switch (this->type) { + case Type::BLANK: + this->stat = 0; + break; + case Type::STAT: + case Type::PLUS_STAT: + case Type::EQUALS_STAT: + this->stat = value; + break; + case Type::MINUS_STAT: + this->stat = -value; + break; + default: + throw runtime_error("invalid card stat type"); + } + } else { + this->stat = 0; + this->type = static_cast(this->type + 4); + } +} + +string CardDefinition::Stat::str() const { + switch (this->type) { + case Type::BLANK: + return ""; + case Type::STAT: + return string_printf("%hhd", this->stat); + case Type::PLUS_STAT: + return string_printf("+%hhd", this->stat); + case Type::MINUS_STAT: + return string_printf("-%d", -this->stat); + case Type::EQUALS_STAT: + return string_printf("=%hhd", this->stat); + case Type::UNKNOWN: + return "?"; + case Type::PLUS_UNKNOWN: + return "+?"; + case Type::MINUS_UNKNOWN: + return "-?"; + case Type::EQUALS_UNKNOWN: + return "=?"; + default: + return string_printf("[%02hhX %02hhX]", this->type, this->stat); + } +} + + + +bool CardDefinition::Effect::is_empty() const { + return (this->effect_num == 0 && + this->type == ConditionType::NONE && + this->expr.is_filled_with(0) && + this->when == 0 && + this->arg1.is_filled_with(0) && + this->arg2.is_filled_with(0) && + this->arg3.is_filled_with(0) && + this->apply_criterion == CriterionCode::NONE && + this->unknown_a2 == 0); +} + +string CardDefinition::Effect::str_for_arg(const string& arg) { + if (arg.empty()) { + return arg; + } + if (arg.size() != 3) { + return arg + "/(invalid)"; + } + size_t value; + try { + value = stoul(arg.c_str() + 1, nullptr, 10); + } catch (const invalid_argument&) { + return arg + "/(invalid)"; + } + + switch (arg[0]) { + case 'a': + return arg + "/(unknown)"; + case 'C': + case 'c': + return string_printf("%s/Req. linked item (%zu=>%zu)", arg.c_str(), value / 10, value % 10); + case 'd': + return string_printf("%s/Req. die roll in [%zu, %zu]", arg.c_str(), value / 10, value % 10); + case 'e': + return arg + "/While equipped"; + case 'h': + return string_printf("%s/Req. HP >= %zu", arg.c_str(), value); + case 'i': + return string_printf("%s/Req. HP <= %zu", arg.c_str(), value); + case 'n': + try { + return string_printf("%s/Req. condition: %s", arg.c_str(), description_for_n_condition.at(value)); + } catch (const out_of_range&) { + return arg + "/(unknown)"; + } + case 'o': + return arg + "/Req. prev effect conditions passed"; + case 'p': + try { + return string_printf("%s/Target: %s", arg.c_str(), description_for_p_target.at(value)); + } catch (const out_of_range&) { + return arg + "/(unknown)"; + } + case 'r': + return string_printf("%s/Req. random with %zu%% chance", arg.c_str(), value == 0 ? 100 : value); + case 's': + return string_printf("%s/Req. cost in [%zu, %zu]", arg.c_str(), value / 10, value % 10); + case 't': + return string_printf("%s/Turns: %zu", arg.c_str(), value); + default: + return arg + "/(unknown)"; + } +} + +string CardDefinition::Effect::str() const { + uint8_t type = static_cast(this->type); + string cmd_str = string_printf("(%hhu) %02hhX", this->effect_num, type); + try { + const char* name = description_for_condition_type.at(type).name; + if (name) { + cmd_str += ':'; + cmd_str += name; + } + } catch (const out_of_range&) { } + + string expr_str = this->expr; + if (!expr_str.empty()) { + expr_str = ", expr=" + expr_str; + } + + string arg1str = this->str_for_arg(this->arg1); + string arg2str = this->str_for_arg(this->arg2); + string arg3str = this->str_for_arg(this->arg3); + return string_printf("(cmd=%s%s, when=%02hhX, arg1=%s, arg2=%s, arg3=%s, cond=%02hhX, a2=%02hhX)", + cmd_str.c_str(), expr_str.c_str(), this->when, arg1str.data(), + arg2str.data(), arg3str.data(), static_cast(this->apply_criterion), this->unknown_a2); +} + + + +bool CardDefinition::is_sc() const { + return (this->type == CardType::HUNTERS_SC) || (this->type == CardType::ARKZ_SC); +} + +bool CardDefinition::is_fc() const { + return (this->type == CardType::ITEM) || (this->type == CardType::CREATURE); +} + +bool CardDefinition::is_named_android_sc() const { + static const unordered_set TARGET_IDS({ + 0x0005, 0x0007, 0x0110, 0x0113, 0x0114, 0x0117, 0x011B, 0x011F}); + return TARGET_IDS.count(this->card_id); +} + +bool CardDefinition::any_top_color_matches(const CardDefinition& other) const { + for (size_t x = 0; x < this->top_colors.size(); x++) { + if (this->top_colors[x] != 0) { + for (size_t y = 0; y < other.top_colors.size(); y++) { + if (this->top_colors[x] == other.top_colors[y]) { + return true; + } + } + } + } + return false; +} + +CardClass CardDefinition::card_class() const { + return static_cast(this->be_card_class.load()); +} + + + +void CardDefinition::decode_range() { + // If the cell representing the FC is nonzero, the card has a range from a + // list of constants. Otherwise, its range is already defined in the range + // array and should be left alone. + uint8_t index = (this->range[4] >> 8) & 0xF; + if (index != 0) { + this->range.clear(0); + switch (index) { + case 1: // Single cell in front of FC + this->range[3] = 0x00000100; + break; + case 2: // Cell in front of FC and the front-left and front-right (Slash) + this->range[3] = 0x00001110; + break; + case 3: // 3 cells in a line in front of FC + this->range[1] = 0x00000100; + this->range[2] = 0x00000100; + this->range[3] = 0x00000100; + break; + case 4: // All 8 cells around FC + this->range[3] = 0x00001110; + this->range[4] = 0x00001010; + this->range[5] = 0x00001110; + break; + case 5: // 2 cells in a line in front of FC + this->range[2] = 0x00000100; + this->range[3] = 0x00000100; + break; + case 6: // Entire field (renders as "A") + for (size_t x = 0; x < 6; x++) { + this->range[x] = 0x000FFFFF; + } + break; + case 7: // Superposition of 4 and 5 (unused) + this->range[2] = 0x00000100; + this->range[3] = 0x00001110; + this->range[4] = 0x00001010; + this->range[5] = 0x00001110; + break; + case 8: // All 8 cells around FC and FC's cell + this->range[3] = 0x00001110; + this->range[4] = 0x00001110; + this->range[5] = 0x00001110; + break; + case 9: // No cells + break; + // The table in the DOL file only appears to contain 9 entries; there are + // some pointers immediately after. So probably if a card specified A-F, + // its range would be filled in with garbage in the original game. + default: + throw runtime_error("invalid fixed range index"); + } + } +} + +string name_for_rarity(CardRarity rarity) { + static const vector names({ + "N1", + "R1", + "S", + "E", + "N2", + "N3", + "N4", + "R2", + "R3", + "R4", + "SS", + "D1", + "D2", + "INVIS", + }); + try { + return names.at(static_cast(rarity) - 1); + } catch (const out_of_range&) { + return string_printf("(%02hhX)", static_cast(rarity)); + } +} + +string name_for_target_mode(TargetMode target_mode) { + static const vector names({ + "NONE", + "SINGLE", + "MULTI", + "SELF", + "TEAM", + "ALL", + "MULTI-ALLY", + "ALL-ALLY", + "ALL-ATTACK", + "OWN-FCS", + }); + try { + return names.at(static_cast(target_mode)); + } catch (const out_of_range&) { + return string_printf("(%02hhX)", static_cast(target_mode)); + } +} + +string string_for_colors(const parray& colors) { + string ret; + for (size_t x = 0; x < 8; x++) { + if (colors[x]) { + ret += '0' + colors[x]; + } + } + if (ret.empty()) { + return "none"; + } + return ret; +} + +string string_for_assist_turns(uint8_t turns) { + if (turns == 90) { + return "ONCE"; + } else if (turns == 99) { + return "FOREVER"; + } else { + return string_printf("%hhu", turns); + } +} + +string string_for_range(const parray& range) { + string ret; + for (size_t x = 0; x < 6; x++) { + ret += string_printf("%05" PRIX32 "/", range[x].load()); + } + while (starts_with(ret, "00000/")) { + ret = ret.substr(6); + } + if (!ret.empty()) { + ret.resize(ret.size() - 1); + } + return ret; +} + +string CardDefinition::str() const { + string type_str; + try { + type_str = name_for_card_type.at(static_cast(this->type)); + } catch (const out_of_range&) { + type_str = string_printf("%02hhX", static_cast(this->type)); + } + string rarity_str = name_for_rarity(this->rarity); + string target_mode_str = name_for_target_mode(this->target_mode); + string range_str = string_for_range(this->range); + string assist_turns_str = string_for_assist_turns(this->assist_turns); + string hp_str = this->hp.str(); + string ap_str = this->ap.str(); + string tp_str = this->tp.str(); + string mv_str = this->mv.str(); + string left_str = string_for_colors(this->left_colors); + string right_str = string_for_colors(this->right_colors); + string top_str = string_for_colors(this->top_colors); + string effects_str; + for (size_t x = 0; x < 3; x++) { + if (this->effects[x].is_empty()) { + continue; + } + if (!effects_str.empty()) { + effects_str += ", "; + } + effects_str += this->effects[x].str(); + } + return string_printf( + "[Card: %04" PRIX32 " name=%s type=%s usable_condition=%02hhX rare=%s " + "cost=%hhX+%hhX target=%s range=%s assist_turns=%s cannot_move=%s " + "cannot_attack=%s hidden=%s hp=%s ap=%s tp=%s mv=%s left=%s right=%s " + "top=%s a2=%04hX class=%04hX assist_effect=[%hu, %hu] " + "drop_rates=[%hu, %hu] effects=[%s]]", + this->card_id.load(), + this->en_name.data(), + type_str.c_str(), + static_cast(this->usable_criterion), + rarity_str.c_str(), + this->self_cost, + this->ally_cost, + target_mode_str.c_str(), + range_str.c_str(), + assist_turns_str.c_str(), + this->cannot_move ? "true" : "false", + this->cannot_attack ? "true" : "false", + this->hide_in_deck_edit ? "true" : "false", + hp_str.c_str(), + ap_str.c_str(), + tp_str.c_str(), + mv_str.c_str(), + left_str.c_str(), + right_str.c_str(), + top_str.c_str(), + this->unknown_a2.load(), + this->be_card_class.load(), + this->assist_effect[0].load(), + this->assist_effect[1].load(), + this->drop_rates[0].load(), + this->drop_rates[1].load(), + effects_str.c_str()); +} + + + +Rules::Rules() { + this->clear(); +} + +void Rules::clear() { + this->overall_time_limit = 0; + this->phase_time_limit = 0; + this->allowed_cards = AllowedCards::ALL; + this->min_dice = 0; + this->max_dice = 0; + this->disable_deck_shuffle = 0; + this->disable_deck_loop = 0; + this->char_hp = 0; + this->hp_type = HPType::DEFEAT_PLAYER; + this->no_assist_cards = 0; + this->disable_dialogue = 0; + this->dice_exchange_mode = DiceExchangeMode::HIGH_ATK; + this->disable_dice_boost = 0; + this->unused.clear(0); +} + +string Rules::str() const { + vector tokens; + + tokens.emplace_back(string_printf("char_hp=%hhu", this->char_hp)); + switch (this->hp_type) { + case HPType::DEFEAT_PLAYER: + tokens.emplace_back("hp_type=DEFEAT_PLAYER"); + break; + case HPType::DEFEAT_TEAM: + tokens.emplace_back("hp_type=DEFEAT_TEAM"); + break; + case HPType::COMMON_HP: + tokens.emplace_back("hp_type=COMMON_HP"); + break; + default: + tokens.emplace_back(string_printf("hp_type=(%02hhX)", + static_cast(this->hp_type))); + break; + } + + tokens.emplace_back(string_printf("min_dice=%hhu", this->min_dice)); + tokens.emplace_back(string_printf("max_dice=%hhu", this->max_dice)); + switch (this->dice_exchange_mode) { + case DiceExchangeMode::HIGH_ATK: + tokens.emplace_back("dice_exchange=HIGH_ATK"); + break; + case DiceExchangeMode::HIGH_DEF: + tokens.emplace_back("dice_exchange=HIGH_DEF"); + break; + case DiceExchangeMode::NONE: + tokens.emplace_back("dice_exchange=NONE"); + break; + default: + tokens.emplace_back(string_printf("dice_exchange=(%02hhX)", + static_cast(this->dice_exchange_mode))); + break; + } + tokens.emplace_back(string_printf("dice_boost=%s", this->disable_dice_boost ? "DISABLED" : "ENABLED")); + + tokens.emplace_back(string_printf("deck_shuffle=%s", this->disable_deck_shuffle ? "DISABLED" : "ENABLED")); + tokens.emplace_back(string_printf("deck_loop=%s", this->disable_deck_loop ? "DISABLED" : "ENABLED")); + + switch (this->allowed_cards) { + case AllowedCards::ALL: + tokens.emplace_back("allowed_cards=ALL"); + break; + case AllowedCards::N_ONLY: + tokens.emplace_back("allowed_cards=N_ONLY"); + break; + case AllowedCards::N_R_ONLY: + tokens.emplace_back("allowed_cards=N_R_ONLY"); + break; + case AllowedCards::N_R_S_ONLY: + tokens.emplace_back("allowed_cards=N_R_S_ONLY"); + break; + default: + tokens.emplace_back(string_printf("allowed_cards=(%02hhX)", + static_cast(this->allowed_cards))); + break; + } + tokens.emplace_back(string_printf("assist_cards=%s", this->no_assist_cards ? "DISALLOWED" : "ALLOWED")); + + tokens.emplace_back(string_printf("time_limit=%zumin", static_cast(this->overall_time_limit * 5))); + tokens.emplace_back(string_printf("phase_time_limit=%hhusec", this->phase_time_limit)); + + tokens.emplace_back(string_printf("dialogue=%s", this->disable_dialogue ? "DISABLED" : "ENABLED")); + + return "Rules[" + join(tokens, ", ") + "]"; +} + + + +StateFlags::StateFlags() { + this->clear(); +} + +void StateFlags::clear() { + this->turn_num = 0; + this->battle_phase = BattlePhase::INVALID_00; + this->current_team_turn1 = 0; + this->current_team_turn2 = 0; + this->action_subphase = ActionSubphase::ATTACK; + this->setup_phase = SetupPhase::REGISTRATION; + this->registration_phase = RegistrationPhase::AWAITING_NUM_PLAYERS; + this->team_exp.clear(0); + this->team_dice_boost.clear(0); + this->first_team_turn = 0; + this->tournament_flag = 0; + this->client_sc_card_types.clear(CardType::HUNTERS_SC); +} + +void StateFlags::clear_FF() { + this->turn_num = 0xFFFF; + this->battle_phase = BattlePhase::INVALID_FF; + this->current_team_turn1 = 0xFF; + this->current_team_turn2 = 0xFF; + this->action_subphase = ActionSubphase::INVALID_FF; + this->setup_phase = SetupPhase::INVALID_FF; + this->registration_phase = RegistrationPhase::INVALID_FF; + this->team_exp.clear(0xFFFFFFFF); + this->team_dice_boost.clear(0xFF); + this->first_team_turn = 0xFF; + this->tournament_flag = 0xFF; + this->client_sc_card_types.clear(CardType::INVALID_FF); +} + + + +string MapDefinition::str(const DataIndex* data_index) const { + deque lines; + auto add_map = [&](const parray, 0x10>& tiles) { + for (size_t y = 0; y < 0x10; y++) { + string line = " "; + for (size_t x = 0; x < 0x10; x++) { + line += string_printf(" %02hhX", tiles[y][x]); + } + lines.emplace_back(move(line)); + } + }; + + 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(" 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:"); + add_map(this->map_tiles); + lines.emplace_back(string_printf(" start_tile_definitions=[%02hhX %02hhX %02hhX %02hhX %02hhX %02hhX], [%02hhX %02hhX %02hhX %02hhX %02hhX %02hhX]", + this->start_tile_definitions[0][0], this->start_tile_definitions[0][1], + this->start_tile_definitions[0][2], this->start_tile_definitions[0][3], + this->start_tile_definitions[0][4], this->start_tile_definitions[0][5], + this->start_tile_definitions[1][0], this->start_tile_definitions[1][1], + this->start_tile_definitions[1][2], this->start_tile_definitions[1][3], + this->start_tile_definitions[1][4], this->start_tile_definitions[1][5])); + for (size_t z = 0; z < this->num_alt_maps; z++) { + for (size_t w = 0; w < 2; w++) { + lines.emplace_back(string_printf(" alt tiles %zu/%zu:", z, w)); + add_map(this->alt_maps1[w][z]); + } + for (size_t w = 0; w < 2; w++) { + lines.emplace_back(string_printf(" alt tiles a3 %zu/%zu=%g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g", z, w, + this->alt_maps_unknown_a3[w][z][0x00].load(), this->alt_maps_unknown_a3[w][z][0x01].load(), + this->alt_maps_unknown_a3[w][z][0x02].load(), this->alt_maps_unknown_a3[w][z][0x03].load(), + this->alt_maps_unknown_a3[w][z][0x04].load(), this->alt_maps_unknown_a3[w][z][0x05].load(), + this->alt_maps_unknown_a3[w][z][0x06].load(), this->alt_maps_unknown_a3[w][z][0x07].load(), + this->alt_maps_unknown_a3[w][z][0x08].load(), this->alt_maps_unknown_a3[w][z][0x09].load(), + this->alt_maps_unknown_a3[w][z][0x0A].load(), this->alt_maps_unknown_a3[w][z][0x0B].load(), + this->alt_maps_unknown_a3[w][z][0x0C].load(), this->alt_maps_unknown_a3[w][z][0x0D].load(), + this->alt_maps_unknown_a3[w][z][0x0E].load(), this->alt_maps_unknown_a3[w][z][0x0F].load(), + this->alt_maps_unknown_a3[w][z][0x10].load(), this->alt_maps_unknown_a3[w][z][0x11].load())); + } + } + 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())); + } + } + lines.emplace_back(" modification tiles:"); + add_map(this->modification_tiles); + for (size_t z = 0; z < 0x70; z += 0x10) { + lines.emplace_back(string_printf(" a6[0x%02zX:0x%02zX]=%02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX %02hhX", z, z + 0x10, + this->unknown_a6[z + 0x00], this->unknown_a6[z + 0x01], this->unknown_a6[z + 0x02], this->unknown_a6[z + 0x03], + this->unknown_a6[z + 0x04], this->unknown_a6[z + 0x05], this->unknown_a6[z + 0x06], this->unknown_a6[z + 0x07], + this->unknown_a6[z + 0x08], this->unknown_a6[z + 0x09], this->unknown_a6[z + 0x0A], this->unknown_a6[z + 0x0B], + this->unknown_a6[z + 0x0C], this->unknown_a6[z + 0x0D], this->unknown_a6[z + 0x0E], this->unknown_a6[z + 0x0F])); + } + lines.emplace_back(string_printf(" a6[0x70:0x74]=%02hhX %02hhX %02hhX %02hhX", + this->unknown_a6[0x70], this->unknown_a6[0x71], this->unknown_a6[0x72], this->unknown_a6[0x73])); + lines.emplace_back(" default_rules: " + this->default_rules.str()); + lines.emplace_back(string_printf(" a7=%02hhX %02hhX %02hhX %02hhX", + this->unknown_a7[0], this->unknown_a6[1], this->unknown_a6[2], this->unknown_a6[3])); + lines.emplace_back(" name: " + string(this->name)); + lines.emplace_back(" location_name: " + string(this->location_name)); + lines.emplace_back(" quest_name: " + string(this->quest_name)); + lines.emplace_back(" description: " + string(this->description)); + lines.emplace_back(string_printf(" map_xy: %hu %hu", this->map_x.load(), this->map_y.load())); + for (size_t z = 0; z < 3; z++) { + lines.emplace_back(string_printf(" npc_chars[%zu]:", z)); + lines.emplace_back(string_printf(" a1=%04hX %04hX", + this->npc_chars[z].unknown_a1[0].load(), this->npc_chars[z].unknown_a1[1].load())); + lines.emplace_back(string_printf(" a2=%02hX %02hX %02hX %02hX", + this->npc_chars[z].unknown_a2[0], this->npc_chars[z].unknown_a2[1], + 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, + 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(), + this->npc_chars[z].unknown_a3[w + 0x06].load(), this->npc_chars[z].unknown_a3[w + 0x07].load())); + } + lines.emplace_back(string_printf(" a3[0x78:0x7E]=%04hX %04hX %04hX %04hX %04hX %04hX", + this->npc_chars[z].unknown_a3[0x78].load(), this->npc_chars[z].unknown_a3[0x79].load(), + this->npc_chars[z].unknown_a3[0x7A].load(), this->npc_chars[z].unknown_a3[0x7B].load(), + this->npc_chars[z].unknown_a3[0x7C].load(), this->npc_chars[z].unknown_a3[0x7D].load())); + lines.emplace_back(string_printf(" npc_decks[%zu]:", z)); + lines.emplace_back(" name: " + string(this->npc_decks[z].name)); + for (size_t w = 0; w < 0x20; w++) { + uint16_t card_id = this->npc_decks[z].card_ids[w]; + shared_ptr entry; + if (data_index) { + try { + entry = data_index->definition_for_card_id(card_id); + } catch (const out_of_range&) { } + } + if (entry) { + string name = entry->def.en_name; + lines.emplace_back(string_printf(" cards[%02zu]: %04hX (%s)", w, card_id, name.c_str())); + } else { + lines.emplace_back(string_printf(" cards[%02zu]: %04hX", w, card_id)); + } + } + for (size_t x = 0; x < 0x10; x++) { + lines.emplace_back(string_printf(" npc_dialogue[%zu][%zu]:", z, x)); + lines.emplace_back(string_printf(" a1=%04hX", this->dialogue_sets[z][x].unknown_a1.load())); + lines.emplace_back(string_printf(" a2=%04hX", this->dialogue_sets[z][x].unknown_a2.load())); + for (size_t w = 0; w < 4; w++) { + if (this->dialogue_sets[z][x].strings[w][0] != 0 && + static_cast(this->dialogue_sets[z][x].strings[w][0]) != 0xFF) { + lines.emplace_back(string_printf(" strings[%zu]=", w) + string(this->dialogue_sets[z][x].strings[w])); + } + } + } + } + lines.emplace_back(" a8=" + format_data_string(this->unknown_a8.data(), this->unknown_a8.bytes())); + if (this->before_message[0]) { + lines.emplace_back(" before_message: " + string(this->before_message)); + } + if (this->after_message[0]) { + lines.emplace_back(" after_message: " + string(this->after_message)); + } + if (this->dispatch_message[0]) { + lines.emplace_back(" dispatch_message: " + string(this->dispatch_message)); + } + for (size_t z = 0; z < 0x10; z++) { + uint16_t card_id = this->reward_card_ids[z]; + shared_ptr entry; + if (data_index) { + try { + entry = data_index->definition_for_card_id(card_id); + } catch (const out_of_range&) { } + } + if (entry) { + string name = entry->def.en_name; + lines.emplace_back(string_printf(" reward_cards[%02zu]: %04hX (%s)", z, card_id, name.c_str())); + } else { + lines.emplace_back(string_printf(" reward_cards[%02zu]: %04hX", z, card_id)); + } + } + 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())); + return join(lines, "\n"); +} + + + +bool Rules::check_invalid_fields() const { + Rules t = *this; + return t.check_and_reset_invalid_fields(); +} + +bool Rules::check_and_reset_invalid_fields() { + bool ret = false; + if (this->overall_time_limit > 36) { + this->overall_time_limit = 6; + ret = true; + } + if (this->phase_time_limit > 120) { + this->phase_time_limit = 60; + ret = true; + } + if (static_cast(this->allowed_cards) > 3) { + this->allowed_cards = AllowedCards::ALL; + ret = true; + } + if (this->min_dice > 9) { + this->min_dice = 0; + ret = true; + } + if (this->max_dice > 9) { + this->max_dice = 0; + ret = true; + } + if ((this->min_dice != 0) && (this->max_dice != 0) && (this->max_dice < this->min_dice)) { + uint8_t t = this->min_dice; + this->min_dice = this->max_dice; + this->max_dice = t; + ret = true; + } + if (this->disable_deck_shuffle > 1) { + this->disable_deck_shuffle = 0; + ret = true; + } + if (this->disable_deck_loop > 1) { + this->disable_deck_loop = 0; + ret = true; + } + if (this->char_hp > 99) { + this->char_hp = 0; + ret = true; + } + if (static_cast(this->hp_type) > 2) { + this->hp_type = HPType::DEFEAT_PLAYER; + ret = true; + } + if (this->no_assist_cards > 1) { + this->no_assist_cards = 0; + ret = true; + } + if (static_cast(this->dice_exchange_mode) > 2) { + this->dice_exchange_mode = DiceExchangeMode::HIGH_ATK; + ret = true; + } + if (this->disable_dice_boost > 1) { + this->disable_dice_boost = 0; + ret = true; + } + if ((this->max_dice != 0) && (this->max_dice < 3)) { + this->disable_dice_boost = 1; + ret = true; + } + return ret; +} + + + +DataIndex::DataIndex(const string& directory, bool debug) + : debug(debug) { + + unordered_map> card_tags; + unordered_map card_text; + if (this->debug) { + try { + string data = prs_decompress(load_file(directory + "/cardtext.mnr")); + StringReader r(data); + + while (!r.eof()) { + uint32_t card_id = stoul(r.get_cstr()); + + // Read all pages for this card + string text; + string first_page; + for (;;) { + string line = r.get_cstr(); + if (line.empty()) { + break; + } + if (first_page.empty()) { + first_page = line; + } + text += '\n'; + text += line; + } + + // In orig_text, turn all \t into $ (following newserv conventions) + string orig_text = text; + for (char& ch : orig_text) { + if (ch == '\t') { + ch = '$'; + } + } + + // Preprocess first page: first, delete all color markers + size_t offset = first_page.find("\tC"); + while (offset != string::npos) { + first_page = first_page.substr(0, offset) + first_page.substr(offset + 3); + offset = first_page.find("\tC"); + } + // Preprocess first page: delete all lines that don't start with \t + offset = first_page.find('\t'); + if (offset == string::npos) { + first_page.clear(); + } else { + first_page = first_page.substr(offset); + } + // Preprocess first page: merge lines that don't begin with \t + for (offset = 0; offset < first_page.size(); offset++) { + if (first_page[offset] == '\n' && first_page[offset + 1] != '\t') { + first_page = first_page.substr(0, offset) + first_page.substr(offset + 1); + offset--; + } + } + + // Split first page into tags, and collapse whitespace in the tag names + vector tags; + auto lines = split(first_page, '\n'); + for (const auto& line : lines) { + string tag; + if (line[0] == '\t' && line[1] == 'D') { + tag = "D: " + line.substr(2); + } else if (line[0] == '\t' && line[1] == 'S') { + tag = "S: " + line.substr(2); + } + if (!tag.empty()) { + for (size_t offset = tag.find(" "); offset != string::npos; offset = tag.find(" ")) { + tag = tag.substr(0, offset) + tag.substr(offset + 1); + } + tags.emplace_back(move(tag)); + } + } + + if (!card_text.emplace(card_id, move(orig_text)).second) { + throw runtime_error("duplicate card text id"); + } + if (!card_tags.emplace(card_id, move(tags)).second) { + throw logic_error("duplicate card tags id"); + } + + r.go((r.where() + 0x3FF) & (~0x3FF)); + } + + } catch (const exception& e) { + static_game_data_log.warning("Failed to load card text: %s", e.what()); + } + } + + try { + this->compressed_card_definitions = load_file(directory + "/cardupdate.mnr"); + string data = prs_decompress(this->compressed_card_definitions); + // There's a footer after the card definitions, but we ignore it + if (data.size() % sizeof(CardDefinition) != sizeof(CardDefinitionsFooter)) { + throw runtime_error(string_printf( + "decompressed card update file size %zX is not aligned with card definition size %zX (%zX extra bytes)", + data.size(), sizeof(CardDefinition), data.size() % sizeof(CardDefinition))); + } + const auto* def = reinterpret_cast(data.data()); + size_t max_cards = data.size() / sizeof(CardDefinition); + for (size_t x = 0; x < max_cards; x++) { + // The last card entry has the build date and some other metadata (and + // isn't a real card, obviously), so skip it. Seems like the card ID is + // always a large number that won't fit in a uint16_t, so we use that to + // determine if the entry is a real card or not. + if (def[x].card_id & 0xFFFF0000) { + continue; + } + shared_ptr entry(new CardEntry({def[x], {}, {}})); + if (!this->card_definitions.emplace(entry->def.card_id, entry).second) { + throw runtime_error(string_printf( + "duplicate card id: %08" PRIX32, entry->def.card_id.load())); + } + + entry->def.hp.decode_code(); + entry->def.ap.decode_code(); + entry->def.tp.decode_code(); + entry->def.mv.decode_code(); + entry->def.decode_range(); + + if (this->debug) { + try { + entry->text = move(card_text.at(def[x].card_id)); + } catch (const out_of_range&) { } + try { + entry->debug_tags = move(card_tags.at(def[x].card_id)); + } catch (const out_of_range&) { } + } + } + + static_game_data_log.info("Indexed %zu Episode 3 card definitions", this->card_definitions.size()); + } catch (const exception& e) { + 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 entry; + + if (ends_with(filename, ".mnmd")) { + entry.reset(new MapEntry(load_object_file(directory + "/" + filename))); + } else if (ends_with(filename, ".mnm")) { + entry.reset(new MapEntry(load_file(directory + "/" + filename))); + } + + if (entry.get()) { + if (!this->maps.emplace(entry->map.map_number, entry).second) { + throw runtime_error("duplicate map number"); + } + string name = entry->map.name; + 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()); + } + } +} + +DataIndex::MapEntry::MapEntry(const MapDefinition& map) : map(map) { } + +DataIndex::MapEntry::MapEntry(const string& compressed) + : compressed_data(compressed) { + string decompressed = prs_decompress(this->compressed_data); + if (decompressed.size() != sizeof(MapDefinition)) { + throw runtime_error(string_printf( + "decompressed data size is incorrect (expected %zu bytes, read %zu bytes)", + sizeof(MapDefinition), decompressed.size())); + } + this->map = *reinterpret_cast(decompressed.data()); +} + +string DataIndex::MapEntry::compressed() const { + if (this->compressed_data.empty()) { + this->compressed_data = prs_compress(&this->map, sizeof(this->map)); + } + return this->compressed_data; +} + +const string& DataIndex::get_compressed_card_definitions() const { + if (this->compressed_card_definitions.empty()) { + throw runtime_error("card definitions are not available"); + } + return this->compressed_card_definitions; +} + +shared_ptr DataIndex::definition_for_card_id( + uint32_t id) const { + return this->card_definitions.at(id); +} + +set DataIndex::all_card_ids() const { + set ret; + for (const auto& it : this->card_definitions) { + ret.emplace(it.first); + } + return ret; +} + +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 + // similar) so we can eliminate all this string copying here. + StringWriter entries_w; + StringWriter strings_w; + + for (const auto& map_it : this->maps) { + MapList::Entry e; + const auto& map = map_it.second->map; + e.map_x = map.map_x; + e.map_y = map.map_y; + e.scene_data2 = map.scene_data2; + e.map_number = map.map_number.load(); + e.width = map.width; + e.height = map.height; + e.map_tiles = map.map_tiles; + e.modification_tiles = map.modification_tiles; + + e.name_offset = strings_w.size(); + strings_w.write(map.name.data(), map.name.len()); + strings_w.put_u8(0); + e.location_name_offset = strings_w.size(); + strings_w.write(map.location_name.data(), map.location_name.len()); + strings_w.put_u8(0); + e.quest_name_offset = strings_w.size(); + strings_w.write(map.quest_name.data(), map.quest_name.len()); + strings_w.put_u8(0); + e.description_offset = strings_w.size(); + strings_w.write(map.description.data(), map.description.len()); + strings_w.put_u8(0); + + e.unknown_a2 = 0xFF000000; + + entries_w.put(e); + } + + MapList header; + header.num_maps = this->maps.size(); + header.unknown_a1 = 0; + header.strings_offset = entries_w.size(); + header.total_size = sizeof(MapList) + entries_w.size() + strings_w.size(); + + PRSCompressor prs; + prs.add(&header, sizeof(header)); + prs.add(entries_w.str()); + prs.add(strings_w.str()); + + StringWriter compressed_w; + 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()); + } + return this->compressed_map_list; +} + +shared_ptr DataIndex::definition_for_map_number(uint32_t id) const { + return this->maps.at(id); +} + +set DataIndex::all_map_ids() const { + set ret; + for (const auto& it : this->maps) { + ret.emplace(it.first); + } + return ret; +} + + + +} // namespace Episode3 diff --git a/src/Episode3/DataIndex.hh b/src/Episode3/DataIndex.hh new file mode 100644 index 00000000..92167337 --- /dev/null +++ b/src/Episode3/DataIndex.hh @@ -0,0 +1,815 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "../Text.hh" + +namespace Episode3 { + + + +// The comment in Server.hh does not apply to this file (and DataIndex.cc). +// Except for the Location structure, these structures and functions are not +// based on Sega's original implementation. + +class DataIndex; + + + +enum class StatSwapType : uint8_t { + NONE = 0, + A_T_SWAP = 1, + A_H_SWAP = 2, +}; + +enum class ActionType : uint8_t { + INVALID_00 = 0, + DEFENSE = 1, + ATTACK = 2, +}; + +enum class AttackMedium : uint8_t { + UNKNOWN = 0, + PHYSICAL = 1, + TECH = 2, + UNKNOWN_03 = 3, // Probably Resta + INVALID_FF = 0xFF, +}; + +enum class CriterionCode : uint8_t { + NONE = 0x00, + HU_CLASS_SC = 0x01, + RA_CLASS_SC = 0x02, + FO_CLASS_SC = 0x03, + SAME_TEAM = 0x04, + SAME_PLAYER = 0x05, + SAME_TEAM_NOT_SAME_PLAYER = 0x06, // Allies only + UNKNOWN_07 = 0x07, + NOT_SC = 0x08, + SC = 0x09, + HU_OR_RA_CLASS_SC = 0x0A, + HUNTER_HUMAN_SC = 0x0B, + HUNTER_HU_CLASS_MALE_SC = 0x0C, + HUNTER_FEMALE_SC = 0x0D, + HUNTER_HU_OR_FO_CLASS_HUMAN_SC = 0x0E, + HUNTER_HU_CLASS_ANDROID_SC = 0x0F, + UNKNOWN_10 = 0x10, + UNKNOWN_11 = 0x11, + HUNTER_HUNEWEARL_CLASS_SC = 0x12, + HUNTER_RA_CLASS_MALE_SC = 0x13, + HUNTER_RA_CLASS_FEMALE_SC = 0x14, + HUNTER_RA_OR_FO_CLASS_FEMALE_SC = 0x15, + HUNTER_HU_OR_RA_CLASS_HUMAN_SC = 0x16, + HUNTER_RA_CLASS_ANDROID_SC = 0x17, + HUNTER_FO_CLASS_FEMALE_SC = 0x18, + HUNTER_FEMALE_HUMAN_SC = 0x19, + HUNTER_ANDROID_SC = 0x1A, + HU_OR_FO_CLASS_SC = 0x1B, + RA_OR_FO_CLASS_SC = 0x1C, + PHYSICAL_OR_UNKNOWN_ATTACK_MEDIUM = 0x1D, + TECH_OR_UNKNOWN_ATTACK_MEDIUM = 0x1E, + PHYSICAL_OR_TECH_OR_UNKNOWN_ATTACK_MEDIUM = 0x1F, + UNKNOWN_20 = 0x20, + UNKNOWN_21 = 0x21, + UNKNOWN_22 = 0x22, +}; + +enum class CardRarity : uint8_t { + N1 = 0x01, + R1 = 0x02, + S = 0x03, + E = 0x04, + N2 = 0x05, + N3 = 0x06, + N4 = 0x07, + R2 = 0x08, + R3 = 0x09, + R4 = 0x0A, + SS = 0x0B, + D1 = 0x0C, + D2 = 0x0D, + INVIS = 0x0E, +}; + +enum class CardType : uint8_t { + HUNTERS_SC = 0x00, + ARKZ_SC = 0x01, + ITEM = 0x02, + CREATURE = 0x03, + ACTION = 0x04, + ASSIST = 0x05, + INVALID_FF = 0xFF, + END_CARD_LIST = 0xFF, +}; + +enum class CardClass : uint16_t { + HU_SC = 0x0000, + RA_SC = 0x0001, + FO_SC = 0x0002, + NATIVE_CREATURE = 0x000A, + A_BEAST_CREATURE = 0x000B, + MACHINE_CREATURE = 0x000C, + DARK_CREATURE = 0x000D, + GUARD_ITEM = 0x0015, + MAG_ITEM = 0x0017, + SWORD_ITEM = 0x0018, + GUN_ITEM = 0x0019, + CANE_ITEM = 0x001A, + ATTACK_ACTION = 0x001E, + DEFENSE_ACTION = 0x001F, + TECH = 0x0020, + PHOTON_BLAST = 0x0021, + CONNECT_ONLY_ATTACK_ACTION = 0x0022, + BOSS_ATTACK_ACTION = 0x0023, + BOSS_TECH = 0x0024, + ASSIST = 0x0028, +}; + +bool card_class_is_tech_like(CardClass cc); + +enum class TargetMode : uint8_t { + NONE = 0x00, // Used for defense cards, mags, shields, etc. + SINGLE_RANGE = 0x01, + MULTI_RANGE = 0x02, + SELF = 0x03, + TEAM = 0x04, + EVERYONE = 0x05, + MULTI_RANGE_ALLIES = 0x06, // e.g. Shifta + ALL_ALLIES = 0x07, // e.g. Anti, Resta, Leilla + ALL = 0x08, // e.g. Last Judgment, Earthquake + OWN_FCS = 0x09, // e.g. Traitor +}; + +enum class ConditionType : uint8_t { + NONE = 0x00, + AP_BOOST = 0x01, // Temporarily increase AP by N + RAMPAGE = 0x02, + MULTI_STRIKE = 0x03, // Duplicate attack N times + DAMAGE_MOD_1 = 0x04, // Set attack damage / AP to N after action cards applied (step 1) + IMMOBILE = 0x05, // Give Immobile condition + HOLD = 0x06, // Give Hold condition + UNKNOWN_07 = 0x07, + TP_BOOST = 0x08, // Add N TP temporarily during attack + GIVE_DAMAGE = 0x09, // Cause direct N HP loss + GUOM = 0x0A, // Give Guom condition + PARALYZE = 0x0B, // Give Paralysis condition + UNKNOWN_0C = 0x0C, // Swap AP and TP temporarily (presumably) + A_H_SWAP = 0x0D, // Swap AP and HP temporarily + PIERCE = 0x0E, // Attack SC directly even if they have items equipped + UNKNOWN_0F = 0x0F, + HEAL = 0x10, // Increase HP by N + RETURN_TO_HAND = 0x11, // Return card to hand + UNKNOWN_12 = 0x12, + UNKNOWN_13 = 0x13, + ACID = 0x14, // Give Acid condition + UNKNOWN_15 = 0x15, + MIGHTY_KNUCKLE = 0x16, // Temporarily increase AP by N, and set ATK dice to zero + UNIT_BLOW = 0x17, // Temporarily increase AP by N * number of this card set within phase + CURSE = 0x18, // Give Curse condition + COMBO_AP = 0x19, // Temporarily increase AP by number of this card set within phase + PIERCE_RAMPAGE_BLOCK = 0x1A, // Block attack if Pierce/Rampage + ABILITY_TRAP = 0x1B, // Temporarily disable opponent abilities + FREEZE = 0x1C, // Give Freeze condition + ANTI_ABNORMALITY_1 = 0x1D, // Cure all abnormal conditions + UNKNOWN_1E = 0x1E, + EXPLOSION = 0x1F, // Damage all SCs and FCs by number of this same card set * 2 + UNKNOWN_20 = 0x20, + UNKNOWN_21 = 0x21, + UNKNOWN_22 = 0x22, + RETURN_TO_DECK = 0x23, // Cancel discard and move to bottom of deck instead + AERIAL = 0x24, // Give Aerial status + AP_LOSS = 0x25, // Make attacker temporarily lose N AP during defense + BONUS_FROM_LEADER = 0x26, // Gain AP equal to the number of cards of type N on the field + FREE_MANEUVER = 0x27, // Enable movement over occupied tiles + HASTE = 0x28, // Multiply all move action costs by expr (which may be zero) + CLONE = 0x29, // Make setting this card free if at least one card of type N is already on the field + DEF_DISABLE_BY_COST = 0x2A, // Disable use of any defense cards costing between (N / 10) and (N % 10) points, inclusive + FILIAL = 0x2B, // Increase controlling SC's HP by N when this card is destroyed + SNATCH = 0x2C, // Steal N EXP during attack + HAND_DISRUPTER = 0x2D, // Discard N cards from hand immediately + DROP = 0x2E, // Give Drop condition + ACTION_DISRUPTER = 0x2F, // Destroy all action cards used by attacker + SET_HP = 0x30, // Set HP to N + NATIVE_SHIELD = 0x31, // Block attacks from Native creatures + A_BEAST_SHIELD = 0x32, // Block attacks from A.Beast creatures + MACHINE_SHIELD = 0x33, // Block attacks from Machine creatures + DARK_SHIELD = 0x34, // Block attacks from Dark creatures + SWORD_SHIELD = 0x35, // Block attacks from Sword items + GUN_SHIELD = 0x36, // Block attacks from Gun items + CANE_SHIELD = 0x37, // Block attacks from Cane items + UNKNOWN_38 = 0x38, + UNKNOWN_39 = 0x39, + DEFENDER = 0x3A, // Make attacks go to setter of this card instead of original target + SURVIVAL_DECOYS = 0x3B, // Redirect damage for multi-sided attack + GIVE_OR_TAKE_EXP = 0x3C, // Give N EXP, or take if N is negative + UNKNOWN_3D = 0x3D, + DEATH_COMPANION = 0x3E, // If this card has 1 or 2 HP, set its HP to N + EXP_DECOY = 0x3F, // If defender has EXP, lose EXP instead of getting damage when attacked + SET_MV = 0x40, // Set MV to N + GROUP = 0x41, // Temporarily increase AP by N * number of this card on field, excluding itself + BERSERK = 0x42, // User of this card receives the same damage as target, and isn't helped by target's defense cards + GUARD_CREATURE = 0x43, // Attacks on controlling SC damage this card instead + TECH = 0x44, // Technique cards cost 1 fewer ATK point + BIG_SWING = 0x45, // Increase all attacking ATK costs by 1 + UNKNOWN_46 = 0x46, + SHIELD_WEAPON = 0x47, // Limit attacker's choice of target to guard items + ATK_DICE_BOOST = 0x48, // Increase ATK dice roll by 1 + UNKNOWN_49 = 0x49, + MAJOR_PIERCE = 0x4A, // If SC has over half of max HP, attacks target SC instead of equipped items + HEAVY_PIERCE = 0x4B, // If SC has 3 or more items equipped, attacks target SC instead of equipped items + MAJOR_RAMPAGE = 0x4C, // If SC has over half of max HP, attacks target SC and all equipped items + HEAVY_RAMPAGE = 0x4D, // If SC has 3 or more items equipped, attacks target SC and all equipped items + AP_GROWTH = 0x4E, // Permanently increase AP by N + TP_GROWTH = 0x4F, // Permanently increase TP by N + REBORN = 0x50, // If any card of type N is on the field, this card goes to the hand when destroyed instead of being discarded + COPY = 0x51, // Temporarily set AP/TP to N percent (or 100% if N is 0) of opponent's values + UNKNOWN_52 = 0x52, + MISC_GUARDS = 0x53, // Add N to card's defense value + AP_OVERRIDE = 0x54, // Set AP to N temporarily + TP_OVERRIDE = 0x55, // Set TP to N temporarily + RETURN = 0x56, // Return card to hand on destruction instead of discarding + A_T_SWAP_PERM = 0x57, // Permanently swap AP and TP + A_H_SWAP_PERM = 0x58, // Permanently swap AP and HP + SLAYERS_ASSASSINS = 0x59, // Temporarily increase AP during attack + ANTI_ABNORMALITY_2 = 0x5A, // Remove all conditions + FIXED_RANGE = 0x5B, // Use SC's range instead of weapon or attack card ranges + ELUDE = 0x5C, // SC does not lose HP when equipped items are destroyed + PARRY = 0x5D, // Forward attack to a random FC within one tile of original target, excluding attacker and original target + BLOCK_ATTACK = 0x5E, // Completely block attack + UNKNOWN_5F = 0x5F, + UNKNOWN_60 = 0x60, + COMBO_TP = 0x61, // Gain TP equal to the number of cards of type N on the field + MISC_AP_BONUSES = 0x62, // Temporarily increase AP by N + MISC_TP_BONUSES = 0x63, // Temporarily increase TP by N + UNKNOWN_64 = 0x64, + MISC_DEFENSE_BONUSES = 0x65, // Decrease damage by N + MOSTLY_HALFGUARDS = 0x66, // Reduce damage from incoming attack by N + PERIODIC_FIELD = 0x67, // Swap immunity to tech or physical attacks + FC_LIMIT_BY_COUNT = 0x68, // Change FC limit from 8 ATK points total to 4 FCs total + UNKNOWN_69 = 0x69, + MV_BONUS = 0x6A, // Increase MV by N + FORWARD_DAMAGE = 0x6B, + WEAK_SPOT_INFLUENCE = 0x6C, // Temporarily decrease AP by N + DAMAGE_MODIFIER_2 = 0x6D, // Set attack damage / AP after action cards applied (step 2) + WEAK_HIT_BLOCK = 0x6E, // Block all attacks of N damage or less + AP_SILENCE = 0x6F, // Temporarily decrease AP of opponent by N + TP_SILENCE = 0x70, // Temporarily decrease TP of opponent by N + A_T_SWAP = 0x71, // Temporarily swap AP and TP + HALFGUARD = 0x72, // Halve damage from attacks that would inflict N or more damage + UNKNOWN_73 = 0x73, + RAMPAGE_AP_LOSS = 0x74, // Temporarily reduce AP by N + UNKNOWN_75 = 0x75, + REFLECT = 0x76, // Generate reverse attack + UNKNOWN_77 = 0x77, + ANY = 0x78, // Not a real condition; used as a wildcard in search functions + UNKNOWN_79 = 0x79, + UNKNOWN_7A = 0x7A, + UNKNOWN_7B = 0x7B, + UNKNOWN_7C = 0x7C, + UNKNOWN_7D = 0x7D, + INVALID_FF = 0xFF, + ANY_FF = 0xFF, // Used as a wildcard in some search functions +}; + +enum class AssistEffect : uint16_t { + NONE = 0x0000, + DICE_HALF = 0x0001, + DICE_PLUS_1 = 0x0002, + DICE_FEVER = 0x0003, + CARD_RETURN = 0x0004, + LAND_PRICE = 0x0005, + POWERLESS_RAIN = 0x0006, + BRAVE_WIND = 0x0007, + SILENT_COLOSSEUM = 0x0008, + RESISTANCE = 0x0009, + INDEPENDENT = 0x000A, + ASSISTLESS = 0x000B, + ATK_DICE_2 = 0x000C, + DEFLATION = 0x000D, + INFLATION = 0x000E, + EXCHANGE = 0x000F, + INFLUENCE = 0x0010, + SKIP_SET = 0x0011, + SKIP_MOVE = 0x0012, + SKIP_ACT = 0x0013, + SKIP_DRAW = 0x0014, + FLY = 0x0015, + NECROMANCER = 0x0016, + PERMISSION = 0x0017, + SHUFFLE_ALL = 0x0018, + LEGACY = 0x0019, + ASSIST_REVERSE = 0x001A, + STAMINA = 0x001B, + AP_ABSORPTION = 0x001C, + HEAVY_FOG = 0x001D, + TRASH_1 = 0x001E, + EMPTY_HAND = 0x001F, + HITMAN = 0x0020, + ASSIST_TRASH = 0x0021, + SHUFFLE_GROUP = 0x0022, + ASSIST_VANISH = 0x0023, + CHARITY = 0x0024, + INHERITANCE = 0x0025, + FIX = 0x0026, + MUSCULAR = 0x0027, + CHANGE_BODY = 0x0028, + GOD_WHIM = 0x0029, + GOLD_RUSH = 0x002A, + ASSIST_RETURN = 0x002B, + REQUIEM = 0x002C, + RANSOM = 0x002D, + SIMPLE = 0x002E, + SLOW_TIME = 0x002F, + QUICK_TIME = 0x0030, + TERRITORY = 0x0031, + OLD_TYPE = 0x0032, + FLATLAND = 0x0033, + IMMORTALITY = 0x0034, + SNAIL_PACE = 0x0035, + TECH_FIELD = 0x0036, + FOREST_RAIN = 0x0037, + CAVE_WIND = 0x0038, + MINE_BRIGHTNESS = 0x0039, + RUIN_DARKNESS = 0x003A, + SABER_DANCE = 0x003B, + BULLET_STORM = 0x003C, + CANE_PALACE = 0x003D, + GIANT_GARDEN = 0x003E, + MARCH_OF_THE_MEEK = 0x003F, + SUPPORT = 0x0040, + RICH = 0x0041, + REVERSE_CARD = 0x0042, + VENGEANCE = 0x0043, + SQUEEZE = 0x0044, + HOMESICK = 0x0045, + BOMB = 0x0046, + SKIP_TURN = 0x0047, + BATTLE_ROYALE = 0x0048, + DICE_FEVER_PLUS = 0x0049, + RICH_PLUS = 0x004A, + CHARITY_PLUS = 0x004B, + ANY = 0x004C, // Unused on cards; used in some search functions +}; + +enum class BattlePhase : uint8_t { + INVALID_00 = 0, + DICE = 1, + SET = 2, + MOVE = 3, + ACTION = 4, + DRAW = 5, + INVALID_FF = 0xFF, +}; + +enum class ActionSubphase : uint8_t { + ATTACK = 0, + DEFENSE = 2, + INVALID_FF = 0xFF, +}; + +enum class SetupPhase : uint8_t { + REGISTRATION = 0, + STARTER_ROLLS = 1, + HAND_REDRAW_OPTION = 2, + MAIN_BATTLE = 3, + BATTLE_ENDED = 4, + INVALID_FF = 0xFF, +}; + +enum class RegistrationPhase : uint8_t { + AWAITING_NUM_PLAYERS = 0, // num_players not set yet + AWAITING_PLAYERS = 1, // num_players set, but some players not registered + AWAITING_DECKS = 2, // all players registered, but some decks missing + REGISTERED = 3, // All players/decks present, but battle not started yet + BATTLE_STARTED = 4, + INVALID_FF = 0xFF, +}; + + + +enum class Direction : uint8_t { + RIGHT = 0, + UP = 1, + LEFT = 2, + DOWN = 3, + INVALID_FF = 0xFF, +}; + +Direction turn_left(Direction d); +Direction turn_right(Direction d); +Direction turn_around(Direction d); +const char* name_for_direction(Direction d); + +struct Location { + uint8_t x; + uint8_t y; + Direction direction; + uint8_t unused; + + Location(); + Location(uint8_t x, uint8_t y); + Location(uint8_t x, uint8_t y, Direction direction); + bool operator==(const Location& other) const = default; + bool operator!=(const Location& other) const = default; + + void clear(); + void clear_FF(); +} __attribute__((packed)); + +struct CardDefinition { + struct Stat { + enum Type : uint8_t { + BLANK = 0, + STAT = 1, + PLUS_STAT = 2, + MINUS_STAT = 3, + EQUALS_STAT = 4, + UNKNOWN = 5, + PLUS_UNKNOWN = 6, + MINUS_UNKNOWN = 7, + EQUALS_UNKNOWN = 8, + }; + be_uint16_t code; + Type type; + int8_t stat; + + void decode_code(); + std::string str() const; + } __attribute__((packed)); + + struct Effect { + uint8_t effect_num; + ConditionType type; + ptext expr; // May be blank if the condition type doesn't use it + uint8_t when; + ptext arg1; + ptext arg2; + ptext arg3; + CriterionCode apply_criterion; + uint8_t unknown_a2; + + bool is_empty() const; + static std::string str_for_arg(const std::string& arg); + std::string str() const; + } __attribute__((packed)); + + be_uint32_t card_id; + parray jp_name; + CardType type; // Type enum. If <0, then this is the end of the card list + uint8_t self_cost; // ATK dice points required + uint8_t ally_cost; // ATK points from allies required; PBs use this + uint8_t unused1; + Stat hp; + Stat ap; + Stat tp; + Stat mv; + parray left_colors; + parray right_colors; + parray top_colors; + parray range; + be_uint32_t unused2; + TargetMode target_mode; + uint8_t assist_turns; // 90 (dec) = once, 99 (dec) = forever + uint8_t cannot_move; // 0 for SC and creature cards; 1 for everything else + uint8_t cannot_attack; // 1 for shields, mags, defense actions, and assist cards + uint8_t unused3; + uint8_t hide_in_deck_edit; // 0 = player can use this card (appears in deck edit) + CriterionCode usable_criterion; + CardRarity rarity; + be_uint16_t unknown_a2; + be_uint16_t be_card_class; // Used for checking attributes (e.g. item types) + // These two fields seem to always contain the same value, and are always 0 + // for non-assist cards and nonzero for assists. Each assist card has a unique + // value here and no effects, which makes it look like this is how assist + // effects are implemented. There seems to be some 1k-modulation going on here + // too; most cards are in the range 101-174 but a few have e.g. 1150, 2141. A + // few pairs of cards have the same effect, which makes it look like some + // other fields are also involved in determining their effects (see e.g. Skip + // Draw / Skip Move, Dice Fever / Dice Fever +, Reverse Card / Rich +). + parray assist_effect; + // Drop rates are decimal-encoded with the following fields: + // - rate % 10 (that is, the lowest decimal place) specifies the required game + // mode. 0 means any mode, 1 means offline only, 2 means 1P free-battle, 3 + // means 2P+ free battle, 4 means story mode. + // - (rate / 10) % 100 (that is, the tens and hundreds decimal places) specify + // something else, but it's not clear what exactly. + // - rate / 1000 (the thousands decimal place) specifies the level class + // required to get this drop. + // - rate / 10000 (the ten-thousands decimal place) must be either 0, 1, or 2, + // but it's not clear yet what each value means. + // The drop rates are completely ignored if any of the following are true + // (which means the card can never be found in a normal post-battle draw): + // - type is SC_HUNTERS or SC_ARKZ + // - unknown_a3 is 0x23 or 0x24 + // - rarity is E, D1, D2, or INVIS + // - hide_in_deck_edit is 1 (specifically 1; other nonzero values here don't + // prevent the card from appearing in post-battle draws) + parray drop_rates; + ptext en_name; + ptext jp_short_name; + ptext en_short_name; + Effect effects[3]; + uint8_t unused4; + + bool is_sc() const; + bool is_fc() const; + bool is_named_android_sc() const; + bool any_top_color_matches(const CardDefinition& other) const; + CardClass card_class() const; + + void decode_range(); + std::string str() const; +} __attribute__((packed)); // 0x128 bytes in total + +struct CardDefinitionsFooter { + be_uint32_t num_cards1; + be_uint32_t unknown_a1; + be_uint32_t num_cards2; + be_uint32_t unknown_a2[11]; + be_uint32_t unknown_offset_a3; + be_uint32_t unknown_a4[3]; + be_uint32_t footer_offset; + be_uint32_t unknown_a5[3]; +} __attribute__((packed)); + +struct DeckDefinition { + ptext name; + be_uint32_t client_id; // 0-3 + // List of card IDs. The card count is the number of nonzero entries here + // before a zero entry (or 50 if no entries are nonzero). The first card ID is + // the SC card, which the game implicitly subtracts from the limit - so a + // valid deck should actually have 31 cards in it. + parray card_ids; + be_uint32_t unknown_a1; + // Last modification time + le_uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + uint8_t unknown_a2; +} __attribute__((packed)); // 0x84 bytes in total + +struct PlayerConfig { + // Offsets in comments in this struct are relative to start of 61/98 command + /* 0728 */ parray unknown_a1; + /* 1B5C */ parray 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 unknown_a3; + /* 299C */ ptext name; + // Other records are probably somewhere in here - e.g. win/loss, play time, etc. + /* 29AC */ parray unknown_a4; +} __attribute__((packed)); + +enum class HPType : uint8_t { + DEFEAT_PLAYER = 0, + DEFEAT_TEAM = 1, + COMMON_HP = 2, +}; + +enum class DiceExchangeMode : uint8_t { + HIGH_ATK = 0, + HIGH_DEF = 1, + NONE = 2, +}; + +enum class AllowedCards : uint8_t { + ALL = 0, + N_ONLY = 1, + N_R_ONLY = 2, + N_R_S_ONLY = 3, +}; + +struct Rules { + // When this structure is used in a map/quest definition, FF in any of these + // fields means the user is allowed to override it. Any non-FF fields are + // fixed for the map/quest and cannot be overridden. + uint8_t overall_time_limit; // In increments of 5 minutes; 0 = unlimited + uint8_t phase_time_limit; // In seconds; 0 = unlimited + AllowedCards allowed_cards; + uint8_t min_dice; // 0 = default (1) + // 4 + uint8_t max_dice; // 0 = default (6) + uint8_t disable_deck_shuffle; // 0 = shuffle on, 1 = off + uint8_t disable_deck_loop; // 0 = loop on, 1 = off + uint8_t char_hp; + // 8 + HPType hp_type; + uint8_t no_assist_cards; // 1 = assist cards disallowed + uint8_t disable_dialogue; // 0 = dialogue on, 1 = dialogue off + DiceExchangeMode dice_exchange_mode; + // C + uint8_t disable_dice_boost; // 0 = dice boost on, 1 = off + parray unused; + + Rules(); + bool operator==(const Rules& other) const = default; + bool operator!=(const Rules& other) const = default; + void clear(); + + bool check_invalid_fields() const; + bool check_and_reset_invalid_fields(); + + std::string str() const; +} __attribute__((packed)); + +struct StateFlags { + le_uint16_t turn_num; + BattlePhase battle_phase; + uint8_t current_team_turn1; + uint8_t current_team_turn2; + ActionSubphase action_subphase; + SetupPhase setup_phase; + RegistrationPhase registration_phase; + parray team_exp; + parray team_dice_boost; + uint8_t first_team_turn; + uint8_t tournament_flag; + parray client_sc_card_types; + + StateFlags(); + bool operator==(const StateFlags& other) const = default; + bool operator!=(const StateFlags& other) const = default; + void clear(); + void clear_FF(); +} __attribute__((packed)); + + + +struct MapList { + be_uint32_t num_maps; + be_uint32_t unknown_a1; // Always 0? + be_uint32_t strings_offset; // From after total_size field (add 0x10 to this value) + be_uint32_t total_size; // Including header, entries, and strings + + struct Entry { // Should be 0x220 bytes in total + be_uint16_t map_x; + be_uint16_t map_y; + be_uint16_t scene_data2; + be_uint16_t map_number; + // Text offsets are from the beginning of the strings block after all map + // entries (that is, add strings_offset to them to get the string offset) + be_uint32_t name_offset; + be_uint32_t location_name_offset; + be_uint32_t quest_name_offset; + be_uint32_t description_offset; + be_uint16_t width; + be_uint16_t height; + parray, 0x10> map_tiles; + parray, 0x10> modification_tiles; + be_uint32_t unknown_a2; // Seems to always be 0xFF000000 + } __attribute__((packed)); + + // Variable-length fields: + // Entry entries[num_maps]; + // char strings[...EOF]; // Null-terminated strings, pointed to by offsets in Entry structs +} __attribute__((packed)); + +struct CompressedMapHeader { // .mnm file format + le_uint32_t map_number; + le_uint32_t compressed_data_size; + // Compressed data immediately follows (which decompresses to a MapDefinition) +} __attribute__((packed)); + +struct MapDefinition { // .mnmd format; also the format of (decompressed) quests + /* 0000 */ be_uint32_t unknown_a1; + /* 0004 */ be_uint32_t map_number; + /* 0008 */ uint8_t width; + /* 0009 */ uint8_t height; + /* 000A */ uint8_t scene_data2; // TODO: What is this? + // All alt_maps fields (including the floats) past num_alt_maps are filled in + // with FF. For example, if num_alt_maps == 8, the last two fields in each + // alt_maps array are filled with FF. + /* 000B */ uint8_t num_alt_maps; // TODO: What are the alt maps for? + // In the map_tiles array, the values are: + // 00 = not a valid tile + // 01 = valid tile unless punched out (later) + // 02 = team A start (1v1) + // 03, 04 = team A start (2v2) + // 05 = ??? + // 06, 07 = team B start (2v2) + // 08 = team B start (1v1) + // Note that the game displays the map reversed vertically in the preview + // window. For example, player 1 is on team A, which usually starts at the top + // of the map as defined in this struct, or at the bottom as shown in the + // preview window. + /* 000C */ parray, 0x10> map_tiles; + // The start_tile_definitions field is a list of 6 bytes for each team. The + // low 6 bits of each byte match the starting location for the relevant player + // in map_tiles; the high 2 bits are the player's initial facing direction. + // - If the team has 1 player, only byte [0] is used. + // - If the team has 2 players, bytes [1] and [2] are used. + // - If the team has 3 players, bytes [3] through [5] are used. + /* 010C */ parray, 2> start_tile_definitions; + /* 0118 */ parray, 0x10> alt_maps1[2][0x0A]; + /* 1518 */ parray alt_maps_unknown_a3[2][0x0A]; + /* 1AB8 */ parray 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) + // 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) + /* 1C68 */ parray, 0x10> modification_tiles; + /* 1D68 */ parray unknown_a6; + /* 1DDC */ Rules default_rules; + /* 1DEC */ parray unknown_a7; + /* 1DF0 */ ptext name; + /* 1E04 */ ptext location_name; + /* 1E18 */ ptext quest_name; // == location_name if not a quest + /* 1E54 */ ptext description; + /* 1FE4 */ be_uint16_t map_x; + /* 1FE6 */ be_uint16_t map_y; + struct NPCDeck { + ptext name; + parray card_ids; // Last one appears to always be FFFF + } __attribute__((packed)); + /* 1FE8 */ NPCDeck npc_decks[3]; // Unused if name[0] == 0 + struct NPCCharacter { + parray unknown_a1; + parray unknown_a2; + ptext name; + parray unknown_a3; + } __attribute__((packed)); + /* 20F0 */ NPCCharacter npc_chars[3]; // Unused if name[0] == 0 + /* 242C */ parray unknown_a8; // Always FF? + /* 2440 */ ptext before_message; + /* 25D0 */ ptext after_message; + /* 2760 */ ptext dispatch_message; // Usually "You can only dispatch " or blank + struct DialogueSet { + be_uint16_t unknown_a1; + be_uint16_t unknown_a2; // Always 0x0064 if valid, 0xFFFF if unused? + ptext strings[4]; + } __attribute__((packed)); // Total size: 0x104 bytes + /* 28F0 */ DialogueSet dialogue_sets[3][0x10]; // Up to 0x10 per valid NPC + /* 59B0 */ parray reward_card_ids; + /* 59D0 */ parray unknown_a9; + /* 59DC */ uint8_t unknown_a10; + /* 59DD */ parray unknown_a11; + /* 5A18 */ + + std::string str(const DataIndex* data_index = nullptr) const; +} __attribute__((packed)); + + + +class DataIndex { +public: + explicit DataIndex(const std::string& directory, bool debug = false); + + struct CardEntry { + CardDefinition def; + std::string text; + std::vector debug_tags; // Empty unless debug == true + }; + + class MapEntry { + public: + MapDefinition map; + + MapEntry(const MapDefinition& map); + MapEntry(const std::string& compressed_data); + + std::string compressed() const; + + private: + mutable std::string compressed_data; + }; + + const std::string& get_compressed_card_definitions() const; + std::shared_ptr definition_for_card_id(uint32_t id) const; + std::set all_card_ids() const; + + const std::string& get_compressed_map_list() const; + std::shared_ptr definition_for_map_number(uint32_t id) const; + std::set all_map_ids() const; + +private: + bool debug; + + std::string compressed_card_definitions; + std::unordered_map> 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 + // is not violated from the caller's perspective even if we don't generate the + // compressed map list at load time. + mutable std::string compressed_map_list; + std::map> maps; +}; + + + +} // namespace Episode3 diff --git a/src/Episode3/DeckState.cc b/src/Episode3/DeckState.cc new file mode 100644 index 00000000..2ccd4471 --- /dev/null +++ b/src/Episode3/DeckState.cc @@ -0,0 +1,286 @@ +#include "DeckState.hh" + +using namespace std; + +namespace Episode3 { + + + +NameEntry::NameEntry() { + this->clear(); +} + +void NameEntry::clear() { + this->client_id = 0xFF; + this->present = 0; + this->unused_by_server = 0; + this->unused = 0; +} + + + +DeckEntry::DeckEntry() { + this->clear(); +} + +void DeckEntry::clear() { + this->team_id = 0xFFFFFFFF; + this->god_whim_flag = 3; + this->unused1 = 0; + this->unused2 = 0; + this->unused3 = 0; + this->card_ids.clear(0xFFFF); +} + + + +uint8_t index_for_card_ref(uint16_t card_ref) { + return card_ref & 0xFF; +} + +uint8_t client_id_for_card_ref(uint16_t card_ref) { + return (card_ref >> 8) & 0xFF; +} + + + +uint8_t DeckState::num_drawable_cards() const { + return this->card_refs.size() - this->draw_index; +} + +bool DeckState::set_card_ref_in_play(uint16_t card_ref) { + if (!this->contains_card_ref(card_ref)) { + return false; + } + uint8_t index = index_for_card_ref(card_ref); + if (this->entries[index].state == CardState::IN_HAND) { + this->entries[index].state = CardState::IN_PLAY; + return true; + } else { + return false; + } +} + +bool DeckState::contains_card_ref(uint16_t card_ref) const { + return index_for_card_ref(card_ref) < this->entries.size(); +} + +void DeckState::disable_loop() { + this->loop_enabled = false; +} + +void DeckState::disable_shuffle() { + this->shuffle_enabled = false; +} + +uint16_t DeckState::draw_card() { + if (this->num_drawable_cards() == 0) { + this->restart(); + } + if (this->num_drawable_cards() == 0) { + return 0xFFFF; + } + + uint16_t ref = this->card_refs[this->draw_index++]; + this->entries[index_for_card_ref(ref)].state = CardState::IN_HAND; + return ref; +} + +bool DeckState::draw_card_by_ref(uint16_t card_ref) { + if (card_ref == 0xFFFF) { + return false; + } + + uint8_t index = index_for_card_ref(card_ref); + if (index > this->entries.size()) { + return false; + } + + // If the card is discarded, then it should be before the draw index, and we + // can just change its state. + if (this->entries[index].state == CardState::DISCARDED) { + this->entries[index].state = CardState::IN_HAND; + return true; + + // If the card is still drawable, we need to move it so it's just in front of + // the draw index, then immediately draw it + } else if (this->entries[index].state == CardState::DRAWABLE) { + ssize_t ref_index; + for (ref_index = this->card_refs.size(); ref_index >= 0; ref_index--) { + if (this->card_refs[ref_index] == card_ref) { + break; + } + } + if (ref_index < 0) { + return false; + } + + size_t ref_uindex = ref_index; + for (; ref_uindex > this->draw_index; ref_uindex--) { + // Note: draw_index is also unsigned, so ref_uindex cannot be zero here + this->card_refs[ref_uindex] = this->card_refs[ref_uindex - 1]; + } + this->card_refs[this->draw_index] = card_ref; + this->entries[index].state = CardState::IN_HAND; + this->draw_index++; + return true; + + } else { + return false; + } +} + +uint16_t DeckState::card_id_for_card_ref(uint16_t card_ref) const { + if (card_ref == 0xFFFF) { + return 0xFFFF; + } + + uint8_t index = index_for_card_ref(card_ref); + if (index < this->entries.size()) { + return this->entries[index].card_id; + } else { + return 0xFFFF; + } +} + +uint16_t DeckState::sc_card_id() const { + return this->entries[0].card_id; +} + +uint16_t DeckState::sc_card_ref() const { + return this->card_refs[0]; +} + +uint16_t DeckState::card_ref_for_index(uint8_t index) const { + return this->card_ref_base | index; +} + +DeckState::CardState DeckState::state_for_card_ref(uint16_t card_ref) const { + uint8_t index = index_for_card_ref(card_ref); + return (index < this->entries.size()) ? this->entries[index].state : CardState::INVALID; +} + +void DeckState::restart() { + // First, if deck loop is on, return all discarded cards to the drawable state + if (this->loop_enabled) { + for (size_t z = 0; z < this->entries.size(); z++) { + if (this->entries[z].state == CardState::DISCARDED) { + this->entries[z].state = CardState::DRAWABLE; + } + } + } + + // For any cards that are still in hand or still in play, move their refs to + // the already-drawn part of the deck + this->draw_index = 0; + for (size_t z = 0; z < this->entries.size(); z++) { + if (this->entries[z].state != CardState::DRAWABLE) { + this->card_refs[this->draw_index++] = this->card_ref_for_index(z); + } + } + + // For now-drawable cards, put their refs after the draw index + size_t index = this->draw_index; + for (size_t z = 0; z < this->entries.size(); z++) { + if (this->entries[z].state == CardState::DRAWABLE) { + this->card_refs[index++] = this->card_ref_for_index(z); + } + } + + this->shuffle(); +} + +void DeckState::do_mulligan() { + for (size_t z = 0; z < this->entries.size(); z++) { + if (this->entries[z].state == CardState::DISCARDED) { + this->entries[z].state = CardState::DRAWABLE; + } + } + this->draw_index = 1; + + if (this->shuffle_enabled) { + // Get the next 5 cards from the deck, and put the previous 5 cards after + // them (so they will be shuffled back in). + for (uint8_t z = 0; z < 5; z++) { + uint8_t index = z + this->draw_index; + uint16_t temp_ref = this->card_refs[index]; + this->card_refs[index] = this->card_refs[index + 5]; + this->card_refs[index + 5] = temp_ref; + } + + // Shuffle the deck, except the first 5 cards (which are about to be drawn). + size_t max = this->num_drawable_cards() - 5; + uint8_t base_index = this->draw_index + 5; + for (size_t z = 0; z < this->card_refs.size(); z++) { + uint8_t index1 = this->random_crypt->next() % max; + uint8_t index2 = this->random_crypt->next() % max; + uint16_t temp_ref = this->card_refs[base_index + index1]; + this->card_refs[base_index + index1] = this->card_refs[base_index + index2]; + this->card_refs[base_index + index2] = temp_ref; + } + } +} + +bool DeckState::set_card_ref_drawable_next(uint16_t card_ref) { + if (card_ref == 0xFFFF) { + return false; + } + if (client_id_for_card_ref(card_ref) != this->client_id) { + return false; + } + + uint8_t index = index_for_card_ref(card_ref); + if (this->entries[index].state == CardState::DRAWABLE) { + return false; + } else if (this->draw_index < 1) { + return false; + } else { + this->entries[index].state = CardState::DRAWABLE; + this->card_refs[--this->draw_index] = card_ref; + return true; + } +} + +bool DeckState::set_card_ref_drawable_at_end(uint16_t card_ref) { + if (this->set_card_ref_drawable_next(card_ref)) { + uint16_t head_card_ref = this->card_refs[this->draw_index]; + if (this->draw_index < this->card_refs.size() - 1) { + for (size_t z = this->draw_index; z < this->card_refs.size() - 1; z++) { + this->card_refs[z] = this->card_refs[z + 1]; + } + } + this->card_refs[this->card_refs.size() - 1] = head_card_ref; + return true; + } else { + return false; + } +} + +void DeckState::set_card_discarded(uint16_t card_ref) { + uint8_t index = index_for_card_ref(card_ref); + if (index < this->entries.size()) { + this->entries[index].state = CardState::DISCARDED; + } +} + +void DeckState::shuffle() { + if (this->shuffle_enabled) { + size_t max = this->num_drawable_cards(); + for (size_t z = 0; z < this->card_refs.size(); z++) { + // Note: This is the way Sega originally implemented shuffling - they just + // do N swaps on the entire array. A more uniform way to do it would be to + // instead swap each item with another random item (possibly itself) that + // doesn't appear earlier than it in the array, but this is not what Sega + // did. + uint8_t index1 = this->draw_index + this->random_crypt->next() % max; + uint8_t index2 = this->draw_index + this->random_crypt->next() % max; + uint16_t temp_ref = this->card_refs[index1]; + this->card_refs[index1] = this->card_refs[index2]; + this->card_refs[index2] = temp_ref; + } + } +} + + + +} // namespace Episode3 diff --git a/src/Episode3/DeckState.hh b/src/Episode3/DeckState.hh new file mode 100644 index 00000000..acd69909 --- /dev/null +++ b/src/Episode3/DeckState.hh @@ -0,0 +1,117 @@ +#pragma once + +#include + +#include + +#include "../Text.hh" +#include "../PSOEncryption.hh" + +namespace Episode3 { + + + +struct NameEntry { + parray name; + uint8_t client_id; + uint8_t present; + uint8_t unused_by_server; + uint8_t unused; + + NameEntry(); + void clear(); +} __attribute__((packed)); + +struct DeckEntry { + ptext name; + le_uint32_t team_id; + parray card_ids; + // If the following flag is not set to 3, then the God Whim assist effect can + // use cards that are hidden from the player during deck building. The client + // always sets this to 3, and it's not clear why this even exists. + uint8_t god_whim_flag; + uint8_t unused1; + le_uint16_t unused2; + be_uint16_t unused3; + + DeckEntry(); + void clear(); +} __attribute__((packed)); + +uint8_t index_for_card_ref(uint16_t card_ref); +uint8_t client_id_for_card_ref(uint16_t card_ref); + +class DeckState { +public: + enum class CardState { + DRAWABLE = 0, + STORY_CHARACTER = 1, + IN_HAND = 2, + IN_PLAY = 3, + DISCARDED = 4, + INVALID = 5, + }; + + template + DeckState( + uint8_t client_id, + const parray& card_ids, + std::shared_ptr random_crypt) + : client_id(client_id), + draw_index(1), + card_ref_base(this->client_id << 8), + shuffle_enabled(true), + loop_enabled(true), + random_crypt(random_crypt) { + for (size_t z = 0; z < card_ids.size(); z++) { + auto& e = this->entries[z]; + e.card_id = card_ids[z]; + e.deck_index = z; + e.state = CardState::DRAWABLE; + this->card_refs[z] = this->card_ref_for_index(z); + } + this->entries[0].state = CardState::STORY_CHARACTER; + } + + void disable_loop(); + void disable_shuffle(); + + uint8_t num_drawable_cards() const; + bool contains_card_ref(uint16_t card_ref) const; + uint16_t card_id_for_card_ref(uint16_t card_ref) const; + uint16_t sc_card_id() const; + uint16_t sc_card_ref() const; + uint16_t card_ref_for_index(uint8_t index) const; + CardState state_for_card_ref(uint16_t card_ref) const; + + uint16_t draw_card(); + bool draw_card_by_ref(uint16_t card_ref); + bool set_card_ref_in_play(uint16_t card_ref); + bool set_card_ref_drawable_next(uint16_t card_ref); + bool set_card_ref_drawable_at_end(uint16_t card_ref); + void set_card_discarded(uint16_t card_ref); + + void restart(); + void shuffle(); + void do_mulligan(); + +private: + struct CardEntry { + uint16_t card_id; + uint8_t deck_index; + CardState state; + }; + uint8_t client_id; + uint8_t draw_index; + uint16_t card_ref_base; + bool shuffle_enabled; + bool loop_enabled; + parray entries; + parray card_refs; + + std::shared_ptr random_crypt; +}; + + + +} // namespace Episode3 diff --git a/src/Episode3/MapState.cc b/src/Episode3/MapState.cc new file mode 100644 index 00000000..08766327 --- /dev/null +++ b/src/Episode3/MapState.cc @@ -0,0 +1,95 @@ +#include "MapState.hh" + +using namespace std; + +namespace Episode3 { + + + +MapState::MapState() { + this->clear(); +} + +void MapState::clear() { + this->width = 0; + this->height = 0; + for (size_t y = 0; y < this->tiles.size(); y++) { + this->tiles[y].clear(0); + } + for (size_t z = 0; z < 2; z++) { + this->start_tile_definitions[z].clear(0); + } +} + +void MapState::print(FILE* stream) const { + fprintf(stream, "[Map: w=%hu h=%hu]\n", this->width.load(), this->height.load()); + for (size_t y = 0; y < this->height; y++) { + fputc(' ', stream); + for (size_t x = 0; x < this->width; x++) { + fprintf(stream, " %02hhX", this->tiles[y][x]); + } + fputc('\n', stream); + } +} + + + +MapAndRulesState::MapAndRulesState() { + this->clear(); +} + +void MapAndRulesState::clear() { + this->map.clear(); + this->num_players = 0; + this->unused1 = 0; + this->unused_by_server = 0; + this->num_players_per_team = 0; + this->num_team0_players = 0; + this->unused2 = 0; + this->start_facing_directions = 0; + this->unused3 = 0; + this->map_number = 0; + this->unused4 = 0; + this->rules.clear(); + this->unused5 = 0; +} + + + +bool MapAndRulesState::loc_is_within_bounds(uint8_t x, uint8_t y) const { + return (x < this->map.width) && (y < this->map.height); +} + +bool MapAndRulesState::tile_is_vacant(uint8_t x, uint8_t y) { + if (!this->loc_is_within_bounds(x, y)) { + return false; + } + return (this->map.tiles[y][x] == 1); +} + +void MapAndRulesState::set_occupied_bit_for_tile(uint8_t x, uint8_t y) { + this->map.tiles[y][x] |= 0x10; +} + +void MapAndRulesState::clear_occupied_bit_for_tile(uint8_t x, uint8_t y) { + this->map.tiles[y][x] &= 0xEF; +} + + + +OverlayState::OverlayState() { + this->clear(); +} + +void OverlayState::clear() { + for (size_t y = 0; y < this->tiles.size(); y++) { + this->tiles[y].clear(0); + } + this->unused1.clear(0); + this->unused2.clear(0); + this->unused3.clear(0); +} + + + +} // namespace Episode3 diff --git a/src/Episode3/MapState.hh b/src/Episode3/MapState.hh new file mode 100644 index 00000000..7baac7f8 --- /dev/null +++ b/src/Episode3/MapState.hh @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include + +#include "../Text.hh" +#include "DataIndex.hh" + +namespace Episode3 { + + + +struct MapState { + le_uint16_t width; + le_uint16_t height; + parray, 0x10> tiles; + parray, 2> start_tile_definitions; + + MapState(); + void clear(); + + void print(FILE* stream) const; +} __attribute__((packed)); + +struct MapAndRulesState { + MapState map; + uint8_t num_players; + uint8_t unused1; + uint8_t unused_by_server; + uint8_t num_players_per_team; + uint8_t num_team0_players; + uint8_t unused2; + le_uint16_t start_facing_directions; + uint32_t unused3; + le_uint32_t map_number; + uint32_t unused4; + Rules rules; + uint32_t unused5; + + MapAndRulesState(); + void clear(); + + bool loc_is_within_bounds(uint8_t x, uint8_t y) const; + bool tile_is_vacant(uint8_t x, uint8_t y); + + void set_occupied_bit_for_tile(uint8_t x, uint8_t y); + void clear_occupied_bit_for_tile(uint8_t x, uint8_t y); +} __attribute__((packed)); + +struct OverlayState { + parray, 0x10> tiles; + parray unused1; + parray unused2; + parray unused3; + + OverlayState(); + void clear(); +} __attribute__((packed)); + + + +} // namespace Episode3 diff --git a/src/Episode3/PlayerState.cc b/src/Episode3/PlayerState.cc new file mode 100644 index 00000000..62b0eb2f --- /dev/null +++ b/src/Episode3/PlayerState.cc @@ -0,0 +1,1835 @@ +#include "PlayerState.hh" + +#include "Server.hh" + +using namespace std; + +namespace Episode3 { + + + +PlayerState::PlayerState(uint8_t client_id, shared_ptr server) + : w_server(server), + client_id(client_id), + num_mulligans_allowed(1), + sc_card_type(CardType::HUNTERS_SC), + team_id(0xFF), + atk_points(0), + def_points(0), + atk_points2(0), + atk_points2_max(6), + atk_bonuses(0), + def_bonuses(0), + dice_results(0), + unknown_a4(2), + dice_max(6), + total_set_cards_cost(0), + sc_card_id(0xFFFF), + sc_card_ref(0xFFFF), + card_refs(0xFFFF), + discard_log_card_refs(0xFFFF), + discard_log_reasons(0), + assist_remaining_turns(0), + assist_card_set_number(0), + set_assist_card_id(0xFFFF), + god_whim_can_use_hidden_cards(false), + unknown_a14(0), + assist_flags(0), + assist_delay_turns(0), + start_facing_direction(Direction::RIGHT), + num_destroyed_fcs(0), + unknown_a16(0), + unknown_a17(0) { } + +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. + throw logic_error("replacing a player state object is not permitted"); + } + + this->deck_state.reset(new DeckState( + this->client_id, + this->server()->base()->deck_entries[client_id]->card_ids, + this->server()->random_crypt)); + if (this->server()->base()->map_and_rules1->rules.disable_deck_shuffle) { + this->deck_state->disable_shuffle(); + } + if (this->server()->base()->map_and_rules1->rules.disable_deck_loop) { + this->deck_state->disable_loop(); + } + + this->sc_card_ref = this->deck_state->sc_card_ref(); + this->sc_card_id = this->server()->card_id_for_card_ref(this->sc_card_ref); + this->team_id = this->server()->base()->deck_entries[this->client_id]->team_id; + auto sc_ce = this->server()->definition_for_card_ref(this->sc_card_ref); + if (!sc_ce) { + throw runtime_error("SC card definition is missing"); + } + if (sc_ce->def.type == CardType::HUNTERS_SC) { + this->sc_card_type = CardType::HUNTERS_SC; + } else if (sc_ce->def.type == CardType::ARKZ_SC) { + this->sc_card_type = CardType::ARKZ_SC; + } else { + // In the original code, sc_card_type gets left as 0xFFFFFFFF (yes, it's a + // uint32_t). This probably breaks some things later on, so we instead + // prevent it upfront. + throw runtime_error("SC card is not a Hunters or Arkz SC"); + } + + this->hand_and_equip.reset(new HandAndEquipState()); + this->card_short_statuses.reset(new parray()); + this->set_card_action_chains.reset(new parray()); + this->set_card_action_metadatas.reset(new parray()); + + this->hand_and_equip->clear_FF(); + for (size_t z = 0; z < 0x10; z++) { + this->card_short_statuses->at(z).clear_FF(); + } + for (size_t z = 0; z < 9; z++) { + this->set_card_action_chains->at(z).clear_FF(); + this->set_card_action_metadatas->at(z).clear_FF(); + } + + this->sc_card.reset(new Card( + this->deck_state->sc_card_id(), + this->sc_card_ref, + this->client_id, + this->server())); + this->sc_card->init(); + this->draw_initial_hand(); + + this->server()->assist_server->hand_and_equip_states[this->client_id] = this->hand_and_equip; + this->server()->assist_server->card_short_statuses[this->client_id] = this->card_short_statuses; + this->server()->assist_server->deck_entries[this->client_id] = this->server()->base()->deck_entries[this->client_id]; + this->server()->assist_server->set_card_action_chains[this->client_id] = this->set_card_action_chains; + this->server()->assist_server->set_card_action_metadatas[this->client_id] = this->set_card_action_metadatas; + this->server()->ruler_server->register_player( + this->client_id, + this->hand_and_equip, + this->card_short_statuses, + this->server()->base()->deck_entries[this->client_id], + this->set_card_action_chains, + this->set_card_action_metadatas); + this->server()->ruler_server->set_client_team_id(this->client_id, this->team_id); + + this->server()->card_special->on_card_set(this->shared_from_this(), this->sc_card_ref); + + this->god_whim_can_use_hidden_cards = (this->server()->base()->deck_entries[this->client_id]->god_whim_flag != 3); +} + +shared_ptr PlayerState::server() { + auto s = this->w_server.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +shared_ptr PlayerState::server() const { + auto s = this->w_server.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +bool PlayerState::draw_cards_allowed() const { + if (!(this->assist_flags & 0x80)) { + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->server()->assist_server->get_active_assist_by_index(z); + if (eff == AssistEffect::SKIP_DRAW) { + return false; + } + } + return true; + } else { + return false; + } +} + +void PlayerState::apply_assist_card_effect_on_set( + shared_ptr setter_ps) { + uint16_t assist_card_id = this->set_assist_card_id; + if (assist_card_id == 0xFFFF) { + assist_card_id = this->server()->card_id_for_card_ref(this->card_refs[6]); + } + + auto assist_effect = assist_effect_number_for_card_id(assist_card_id); + if ((assist_effect == AssistEffect::RESISTANCE) || + (assist_effect == AssistEffect::INDEPENDENT)) { + this->assist_card_set_number = 0; + } + + if (this->server()->assist_server->should_block_assist_effects_for_client(this->client_id)) { + return; + } + + switch (assist_effect) { + case AssistEffect::CARD_RETURN: { + size_t hand_index; + for (hand_index = 0; hand_index < 6; hand_index++) { + if (this->card_refs[hand_index] == 0xFFFF) { + break; + } + } + + if (hand_index < 6) { + for (size_t z = 0; z < 0x10; z++) { + if (this->deck_state->draw_card_by_ref(this->discard_log_card_refs[z])) { + this->card_refs[hand_index] = this->discard_log_card_refs[z]; + this->discard_log_card_refs[z] = 0xFFFF; + break; + } + } + } + break; + } + + case AssistEffect::ATK_DICE_2: + this->assist_delay_turns = (!setter_ps || (setter_ps->team_id == this->team_id)) ? 2 : 1; + break; + + case AssistEffect::EXCHANGE: { + uint8_t t = this->atk_points; + this->atk_points = this->def_points; + this->def_points = t; + this->atk_points2 = this->atk_points; + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + break; + } + + case AssistEffect::SKIP_SET: + case AssistEffect::SKIP_ACT: + this->assist_delay_turns = 2; + break; + + case AssistEffect::NECROMANCER: { + ssize_t hand_index; + for (hand_index = 5; hand_index >= 0; hand_index--) { + if (this->card_refs[hand_index] != 0xFFFF) { + break; + } + } + + size_t log_index; + for (log_index = 0; log_index < 0x10; log_index++) { + auto ce = this->server()->definition_for_card_ref( + this->discard_log_card_refs[log_index]); + if (ce && ((ce->def.type == CardType::ITEM || ce->def.type == CardType::CREATURE))) { + break; + } + } + + if ((hand_index >= 0) && (log_index < 0x10) && + this->deck_state->draw_card_by_ref(this->discard_log_card_refs[log_index])) { + uint16_t hand_card_ref = this->card_refs[hand_index]; + this->card_refs[hand_index] = this->discard_log_card_refs[log_index]; + this->discard_log_card_refs[log_index] = hand_card_ref; + this->deck_state->set_card_discarded(hand_card_ref); + } + break; + } + + case AssistEffect::LEGACY: { + uint16_t total_cost = 0; + for (ssize_t z = 7; z >= 0; z--) { + shared_ptr card = this->set_cards[z]; + if (card) { + auto ce = card->get_definition(); + uint8_t card_cost = ce->def.self_cost; + if (this->discard_card_or_add_to_draw_pile(this->card_refs[8 + z], false)) { + total_cost += card_cost; + } + } + } + this->on_cards_destroyed(); + + this->atk_points = min(9, this->atk_points + (total_cost >> 1)); + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + this->server()->send_6xB4x05(); + break; + } + + case AssistEffect::MUSCULAR: + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->get_player_state(client_id); + if (other_ps) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = other_ps->get_set_card(set_index); + if (card) { + card->ap++; + card->send_6xB4x4E_4C_4D_if_needed(); + } + } + other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + } + break; + + case AssistEffect::CHANGE_BODY: + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->get_player_state(client_id); + if (other_ps) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = other_ps->get_set_card(set_index); + if (card) { + uint8_t orig_ap = card->ap; + card->ap = card->tp; + card->tp = orig_ap; + card->send_6xB4x4E_4C_4D_if_needed(); + } + } + other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + } + break; + + case AssistEffect::GOD_WHIM: + this->replace_all_set_assists_with_random_assists(); + break; + + case AssistEffect::ASSIST_RETURN: + if (this->card_refs[7] != 0xFFFF) { + uint8_t client_id = client_id_for_card_ref(this->card_refs[7]); + auto other_ps = this->server()->get_player_state(client_id); + if (other_ps.get() != this) { + other_ps->deck_state->draw_card_by_ref(this->card_refs[7]); + other_ps->set_card_from_hand( + this->card_refs[7], 0xF, nullptr, client_id, 1); + } + } + break; + + case AssistEffect::REQUIEM: + this->server()->add_team_exp(this->team_id, this->num_destroyed_fcs << 1); + this->server()->update_battle_state_flags_and_send_6xB4x03_if_needed(); + + this->num_destroyed_fcs = 0; + this->server()->team_num_cards_destroyed[this->team_id] = 0; + for (size_t client_id = 0; client_id < 4; client_id++) { + const auto other_ps = this->server()->get_player_state(client_id); + if (other_ps && (this->team_id == other_ps->get_team_id())) { + auto card = other_ps->get_sc_card(); + if (card) { + card->num_cards_destroyed_by_team_at_set_time = 0; + card->num_destroyed_ally_fcs = 0; + } + for (size_t set_index = 0; set_index < 8; set_index++) { + auto set_card = other_ps->get_set_card(set_index); + if (set_card) { + set_card->num_cards_destroyed_by_team_at_set_time = 0; + set_card->num_destroyed_ally_fcs = 0; + } + } + } + } + break; + + case AssistEffect::SLOW_TIME: + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->get_player_state(client_id); + if (!other_ps) { + continue; + } + + if (other_ps->assist_remaining_turns < 10) { + other_ps->assist_remaining_turns = min(9, other_ps->assist_remaining_turns << 1); + } + + for (ssize_t set_index = -1; set_index < 8; set_index++) { + auto card = (set_index == -1) + ? other_ps->get_sc_card() + : other_ps->get_set_card(set_index); + if (card) { + for (size_t cond_index = 0; cond_index < 9; cond_index++) { + auto& cond = card->action_chain.conditions[cond_index]; + if ((cond.type != ConditionType::NONE) && + (cond.remaining_turns < 10)) { + cond.remaining_turns = min(9, cond.remaining_turns << 1); + } + } + } + } + + other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + other_ps->send_set_card_updates(); + } + break; + + case AssistEffect::QUICK_TIME: + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->get_player_state(client_id); + if (!other_ps) { + continue; + } + + if (other_ps->assist_remaining_turns < 10) { + other_ps->assist_remaining_turns = ((other_ps->assist_remaining_turns + 1) >> 1); + } + + for (ssize_t set_index = -1; set_index < 8; set_index++) { + auto card = (set_index == -1) + ? other_ps->get_sc_card() + : other_ps->get_set_card(set_index); + if (card) { + for (size_t cond_index = 0; cond_index < 9; cond_index++) { + auto& cond = card->action_chain.conditions[cond_index]; + if ((cond.type != ConditionType::NONE) && + (cond.remaining_turns < 10)) { + cond.remaining_turns = (cond.remaining_turns + 1) >> 1; + } + } + } + } + other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + other_ps->send_set_card_updates(); + } + break; + + case AssistEffect::SQUEEZE: + this->set_random_assist_card_from_hand_for_free(); + break; + + case AssistEffect::BOMB: + this->assist_delay_turns = 6; + break; + + case AssistEffect::SKIP_TURN: + if (!setter_ps || (setter_ps->team_id == this->team_id)) { + this->assist_delay_turns = 6; + } else { + this->assist_delay_turns = 5; + } + break; + + default: + break; + } +} + +void PlayerState::apply_dice_effects() { + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->server()->assist_server->get_active_assist_by_index(z); + switch (eff) { + case AssistEffect::DICE_FEVER: + for (size_t die_index = 0; die_index < 2; die_index++) { + if (this->dice_results[die_index] > 0) { + this->dice_results[die_index] = 5; + } + } + break; + case AssistEffect::DICE_HALF: + for (size_t die_index = 0; die_index < 2; die_index++) { + if (this->dice_results[die_index] > 0) { + this->dice_results[die_index] = (this->dice_results[die_index] + 1) >> 1; + } + } + break; + case AssistEffect::DICE_PLUS_1: + for (size_t die_index = 0; die_index < 2; die_index++) { + if (this->dice_results[die_index] > 0) { + this->dice_results[die_index] = this->dice_results[die_index] + 1; + } + } + break; + case AssistEffect::DICE_FEVER_PLUS: + for (size_t die_index = 0; die_index < 2; die_index++) { + if (this->dice_results[die_index] > 0) { + this->dice_results[die_index] = 6; + } + } + break; + default: + break; + } + } + + for (size_t die_index = 0; die_index < 2; die_index++) { + this->dice_results[die_index] = min(this->dice_results[die_index], 9); + } +} + +uint16_t PlayerState::card_ref_for_hand_index(size_t hand_index) const { + return (hand_index < 6) ? this->card_refs[hand_index] : 0xFFFF; +} + +int16_t PlayerState::compute_attack_or_defense_atk_costs(const ActionState& pa) const { + return this->server()->ruler_server->compute_attack_or_defense_costs(pa, 0, 0); +} + +void PlayerState::compute_total_set_cards_cost() { + this->total_set_cards_cost = 0; + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = this->set_cards[set_index]; + if (!card) { + continue; + } + auto ce = card->get_definition(); + if (ce) { + this->total_set_cards_cost += ce->def.self_cost; + } + } +} + +size_t PlayerState::count_set_cards() const { + size_t ret = 0; + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = this->set_cards[set_index]; + if (card && !(card->card_flags & 2)) { + ret++; + } + } + return ret; +} + +size_t PlayerState::count_set_refs() const { + size_t ret = 0; + for (size_t set_index = 0; set_index < 8; set_index++) { + if (this->card_refs[set_index] != 0xFFFF) { + ret++; + } + } + return ret; +} + +void PlayerState::discard_all_assist_cards_from_hand() { + parray temp_card_refs; + for (size_t hand_index = 0; hand_index < 6; hand_index++) { + temp_card_refs[hand_index] = this->card_refs[hand_index]; + } + + for (size_t hand_index = 0; hand_index < 6; hand_index++) { + uint16_t card_ref = temp_card_refs[hand_index]; + auto ce = this->server()->definition_for_card_ref(card_ref); + if (ce && (ce->def.type == CardType::ASSIST)) { + this->discard_ref_from_hand(card_ref); + } + } + + this->move_null_hand_refs_to_end(); +} + +void PlayerState::discard_all_attack_action_cards_from_hand() { + parray temp_card_refs; + for (size_t hand_index = 0; hand_index < 6; hand_index++) { + temp_card_refs[hand_index] = this->card_refs[hand_index]; + } + + for (size_t hand_index = 0; hand_index < 6; hand_index++) { + uint16_t card_ref = temp_card_refs[hand_index]; + auto ce = this->server()->definition_for_card_ref(card_ref); + if (ce && (ce->def.type == CardType::ACTION) && + (ce->def.card_class() != CardClass::DEFENSE_ACTION)) { + this->discard_ref_from_hand(card_ref); + } + } + + this->move_null_hand_refs_to_end(); +} + +void PlayerState::discard_all_item_and_creature_cards_from_hand() { + parray temp_card_refs; + for (size_t hand_index = 0; hand_index < 6; hand_index++) { + temp_card_refs[hand_index] = this->card_refs[hand_index]; + } + + for (size_t hand_index = 0; hand_index < 6; hand_index++) { + uint16_t card_ref = temp_card_refs[hand_index]; + auto ce = this->server()->definition_for_card_ref(card_ref); + if (ce && ((ce->def.type == CardType::ITEM) || (ce->def.type == CardType::CREATURE))) { + this->discard_ref_from_hand(card_ref); + } + } + + this->move_null_hand_refs_to_end(); +} + +void PlayerState::discard_and_redraw_hand() { + while (this->card_refs[0] != 0xFFFF) { + this->discard_ref_from_hand(this->card_refs[0]); + } + + G_Unknown_GC_Ep3_6xB4x2C cmd; + cmd.change_type = 3; + cmd.client_id = this->client_id; + this->server()->send(cmd); + + this->deck_state->restart(); + this->draw_hand(); + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); +} + +bool PlayerState::discard_card_or_add_to_draw_pile( + uint16_t card_ref, bool add_to_draw_pile) { + ssize_t set_index = this->set_index_for_card_ref(card_ref); + if (set_index < 0) { + return false; + } + + this->deck_state->set_card_discarded(card_ref); + this->card_refs[set_index + 8] = 0xFFFF; + this->set_cards[set_index]->card_flags |= 2; + if (add_to_draw_pile) { + this->deck_state->set_card_ref_drawable_at_end(card_ref); + } + this->log_discard(card_ref, 0); + return true; +} + +void PlayerState::discard_random_hand_card() { + size_t max = this->get_hand_size(); + if (max > 0) { + this->discard_ref_from_hand(this->card_refs[this->server()->get_random(max)]); + } + this->move_null_hand_refs_to_end(); +} + +bool PlayerState::discard_ref_from_hand(uint16_t card_ref) { + ssize_t index = this->hand_index_for_card_ref(card_ref); + if (index >= 0) { + this->deck_state->set_card_discarded(card_ref); + this->card_refs[index] = 0xFFFF; + this->move_null_hand_refs_to_end(); + this->log_discard(card_ref, 0); + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + return true; + } else { + return false; + } +} + +void PlayerState::discard_set_assist_card() { + this->set_assist_card_id = 0xFFFF; + uint8_t client_id = client_id_for_card_ref(this->card_refs[6]); + auto setter_ps = this->server()->get_player_state(client_id); + if (setter_ps) { + setter_ps->get_deck()->set_card_discarded(this->card_refs[6]); + this->card_refs[6] = 0xFFFF; + } + this->card_refs[7] = 0xFFFF; + this->assist_remaining_turns = 0; + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + + this->server()->assist_server->populate_effects(); + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->get_player_state(client_id); + if (!other_ps) { + continue; + } + uint32_t prev_assist_flags = other_ps->assist_flags; + other_ps->set_assist_flags_from_assist_effects(); + if (other_ps->assist_flags != prev_assist_flags) { + other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + } + + this->server()->destroy_cards_with_zero_hp(); +} + +bool PlayerState::do_mulligan() { + if (!this->is_mulligan_allowed()) { + return false; + } + + this->num_mulligans_allowed--; + while (this->card_refs[0] != 0xFFFF) { + this->discard_ref_from_hand(this->card_refs[0]); + } + + G_Unknown_GC_Ep3_6xB4x2C cmd; + cmd.change_type = 3; + cmd.client_id = this->client_id; + this->server()->send(cmd); + + this->deck_state->do_mulligan(); + this->draw_hand(5); + + this->discard_log_card_refs.clear(0xFFFF); + return true; +} + +void PlayerState::draw_hand(ssize_t override_count) { + ssize_t count = 5 - this->get_hand_size(); + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->server()->assist_server->get_active_assist_by_index(z); + if (eff == AssistEffect::RICH_PLUS) { + count = 4 - this->get_hand_size(); + } else if (eff == AssistEffect::RICH) { + count = 6 - this->get_hand_size(); + } + } + + if ((override_count != 0) && (override_count < count)) { + count = override_count; + } + + for (; count > 0; count--) { + uint16_t card_ref = this->deck_state->draw_card(); + for (size_t z = 0; z < 6; z++) { + if (this->card_refs[z] == 0xFFFF) { + this->card_refs[z] = card_ref; + break; + } + } + if (this->server()->get_setup_phase() == SetupPhase::MAIN_BATTLE) { + this->stats.num_cards_drawn++; + } + } + + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); +} + +void PlayerState::draw_initial_hand() { + // Note: The original code called this->deck_state->init_card_states here, but + // we don't because that logic is now in the DeckState constructor, and this + // function should only be called during PlayerState construction (so, shortly + // after DeckState construction as well). + this->deck_state->restart(); + this->card_refs.clear(0xFFFF); + this->draw_hand(5); + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); +} + +int32_t PlayerState::error_code_for_client_setting_card( + uint16_t card_ref, + uint8_t card_index, + const Location* loc, + uint8_t assist_target_client_id) const { + int32_t code = this->server()->ruler_server->error_code_for_client_setting_card( + this->client_id, card_ref, loc, assist_target_client_id); + if (code) { + return code; + } + + if (this->hand_index_for_card_ref(card_ref) < 0) { + return -0x7F; + } + + if (this->deck_state->state_for_card_ref(card_ref) != DeckState::CardState::IN_HAND) { + return -0x7D; + } + + auto ce = this->server()->definition_for_card_ref(card_ref); + if (!ce) { + return -0x7D; + } + + switch (ce->def.type) { + case CardType::ITEM: + case CardType::CREATURE: + if ((card_index < 7) || (card_index >= 15)) { + return -0x7E; + } + if (this->card_refs[card_index + 1] != 0xFFFF) { + return -0x7E; + } + if ((ce->def.type == CardType::CREATURE) && + !this->server()->base()->map_and_rules1->tile_is_vacant(loc->x, loc->y)) { + return -0x7A; + } + return 0; + + case CardType::ASSIST: + return (card_index == 15) ? 0 : -0x7E; + + case CardType::HUNTERS_SC: + case CardType::ARKZ_SC: + case CardType::ACTION: + default: + return -0x7D; + } +} + +vector PlayerState::get_all_cards_within_range( + const parray& range, + const Location& loc, + uint8_t target_team_id) const { + vector ret; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->player_states[client_id]; + if (other_ps && + ((target_team_id == 0xFF) || (target_team_id == other_ps->get_team_id()))) { + ret = get_card_refs_within_range( + range, loc, *other_ps->card_short_statuses); + } + } + return ret; +} + +uint8_t PlayerState::get_atk_points() const { + return this->atk_points; +} + +void PlayerState::get_short_status_for_card_index_in_hand( + size_t hand_index, CardShortStatus* stat) const { + stat->card_ref = this->card_refs[hand_index - 1]; +} + +shared_ptr PlayerState::get_deck() { + return this->deck_state; +} + +uint8_t PlayerState::get_def_points() const { + return this->def_points; +} + +uint8_t PlayerState::get_dice_result(size_t which) const { + return this->dice_results[which]; +} + +size_t PlayerState::get_hand_size() const { + size_t ret = 0; + for (size_t z = 0; z < 6; z++) { + if (this->card_refs[z] != 0xFFFF) { + ret++; + } + } + return ret; +} + +uint16_t PlayerState::get_sc_card_id() const { + return this->sc_card_id; +} + +shared_ptr PlayerState::get_sc_card() { + return this->sc_card; +} + +shared_ptr PlayerState::get_sc_card() const { + return this->sc_card; +} + +uint16_t PlayerState::get_sc_card_ref() const { + return this->sc_card_ref; +} + +CardType PlayerState::get_sc_card_type() const { + return this->sc_card_type; +} + +shared_ptr PlayerState::get_set_card(size_t set_index) { + return (set_index < 8) ? this->set_cards[set_index] : nullptr; +} + +shared_ptr PlayerState::get_set_card(size_t set_index) const { + return (set_index < 8) ? this->set_cards[set_index] : nullptr; +} + +uint16_t PlayerState::get_set_ref(size_t set_index) const { + return this->card_refs[set_index + 8]; +} + +uint8_t PlayerState::get_team_id() const { + return this->team_id; +} + +ssize_t PlayerState::hand_index_for_card_ref(uint16_t card_ref) const { + for (size_t z = 0; z < 6; z++) { + if (this->card_refs[z] == card_ref) { + return z; + } + } + return -1; +} + +size_t PlayerState::set_index_for_card_ref(uint16_t card_ref) const { + for (size_t z = 0; z < 8; z++) { + if (this->card_refs[z + 8] == card_ref) { + return z; + } + } + return -1; +} + +bool PlayerState::is_mulligan_allowed() const { + return (this->num_mulligans_allowed > 0); +} + +bool PlayerState::is_team_turn() const { + // Note: The original code checks if this->w_server is null before doing this. + // We don't check because that should never happen, and server() will throw if + // it does. + return this->server()->get_current_team_turn() == this->team_id; +} + +void PlayerState::log_discard(uint16_t card_ref, uint16_t reason) { + for (size_t z = 15; z > 0; z--) { + this->discard_log_card_refs[z] = this->discard_log_card_refs[z - 1]; + this->discard_log_reasons[z] = this->discard_log_reasons[z - 1]; + } + this->discard_log_card_refs[0] = card_ref; + this->discard_log_reasons[0] = reason; +} + +bool PlayerState::move_card_to_location_by_card_index( + size_t card_index, const Location& new_loc) { + shared_ptr card; + if (card_index == 0) { + card = this->sc_card; + } else { + if ((card_index < 7) || (card_index >= 15)) { + this->server()->ruler_server->error_code2 = -0x78; + return false; + } + card = this->set_cards[card_index - 7]; + } + if (!card) { + this->server()->ruler_server->error_code2 = -0x78; + return false; + } + + int32_t code = card->error_code_for_move_to_location(new_loc); + if (code) { + this->server()->ruler_server->error_code2 = code; + return false; + } + + card->move_to_location(new_loc); + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + this->send_6xB4x04_if_needed(); + this->server()->send_6xB4x05(); + this->server()->send_6xB4x39(); + this->server()->card_special->unknown_80244AA8(card); + return true; +} + +void PlayerState::move_null_hand_refs_to_end() { + size_t write_offset = 0; + for (size_t read_offset = 0; read_offset < 6; read_offset++) { + uint16_t card_ref = this->card_refs[read_offset]; + if (card_ref != 0xFFFF) { + this->card_refs[write_offset++] = card_ref; + } + } + for (; write_offset < 6; write_offset++) { + this->card_refs[write_offset] = 0xFFFF; + } +} + +void PlayerState::on_cards_destroyed() { + // {card_ref: should_return_to_hand} + unordered_multimap card_refs_map; + + for (size_t z = 0; z < 8; z++) { + auto card = this->set_cards[z]; + if (!card || !(card->card_flags & 2)) { + continue; + } + + uint16_t card_ref = this->card_refs[z + 8]; + card_refs_map.emplace(card_ref, this->server()->card_special->should_return_card_ref_to_hand_on_destruction(this->card_refs[z + 8])); + + bool should_discard = true; + for (size_t hand_index = 0; hand_index < 6; hand_index++) { + if (this->card_refs[hand_index] == card_ref) { + should_discard = false; + break; + } + } + + if (should_discard) { + this->log_discard(card_ref, 1); + this->deck_state->set_card_discarded(this->card_refs[z + 8]); + } + + this->card_refs[z + 8] = 0xFFFF; + this->set_cards[z].reset(); + } + + size_t write_index = 0; + bool indexes_diverged = false; + for (size_t read_index = 0; read_index < 8; read_index++) { + auto card = this->set_cards[read_index]; + if (card) { + if (read_index != write_index) { + this->set_cards[write_index] = card; + this->card_refs[write_index + 8] = this->card_refs[read_index + 8]; + indexes_diverged = true; + } + write_index++; + } + } + for (; write_index < 8; write_index++) { + this->set_cards[write_index].reset(); + this->card_refs[write_index + 8] = 0xFFFF; + } + + if (indexes_diverged) { + this->send_set_card_updates(); + } + + for (const auto& it : card_refs_map) { + uint16_t card_ref = it.first; + bool should_return = it.second; + + if (should_return) { + size_t hand_index; + for (hand_index = 0; hand_index < 6; hand_index++) { + if (this->card_refs[hand_index] == 0xFFFF) { + break; + } + } + if ((hand_index < 6) && this->deck_state->draw_card_by_ref(card_ref)) { + this->card_refs[hand_index] = card_ref; + } + } + } +} + +void PlayerState::replace_all_set_assists_with_random_assists() { + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->get_player_state(client_id); + if (other_ps && + ((other_ps->card_refs[6] != 0xFFFF) || (other_ps->set_assist_card_id != 0xFFFF))) { + uint16_t card_id = 0x0130; + while (card_id == 0x0130) { // God Whim + size_t index = this->server()->get_random(ALL_ASSIST_CARD_IDS.size()); + card_id = ALL_ASSIST_CARD_IDS[index]; + if (!this->god_whim_can_use_hidden_cards) { + auto ce = this->server()->definition_for_card_id(card_id); + if (!ce || ce->def.hide_in_deck_edit) { + continue; + } + } + } + other_ps->replace_assist_card_by_id(card_id); + } + } +} + +bool PlayerState::replace_assist_card_by_id(uint16_t card_id) { + auto ce = this->server()->definition_for_card_id(card_id); + if (!ce || (ce->def.type != CardType::ASSIST)) { + return false; + } + + this->discard_set_assist_card(); + this->set_assist_card_id = card_id; + this->assist_remaining_turns = ce->def.assist_turns; + this->assist_card_set_number = this->server()->next_assist_card_set_number++; + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + this->server()->assist_server->populate_effects(); + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->get_player_state(client_id); + if (other_ps) { + uint32_t prev_assist_flags = other_ps->assist_flags; + other_ps->set_assist_flags_from_assist_effects(); + if (prev_assist_flags != other_ps->assist_flags) { + other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + } + } + + this->apply_assist_card_effect_on_set(this->shared_from_this()); + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + return true; +} + +bool PlayerState::return_set_card_to_hand2(uint16_t card_ref) { + size_t set_index; + for (set_index = 0; set_index < 8; set_index++) { + if (this->card_refs[set_index + 8] == card_ref) { + break; + } + } + + if (set_index < 8) { + size_t hand_index; + for (hand_index = 0; hand_index < 6; hand_index++) { + if (this->card_refs[hand_index] == 0xFFFF) { + break; + } + } + if (hand_index < 6) { + this->deck_state->set_card_discarded(card_ref); + if (this->deck_state->draw_card_by_ref(card_ref)) { + this->card_refs[hand_index] = card_ref; + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + this->send_6xB4x04_if_needed(); + return true; + } + } + } + return false; +} + +bool PlayerState::return_set_card_to_hand1(uint16_t card_ref) { + size_t hand_index; + for (hand_index = 0; hand_index < 6; hand_index++) { + if (this->card_refs[hand_index] == 0xFFFF) { + break; + } + } + + if ((hand_index < 6) && (card_ref != 0xFFFF)) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = this->set_cards[set_index]; + if (card && (card->get_card_ref() == card_ref)) { + uint16_t set_card_ref = this->card_refs[set_index + 8]; + this->card_refs[set_index + 8] = 0xFFFF; + card->card_flags |= 2; + this->deck_state->set_card_discarded(set_card_ref); + if (this->deck_state->draw_card_by_ref(set_card_ref)) { + this->card_refs[hand_index] = set_card_ref; + return true; + } + } + } + } + + return false; +} + +uint8_t PlayerState::roll_dice(size_t num_dice) { + uint8_t ret = 0; + for (size_t z = 0; z < num_dice; z++) { + this->dice_results[z] = this->server()->get_random(this->dice_max) + 1; + ret += this->dice_results[z]; + } + + if (num_dice < 1) { + this->dice_results[0] = 0; + } + if (num_dice < 2) { + this->dice_results[1] = 0; + } + + return ret; +} + +uint8_t PlayerState::roll_dice_with_effects(size_t num_dice) { + this->roll_dice(num_dice); + this->apply_dice_effects(); + return this->dice_results[0]; +} + +void PlayerState::send_set_card_updates(bool always_send) { + uint16_t mask; + if (!this->sc_card) { + this->set_card_action_chains->at(0).clear(); + this->set_card_action_metadatas->at(0).clear(); + mask = 1; + } else { + this->sc_card->send_6xB4x4E_4C_4D_if_needed(always_send); + mask = 0; + } + + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = this->set_cards[set_index]; + if (!card) { + mask |= 1 << (set_index + 1); + this->set_card_action_chains->at(set_index + 1).clear(); + this->set_card_action_metadatas->at(set_index + 1).clear(); + } else { + card->send_6xB4x4E_4C_4D_if_needed(always_send); + } + } + + if (mask && !this->server()->get_should_copy_prev_states_to_current_states()) { + G_ClearSetCardConditions_GC_Ep3_6xB4x4F cmd; + cmd.client_id = this->client_id; + cmd.clear_mask = mask; + this->server()->send(cmd); + } +} + +void PlayerState::set_assist_flags_from_assist_effects() { + this->assist_flags &= 0xFFFFF88F; + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); + for (size_t z = 0; z < num_assists; z++) { + switch (this->server()->assist_server->get_active_assist_by_index(z)) { + case AssistEffect::SIMPLE: + this->assist_flags |= 0x10; + break; + case AssistEffect::TERRITORY: + this->assist_flags |= 0x200; + break; + case AssistEffect::OLD_TYPE: + this->assist_flags |= 0x400; + break; + case AssistEffect::FLATLAND: + this->assist_flags |= 0x20; + break; + case AssistEffect::IMMORTALITY: + this->assist_flags |= 0x100; + break; + case AssistEffect::SNAIL_PACE: + this->assist_flags |= 0x40; + break; + default: + break; + } + } + return; +} + +bool PlayerState::set_card_from_hand( + uint16_t card_ref, + uint8_t card_index, + const Location* loc, + uint8_t assist_target_client_id, + bool skip_error_checks_and_atk_sub) { + if (!skip_error_checks_and_atk_sub) { + int32_t code = this->error_code_for_client_setting_card( + card_ref, card_index, loc, assist_target_client_id); + if (code) { + this->server()->ruler_server->error_code1 = code; + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + return false; + } + } + + ssize_t hand_index = this->hand_index_for_card_ref(card_ref); + if (hand_index >= 0) { + this->card_refs[hand_index] = 0xFFFF; + this->move_null_hand_refs_to_end(); + } + + if (!skip_error_checks_and_atk_sub) { + int16_t cost = this->server()->ruler_server->set_cost_for_card( + this->client_id, card_ref); + this->subtract_atk_points(cost); + } + + this->deck_state->set_card_ref_in_play(card_ref); + + auto ce = this->server()->definition_for_card_ref(card_ref); + if (ce->def.type == CardType::ITEM || ce->def.type == CardType::CREATURE) { + if ((card_index < 7) || (card_index >= 15)) { + return 0; + } + this->card_refs[card_index + 1] = card_ref; + this->set_cards[card_index - 7].reset(new Card( + this->server()->card_id_for_card_ref(card_ref), + card_ref, + this->client_id, + this->server())); + auto new_card = this->set_cards[card_index - 7]; + new_card->init(); + + if (ce->def.type == CardType::CREATURE) { + new_card->loc.x = loc->x; + new_card->loc.y = loc->y; + } + this->stats.num_item_or_creature_cards_set++; + + } else if (ce->def.type == CardType::ASSIST) { + if (card_index != 15) { + return false; + } + + auto target_ps = this->server()->player_states[assist_target_client_id]; + if (target_ps) { + uint16_t prev_assist_card_ref = target_ps->card_refs[6]; + target_ps->discard_set_assist_card(); + target_ps->card_refs[6] = card_ref; + target_ps->card_refs[7] = prev_assist_card_ref; + + target_ps->assist_remaining_turns = ce->def.assist_turns; + target_ps->assist_delay_turns = 0; + target_ps->assist_card_set_number = this->server()->next_assist_card_set_number++; + + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + target_ps->apply_assist_card_effect_on_set(this->shared_from_this()); + target_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + this->server()->assist_server->populate_effects(); + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->get_player_state(client_id); + if (!other_ps) { + continue; + } + uint32_t prev_assist_flags = other_ps->assist_flags; + other_ps->set_assist_flags_from_assist_effects(); + if (other_ps->assist_flags != prev_assist_flags) { + other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + } + } + this->stats.num_assist_cards_set++; + } + this->stats.num_cards_set++; + + this->compute_total_set_cards_cost(); + this->server()->card_special->on_card_set(this->shared_from_this(), card_ref); + if (ce->def.type == CardType::ASSIST) { + this->server()->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02(); + } + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + this->server()->send_6xB4x05(); + + G_Unknown_GC_Ep3_6xB4x4A cmd; + cmd.card_refs[0] = card_ref; + cmd.client_id = this->client_id; + cmd.entry_count = 1; + cmd.round_num = this->server()->get_round_num(); + this->server()->send(cmd); + + return true; +} + +void PlayerState::set_initial_location() { + auto mr = this->server()->base()->map_and_rules1; + + uint8_t num_team_players; + if (this->team_id == 0) { + num_team_players = mr->num_team0_players; + } else { + num_team_players = mr->num_players - mr->num_team0_players; + } + + uint8_t player_index_within_team = 0; + for (size_t client_id = 0; client_id < 4; client_id++) { + if (client_id == this->client_id) { + break; + } + auto other_ps = this->server()->player_states[client_id]; + if (other_ps && (this->team_id == other_ps->get_team_id())) { + player_index_within_team++; + } + } + + static const uint8_t start_tile_defs_offset_for_team_size[4] = {0, 0, 1, 3}; + if (num_team_players >= 4) { + throw logic_error("too many players on team"); + } + size_t start_tile_def_index = start_tile_defs_offset_for_team_size[num_team_players] + player_index_within_team; + uint8_t player_start_tile = mr->map.start_tile_definitions[this->team_id][start_tile_def_index]; + + Direction facing_direction = static_cast((player_start_tile >> 6) & 3); + this->start_facing_direction = facing_direction; + mr->start_facing_directions |= (static_cast(this->start_facing_direction) << (this->client_id << 2)); + + for (size_t y = 0; y < 0x10; y++) { + for (size_t x = 0; x < 0x10; x++) { + if (mr->map.tiles[y][x] == (player_start_tile & 0x3F)) { + this->sc_card->loc.x = x; + this->sc_card->loc.y = y; + this->sc_card->loc.direction = facing_direction; + y = 0x10; + break; + } + } + } +} + +void PlayerState::set_map_occupied_bit_for_card_on_warp_tile( + shared_ptr card) { + if (!card) { + return; + } + + auto s = this->server(); + for (size_t warp_type = 0; warp_type < 5; warp_type++) { + for (size_t warp_end = 0; warp_end < 2; warp_end++) { + if ((s->warp_positions[warp_type][warp_end][0] == card->loc.x) && + (s->warp_positions[warp_type][warp_end][1] == card->loc.y)) { + s->base()->map_and_rules1->set_occupied_bit_for_tile( + s->warp_positions[warp_type][warp_end ^ 1][0], + s->warp_positions[warp_type][warp_end ^ 1][1]); + } + } + } +} + +void PlayerState::set_map_occupied_bits_for_sc_and_creatures() { + if (this->sc_card && !(this->sc_card->card_flags & 2)) { + this->server()->base()->map_and_rules1->set_occupied_bit_for_tile( + this->sc_card->loc.x, this->sc_card->loc.y); + this->set_map_occupied_bit_for_card_on_warp_tile(this->sc_card); + } + + if (this->sc_card_type == CardType::ARKZ_SC) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = this->set_cards[set_index]; + if (card) { + this->server()->base()->map_and_rules1->set_occupied_bit_for_tile( + card->loc.x, card->loc.y); + this->set_map_occupied_bit_for_card_on_warp_tile(card); + } + } + } +} + +void PlayerState::subtract_def_points(uint8_t cost) { + this->def_points -= cost; +} + +bool PlayerState::subtract_or_check_atk_or_def_points_for_action( + const ActionState& pa, bool deduct_points) { + int16_t cost = this->compute_attack_or_defense_atk_costs(pa); + auto type = this->server()->ruler_server->get_pending_action_type(pa); + + if ((type == ActionType::ATTACK) && (cost <= this->atk_points)) { + if (deduct_points) { + this->subtract_atk_points(cost); + } + return true; + + } else if ((type == ActionType::DEFENSE) && (cost <= (short)this->def_points)) { + if (deduct_points) { + this->subtract_def_points(cost); + } + return true; + } + + return false; +} + +void PlayerState::subtract_atk_points(uint8_t cost) { + this->atk_points -= cost; + this->atk_points2 = min(this->atk_points, this->atk_points2_max); +} + +void PlayerState::update_hand_and_equip_state_and_send_6xB4x02_if_needed( + bool always_send) { + G_UpdateHand_GC_Ep3_6xB4x02 cmd; + cmd.client_id = this->client_id; + cmd.state.dice_results = this->dice_results; + cmd.state.atk_points = this->atk_points; + cmd.state.def_points = this->def_points; + cmd.state.atk_points2 = this->atk_points2; + cmd.state.unknown_a1 = this->unknown_a14; + cmd.state.total_set_cards_cost = this->total_set_cards_cost; + cmd.state.is_cpu_player = this->server()->base()->presence_entries[this->client_id].is_cpu_player; + cmd.state.assist_flags = this->assist_flags; + for (size_t z = 0; z < 6; z++) { + cmd.state.hand_card_refs[z] = this->card_refs[z]; + cmd.state.hand_card_refs2[z] = this->card_refs[z]; + } + for (size_t z = 0; z < 8; z++) { + cmd.state.set_card_refs[z] = this->card_refs[z + 8]; + cmd.state.set_card_refs2[z] = this->card_refs[z + 8]; + } + cmd.state.assist_card_ref = this->card_refs[6]; + cmd.state.sc_card_ref = this->sc_card_ref; + cmd.state.assist_card_ref2 = this->card_refs[6]; + cmd.state.assist_card_set_number = (this->card_refs[6] == 0xFFFF) + ? 0 : this->assist_card_set_number; + cmd.state.assist_card_id = this->set_assist_card_id; + cmd.state.assist_remaining_turns = this->assist_remaining_turns; + cmd.state.assist_delay_turns = this->assist_delay_turns; + cmd.state.atk_bonuses = this->atk_bonuses; + cmd.state.def_bonuses = this->def_bonuses; + if (always_send || memcmp(&this->hand_and_equip, &cmd.state, sizeof(this->hand_and_equip))) { + *this->hand_and_equip = cmd.state; + this->server()->send(cmd); + } + this->send_6xB4x04_if_needed(always_send); +} + +void PlayerState::set_random_assist_card_from_hand_for_free() { + vector candidate_card_refs; + for (size_t hand_index = 0; hand_index < 6; hand_index++) { + uint16_t card_ref = this->card_refs[hand_index]; + auto ce = this->server()->definition_for_card_ref(card_ref); + if (ce && (ce->def.type == CardType::ASSIST) && + (assist_effect_number_for_card_id(ce->def.card_id) != AssistEffect::SQUEEZE)) { + candidate_card_refs.emplace_back(card_ref); + } + } + + if (!candidate_card_refs.empty()) { + this->discard_set_assist_card(); + size_t index = this->server()->get_random(candidate_card_refs.size()); + this->set_card_from_hand( + candidate_card_refs[index], 15, nullptr, this->client_id, 1); + } +} + +void PlayerState::send_6xB4x04_if_needed(bool always_send) { + G_UpdateStats_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 + // cleared them at construction time; instead, we just clear the fields that + // won't be overwritten and aren't initialized to zero already. + for (size_t z = 0; z < 0x10; z++) { + cmd.card_statuses[z].unused1 = 0; + } + + if (!this->sc_card) { + cmd.card_statuses[0].card_ref = 0xFFFF; + } else { + cmd.card_statuses[0] = this->sc_card->get_short_status(); + } + + for (size_t hand_index = 0; hand_index < 6; hand_index++) { + this->get_short_status_for_card_index_in_hand( + hand_index + 1, &cmd.card_statuses[hand_index + 1]); + // This write is required to mimic memset()'s effect from the original code. + // This field is probably ignored for hand refs anyway, but we might as well + // be as consistent as possible. + cmd.card_statuses[hand_index + 1].unused1 = 0; + } + + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = this->set_cards[set_index]; + if (!card) { + cmd.card_statuses[set_index + 7].card_ref = 0xFFFF; + } else { + cmd.card_statuses[set_index + 7] = card->get_short_status(); + } + } + + cmd.card_statuses[15].card_ref = this->card_refs[6]; + + if (always_send || (cmd.card_statuses != *this->card_short_statuses)) { + *this->card_short_statuses = cmd.card_statuses; + if (!this->server()->get_should_copy_prev_states_to_current_states()) { + this->server()->send(cmd); + } + } +} + +vector PlayerState::get_card_refs_within_range_from_all_players( + const parray& range, + const Location& loc, + CardType type) const { + vector ret; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto other_ps = this->server()->player_states[client_id]; + if (other_ps && ((other_ps->get_sc_card_type() == type) || (type == CardType::ITEM))) { + auto card_refs = get_card_refs_within_range(range, loc, *other_ps->card_short_statuses); + ret.insert(ret.end(), card_refs.begin(), card_refs.end()); + } + } + return ret; +} + +void PlayerState::unknown_80239460() { + if (this->sc_card) { + this->sc_card->unknown_80235AA0(); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + if (this->set_cards[set_index]) { + this->set_cards[set_index]->unknown_80235AA0(); + } + } +} + +void PlayerState::unknown_802394C4() { + if (this->sc_card) { + this->sc_card->unknown_80235AD4(); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + if (this->set_cards[set_index]) { + this->set_cards[set_index]->unknown_80235AD4(); + } + } +} + +void PlayerState::unknown_80239528() { + if (this->sc_card) { + this->sc_card->unknown_80235B10(); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + if (this->set_cards[set_index]) { + this->set_cards[set_index]->unknown_80235B10(); + } + } +} + +void PlayerState::handle_before_turn_assist_effects() { + if ((this->assist_delay_turns > 0) && + (--this->assist_delay_turns == 0)) { + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); + for (size_t z = 0; z < num_assists; z++) { + switch (this->server()->assist_server->get_active_assist_by_index(z)) { + case AssistEffect::BOMB: + this->server()->execute_bomb_assist_effect(); + break; + case AssistEffect::ATK_DICE_2: + // Note: This behavior doesn't match the card description. Is it + // supposed to add 2 or multiply by 2? + this->atk_points = min(this->atk_points + 2, 9); + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + break; + case AssistEffect::SKIP_TURN: + this->assist_flags |= 0x80; + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + break; + default: + break; + } + } + } +} + +int16_t PlayerState::get_assist_turns_remaining() { + if ((this->card_refs[6] == 0xFFFF) && (this->set_assist_card_id == 0xFFFF)) { + return -1; + } + return this->assist_remaining_turns; +} + +bool PlayerState::set_action_cards_for_action_state(const ActionState& pa) { + auto attacker_card = this->server()->card_for_set_card_ref(pa.attacker_card_ref); + if (attacker_card) { + attacker_card->card_flags |= 0x100; + } + + auto action_type = this->server()->ruler_server->get_pending_action_type(pa); + this->subtract_or_check_atk_or_def_points_for_action(pa, 1); + + if (action_type == ActionType::ATTACK) { + G_Unknown_GC_Ep3_6xB4x4A cmd; + cmd.client_id = this->client_id; + cmd.round_num = this->server()->get_round_num(); + cmd.entry_count = 0; + auto card = this->server()->card_for_set_card_ref(pa.attacker_card_ref); + if (card) { + card->loc.direction = pa.facing_direction; + size_t z = 0; + do { + card->unknown_80237A90(pa, pa.action_card_refs[z]); + card->unknown_802379BC(pa.action_card_refs[z]); + if (pa.action_card_refs[z] != 0xFFFF) { + cmd.card_refs[z] = pa.action_card_refs[z]; + cmd.entry_count++; + } + auto ce = this->server()->definition_for_card_ref(pa.action_card_refs[z]); + if (ce) { + auto card_class = ce->def.card_class(); + if ((card_class == CardClass::TECH) || + (card_class == CardClass::PHOTON_BLAST) || + (card_class == CardClass::BOSS_TECH)) { + this->stats.num_tech_cards_set++; + } + if ((card_class == CardClass::ATTACK_ACTION) || + (card_class == CardClass::CONNECT_ONLY_ATTACK_ACTION) || + (card_class == CardClass::BOSS_ATTACK_ACTION)) { + this->stats.num_attack_actions_set++; + } + this->stats.num_cards_set++; + } + z++; + } while ((z < 8) && (pa.action_card_refs[z] != 0xFFFF)); + if (cmd.entry_count > 0) { + this->server()->send(cmd); + } + } + + } else if (action_type == ActionType::DEFENSE) { + G_Unknown_GC_Ep3_6xB4x4A cmd; + cmd.client_id = this->client_id; + cmd.round_num = this->server()->get_round_num(); + for (size_t z = 0; (z < 4 * 9) && (pa.target_card_refs[z] != 0xFFFF); z++) { + auto target_card = this->server()->card_for_set_card_ref(pa.target_card_refs[z]); + if (target_card) { + target_card->unknown_802379DC(pa); + if (this->client_id == target_card->get_client_id()) { + this->stats.defense_actions_set_on_self++; + } else { + this->stats.defense_actions_set_on_ally++; + } + this->stats.num_cards_set++; + } + } + cmd.card_refs[0] = pa.defense_card_ref; + cmd.entry_count = 1; + this->server()->send(cmd); + } + for (size_t z = 0; (z < 4 * 9) && (pa.action_card_refs[z] != 0xFFFF); z++) { + this->discard_ref_from_hand(pa.action_card_refs[z]); + } + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + return true; +} + +void PlayerState::unknown_8023C174() { + if (this->sc_card) { + this->sc_card->unknown_8023813C(); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + if (this->set_cards[set_index]) { + this->set_cards[set_index]->unknown_8023813C(); + } + } + + this->compute_total_set_cards_cost(); + this->unknown_a14 = 0; + if ((this->assist_remaining_turns > 0) && + (this->assist_remaining_turns < 90) && + (this->assist_delay_turns == 0)) { + this->assist_remaining_turns--; + if (this->assist_remaining_turns < 1) { + this->discard_set_assist_card(); + } + } + if (this->is_team_turn()) { + this->atk_points = 0; + this->def_points = 0; + this->atk_bonuses = 0; + this->def_bonuses = 0; + this->roll_dice(2); + } + this->assist_flags &= 0x9804; + this->set_assist_flags_from_assist_effects(); + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(0); + this->send_set_card_updates(0); +} + +void PlayerState::handle_homesick_assist_effect(shared_ptr card) { + if (!card) { + return; + } + + size_t set_index; + for (set_index = 0; set_index < 8; set_index++) { + if (this->set_cards[set_index] == card) { + break; + } + } + + if (set_index < 8) { + uint16_t card_ref = card->get_card_ref(); + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); + for (size_t z = 0; z < num_assists; z++) { + if (this->server()->assist_server->get_active_assist_by_index(z) == AssistEffect::HOMESICK) { + this->return_set_card_to_hand2(card_ref); + this->log_discard(card_ref, 1); + this->set_cards[set_index]->card_flags |= 2; + return; + } + } + + if (this->deck_state->set_card_ref_drawable_next(card_ref)) { + this->log_discard(card_ref, 1); + this->set_cards[set_index]->card_flags |= 2; + } + } +} + +void PlayerState::apply_main_die_assist_effects(uint8_t* die_value) const { + size_t num_assists = this->server()->assist_server->compute_num_assist_effects_for_client(this->client_id); + for (size_t z = 0; z < num_assists; z++) { + switch (this->server()->assist_server->get_active_assist_by_index(z)) { + case AssistEffect::DICE_FEVER: + *die_value = 5; + break; + case AssistEffect::DICE_HALF: + *die_value = ((*die_value + 1) >> 1); + break; + case AssistEffect::DICE_PLUS_1: + (*die_value)++; + break; + case AssistEffect::DICE_FEVER_PLUS: + *die_value = 6; + break; + default: + break; + } + } +} + +void PlayerState::roll_main_dice() { + const auto& rules = this->server()->base()->map_and_rules1->rules; + + uint8_t min_dice = rules.min_dice; + uint8_t max_dice = rules.max_dice; + if (min_dice == 0) { + min_dice = 1; + } + if (max_dice == 0) { + max_dice = 6; + } + + if (max_dice < min_dice) { + uint8_t t = max_dice; + max_dice = min_dice; + min_dice = t; + } + + uint8_t dice_range_width = (max_dice - min_dice) + 1; + if (dice_range_width < 2) { + this->dice_results[0] = min_dice; + this->dice_results[1] = min_dice; + this->atk_points = min_dice; + this->def_points = min_dice; + } else { + this->dice_results[0] = min_dice + this->server()->get_random(dice_range_width); + this->dice_results[1] = min_dice + this->server()->get_random(dice_range_width); + this->atk_points = this->dice_results[0]; + this->def_points = this->dice_results[1]; + } + + bool should_exchange = false; + if (rules.dice_exchange_mode == DiceExchangeMode::HIGH_DEF) { + should_exchange = (this->dice_results[0] > this->dice_results[1]); + } else if (rules.dice_exchange_mode == DiceExchangeMode::HIGH_ATK) { + should_exchange = (this->dice_results[0] < this->dice_results[1]); + } + + if (!should_exchange) { + this->atk_points = (short)(char)this->dice_results[0]; + this->def_points = (short)(char)this->dice_results[1]; + this->assist_flags = this->assist_flags & 0xFFFFFFFD; + } else { + this->atk_points = (short)(char)this->dice_results[1]; + this->def_points = (short)(char)this->dice_results[0]; + this->assist_flags = this->assist_flags | 2; + } + + this->atk_points = this->atk_points + this->server()->card_special->client_has_atk_dice_boost_condition(this->client_id); + + uint8_t atk_before_bonuses = this->atk_points; + uint8_t def_before_bonuses = this->def_points; + + this->apply_main_die_assist_effects(&this->atk_points); + this->apply_main_die_assist_effects(&this->def_points); + this->dice_results[0] = this->atk_points; + this->dice_results[1] = this->def_points; + + this->atk_points += this->server()->team_dice_boost[this->team_id]; + this->def_points += this->server()->team_dice_boost[this->team_id]; + this->atk_points = clamp(this->atk_points, 1, 9); + this->def_points = clamp(this->def_points, 1, 9); + this->atk_bonuses = this->atk_points - atk_before_bonuses; + this->def_bonuses = this->def_points - def_before_bonuses; + this->atk_points2 = min(this->atk_points2_max, this->atk_points); + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); +} + +void PlayerState::unknown_8023C110() { + if (this->sc_card) { + this->sc_card->unknown_802380C0(); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = this->set_cards[set_index]; + if (card) { + card->unknown_802380C0(); + } + } +} + +void PlayerState::compute_team_dice_boost_after_draw_phase() { + if (this->sc_card) { + this->sc_card->unknown_80237F88(); + } + + for (size_t set_index = 0; set_index < 8; set_index++) { + if (this->set_cards[set_index]) { + this->set_cards[set_index]->unknown_80237F88(); + } + } + + uint8_t current_team_turn = this->server()->get_current_team_turn(); + uint8_t dice_boost = this->server()->get_team_exp(current_team_turn) / + (this->server()->team_client_count[current_team_turn] * 12); + this->server()->card_special->adjust_dice_boost_if_team_has_condition_52( + current_team_turn, &dice_boost, 0); + this->server()->team_dice_boost[current_team_turn] = clamp(dice_boost, 0, 8); + this->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); +} + + + +} // namespace Episode3 diff --git a/src/Episode3/PlayerState.hh b/src/Episode3/PlayerState.hh new file mode 100644 index 00000000..e92c6b7e --- /dev/null +++ b/src/Episode3/PlayerState.hh @@ -0,0 +1,193 @@ +#pragma once + +#include + +#include + +#include "../Text.hh" +#include "DataIndex.hh" +#include "Card.hh" +#include "DeckState.hh" +#include "PlayerStateSubordinates.hh" + +namespace Episode3 { + + + +class ServerBase; +class Server; + +class PlayerState : public std::enable_shared_from_this { +public: + PlayerState(uint8_t client_id, std::shared_ptr server); + void init(); + std::shared_ptr server(); + std::shared_ptr server() const; + + bool draw_cards_allowed() const; + void apply_assist_card_effect_on_set( + std::shared_ptr setter_ps); + void apply_dice_effects(); + uint16_t card_ref_for_hand_index(size_t hand_index) const; + int16_t compute_attack_or_defense_atk_costs(const ActionState& pa) const; + void compute_total_set_cards_cost(); + size_t count_set_cards() const; + size_t count_set_refs() const; + void discard_all_assist_cards_from_hand(); + void discard_all_attack_action_cards_from_hand(); + void discard_all_item_and_creature_cards_from_hand(); + void discard_and_redraw_hand(); + bool discard_card_or_add_to_draw_pile( + uint16_t card_ref, bool add_to_draw_pile); + void discard_random_hand_card(); + bool discard_ref_from_hand(uint16_t card_ref); + void discard_set_assist_card(); + bool do_mulligan(); + void draw_hand(ssize_t override_count = 0); + void draw_initial_hand(); + int32_t error_code_for_client_setting_card( + uint16_t card_ref, + uint8_t card_index, + const Location* loc, + uint8_t assist_target_client_id) const; + std::vector get_all_cards_within_range( + const parray& range, + const Location& loc, + uint8_t target_team_id) const; + uint8_t get_atk_points() const; + void get_short_status_for_card_index_in_hand( + size_t hand_index, CardShortStatus* stat) const; + std::shared_ptr get_deck(); + uint8_t get_def_points() const; + uint8_t get_dice_result(size_t which) const; + size_t get_hand_size() const; + uint16_t get_sc_card_id() const; + std::shared_ptr get_sc_card(); + std::shared_ptr get_sc_card() const; + uint16_t get_sc_card_ref() const; + CardType get_sc_card_type() const; + std::shared_ptr get_set_card(size_t set_index); + std::shared_ptr get_set_card(size_t set_index) const; + uint16_t get_set_ref(size_t set_index) const; + uint8_t get_team_id() const; + ssize_t hand_index_for_card_ref(uint16_t card_ref) const; + size_t set_index_for_card_ref(uint16_t card_ref) const; + bool is_mulligan_allowed() const; + bool is_team_turn() const; + void log_discard(uint16_t card_ref, uint16_t reason); + bool move_card_to_location_by_card_index( + size_t card_index, const Location& new_loc); + void move_null_hand_refs_to_end(); + void on_cards_destroyed(); + void replace_all_set_assists_with_random_assists(); + bool replace_assist_card_by_id(uint16_t card_id); + bool return_set_card_to_hand2(uint16_t card_ref); + bool return_set_card_to_hand1(uint16_t card_ref); + uint8_t roll_dice(size_t num_dice); + uint8_t roll_dice_with_effects(size_t num_dice); + void send_set_card_updates(bool always_send = false); + void set_assist_flags_from_assist_effects(); + bool set_card_from_hand( + uint16_t card_ref, + uint8_t card_index, + const Location* loc, + uint8_t assist_target_client_id, + bool skip_error_checks_and_atk_sub); + void set_initial_location(); + void set_map_occupied_bit_for_card_on_warp_tile( + std::shared_ptr card); + void set_map_occupied_bits_for_sc_and_creatures(); + void subtract_def_points(uint8_t cost); + bool subtract_or_check_atk_or_def_points_for_action( + const ActionState& pa, bool deduct_points); + void subtract_atk_points(uint8_t cost); + void update_hand_and_equip_state_and_send_6xB4x02_if_needed( + bool always_send = false); + void set_random_assist_card_from_hand_for_free(); + void send_6xB4x04_if_needed(bool always_send = false); + std::vector get_card_refs_within_range_from_all_players( + const parray& range, + const Location& loc, + CardType type) const; + void unknown_80239460(); + void unknown_802394C4(); + void unknown_80239528(); + void handle_before_turn_assist_effects(); + int16_t get_assist_turns_remaining(); + bool set_action_cards_for_action_state(const ActionState& pa); + void unknown_8023C174(); + void handle_homesick_assist_effect(std::shared_ptr card); + void apply_main_die_assist_effects(uint8_t* die_value) const; + void roll_main_dice(); + void unknown_8023C110(); + void compute_team_dice_boost_after_draw_phase(); + +private: + std::weak_ptr w_server; + +public: + std::shared_ptr sc_card; + std::shared_ptr set_cards[8]; + uint8_t client_id; + uint16_t num_mulligans_allowed; + CardType sc_card_type; + uint8_t team_id; + uint8_t atk_points; + uint8_t def_points; + uint8_t atk_points2; + uint8_t atk_points2_max; + uint8_t atk_bonuses; + uint8_t def_bonuses; + parray dice_results; + uint8_t unknown_a4; + uint8_t dice_max; + uint8_t total_set_cards_cost; + uint16_t sc_card_id; + uint16_t sc_card_ref; + + // This array is unfortunately heterogeneous; specifically: + // [0] through [5] are hand refs + // [6] is the current assist card ref (which may belong to another player) + // [7] is the previous assist card ref + // [8] through [15] are set refs + parray card_refs; + + std::shared_ptr deck_state; + parray discard_log_card_refs; + parray discard_log_reasons; + uint8_t assist_remaining_turns; + uint16_t assist_card_set_number; + uint16_t set_assist_card_id; + bool god_whim_can_use_hidden_cards; + ActionChainWithConds unknown_a12; + ActionMetadata unknown_a13; + uint32_t unknown_a14; + uint32_t assist_flags; + uint8_t assist_delay_turns; + Direction start_facing_direction; + std::shared_ptr hand_and_equip; + + // Like card_refs above, these arrays are also heterogeneous, but the indices + // are not the same as for card_refs! THe indices here are: + // [0] is the SC card status + // [1] through [6] are hand cards + // [7] through [14] are set cards + // [15] is the assist card + std::shared_ptr> card_short_statuses; + parray prev_card_short_statuses; + + // In these arrays, [0] is the SC card and the rest are the set cards. + std::shared_ptr> set_card_action_chains; + std::shared_ptr> set_card_action_metadatas; + parray prev_set_card_action_chains; + parray prev_set_card_action_metadatas; + + uint32_t num_destroyed_fcs; + uint8_t unknown_a16; + uint8_t unknown_a17; + PlayerStats stats; +}; + + + +} // namespace Episode3 diff --git a/src/Episode3/PlayerStateSubordinates.cc b/src/Episode3/PlayerStateSubordinates.cc new file mode 100644 index 00000000..45d867cc --- /dev/null +++ b/src/Episode3/PlayerStateSubordinates.cc @@ -0,0 +1,547 @@ +#include "PlayerState.hh" + +#include "Server.hh" + +using namespace std; + +namespace Episode3 { + + + +Condition::Condition() { + this->clear(); +} + +void Condition::clear() { + this->type = ConditionType::NONE; + this->remaining_turns = 0; + this->a_arg_value = 0; + this->dice_roll_value = 0; + this->flags = 0; + this->card_definition_effect_index = 0; + this->card_ref = 0xFFFF; + this->value = 0; + this->condition_giver_card_ref = 0xFFFF; + this->random_percent = 0; + this->value8 = 0; + this->order = 0; + this->unknown_a8 = 0; +} + +void Condition::clear_FF() { + this->type = ConditionType::INVALID_FF; + this->remaining_turns = 0xFF; + this->a_arg_value = -1; + this->dice_roll_value = 0xFF; + this->flags = 0xFF; + this->card_definition_effect_index = 0xFF; + this->card_ref = 0xFFFF; + this->value = -1; + this->condition_giver_card_ref = 0xFFFF; + this->random_percent = 0xFF; + this->value8 = -1; + this->order = 0xFF; + this->unknown_a8 = 0xFF; +} + + + +EffectResult::EffectResult() { + this->clear(); +} + +void EffectResult::clear() { + this->attacker_card_ref = 0xFFFF; + this->target_card_ref = 0xFFFF; + this->value = 0; + this->current_hp = 0; + this->ap = 0; + this->tp = 0; + this->flags = 0; + this->operation = 0; + this->condition_index = 0; + this->dice_roll_value = 0; +} + + + +CardShortStatus::CardShortStatus() { + this->clear(); +} + +void CardShortStatus::clear() { + this->card_ref = 0xFFFF; + this->current_hp = 0; + this->card_flags = 0; + this->loc.clear(); + this->unused1 = 0xFFFF; + this->max_hp = 0; + this->unused2 = 0; +} + +void CardShortStatus::clear_FF() { + this->card_ref = 0xFFFF; + this->current_hp = 0xFFFF; + this->card_flags = 0xFFFFFFFF; + this->loc.clear_FF(); + this->unused1 = 0xFFFF; + this->max_hp = -1; + this->unused2 = 0xFF; +} + + + +ActionState::ActionState() { + this->clear(); +} + +void ActionState::clear() { + this->client_id = 0xFFFF; + this->unused = 0; + this->facing_direction = Direction::RIGHT; + this->attacker_card_ref = 0xFFFF; + this->defense_card_ref = 0xFFFF; + this->original_attacker_card_ref = 0xFFFF; + this->target_card_refs.clear(0xFFFF); + this->action_card_refs.clear(0xFFFF); +} + + + +ActionChain::ActionChain() { + this->clear(); +} + +void ActionChain::clear() { + this->effective_ap = 0; + this->effective_tp = 0; + this->ap_effect_bonus = 0; + this->damage = 0; + this->acting_card_ref = 0xFFFF; + this->unknown_card_ref_a3 = 0xFFFF; + this->attack_action_card_ref_count = 0; + this->attack_medium = AttackMedium::UNKNOWN; + this->target_card_ref_count = 0; + this->action_subphase = ActionSubphase::INVALID_FF; + this->strike_count = 1; + this->damage_multiplier = 1; + this->attack_number = 0xFF; + this->tp_effect_bonus = 0; + this->unused1 = 0; + this->unused2 = 0; + this->card_ap = 0; + this->card_tp = 0; + this->flags = 0; + this->attack_action_card_refs.clear(0xFFFF); + this->target_card_refs.clear(0xFFFF); +} + +void ActionChain::clear_FF() { + this->effective_ap = -1; + this->effective_tp = -1; + this->ap_effect_bonus = -1; + this->damage = -1; + this->acting_card_ref = 0xFFFF; + this->unknown_card_ref_a3 = 0xFFFF; + this->attack_action_card_refs.clear(0xFFFF); + this->attack_action_card_ref_count = 0xFF; + this->attack_medium = AttackMedium::INVALID_FF; + this->target_card_ref_count = 0xFF; + this->action_subphase = ActionSubphase::INVALID_FF; + this->strike_count = 0xFF; + this->damage_multiplier = -1; + this->attack_number = 0xFF; + this->tp_effect_bonus = -1; + this->unused1 = 0xFF; + this->unused2 = 0xFF; + this->card_ap = -1; + this->card_tp = -1; + this->flags = 0xFFFFFFFF; + this->target_card_refs.clear(0xFFFF); +} + + + +ActionChainWithConds::ActionChainWithConds() { + this->clear(); +} + +void ActionChainWithConds::clear() { + this->chain.effective_ap = 0; + this->chain.effective_tp = 0; + this->chain.ap_effect_bonus = 0; + this->chain.damage = 0; + this->clear_inner(); +} + +void ActionChainWithConds::clear_FF() { + this->chain.clear_FF(); + for (size_t z = 0; z < 9; z++) { + this->conditions[z].clear_FF(); + } +} + +void ActionChainWithConds::clear_inner() { + this->chain.unknown_card_ref_a3 = 0xFFFF; + this->chain.acting_card_ref = 0xFFFF; + this->chain.attack_medium = AttackMedium::INVALID_FF; + this->chain.flags = 0; + this->chain.action_subphase = ActionSubphase::INVALID_FF; + this->chain.attack_number = 0xFF; + this->reset(); + this->clear_target_card_refs(); + this->chain.attack_action_card_ref_count = 0; + this->chain.attack_action_card_refs.clear(0xFFFF); +} + +void ActionChainWithConds::clear_target_card_refs() { + this->chain.target_card_ref_count = 0; + this->chain.target_card_refs.clear(0xFFFF); +} + +void ActionChainWithConds::reset() { + this->chain.effective_ap = 0; + this->chain.effective_tp = 0; + this->chain.ap_effect_bonus = 0; + this->chain.tp_effect_bonus = 0; + this->chain.unused1 = 0; + this->chain.unused2 = 0; + this->chain.damage = 0; + this->chain.strike_count = 1; + this->chain.damage_multiplier = 1; +} + +bool ActionChainWithConds::check_flag(uint32_t flags) const { + return (this->chain.flags & flags) != 0; +} + +void ActionChainWithConds::clear_flags(uint32_t flags) { + this->chain.flags &= ~flags; +} + +void ActionChainWithConds::set_flags(uint32_t flags) { + this->chain.flags |= flags; +} + +void ActionChainWithConds::add_attack_action_card_ref( + uint16_t card_ref, shared_ptr server) { + if (card_ref != 0xFFFF) { + this->chain.attack_action_card_refs[this->chain.attack_action_card_ref_count++] = card_ref; + } + this->set_flags(8); + this->chain.action_subphase = server->get_current_action_subphase(); +} + +void ActionChainWithConds::add_target_card_ref(uint16_t card_ref) { + if (card_ref != 0xFFFF && + this->chain.target_card_ref_count < this->chain.target_card_refs.size()) { + this->chain.target_card_refs[this->chain.target_card_ref_count++] = card_ref; + } +} + +void ActionChainWithConds::compute_attack_medium(shared_ptr server) { + this->chain.attack_medium = AttackMedium::PHYSICAL; + for (size_t z = 0; z < this->chain.attack_action_card_ref_count; z++) { + uint16_t card_ref = this->chain.attack_action_card_refs[z]; + if (card_ref == 0xFFFF) { + break; + } + auto ce = server->definition_for_card_ref(card_ref); + if (!ce) { + continue; + } + if (card_class_is_tech_like(ce->def.card_class())) { + this->chain.attack_medium = AttackMedium::TECH; + } + } +} + +bool ActionChainWithConds::get_condition_value( + ConditionType cond_type, + uint16_t card_ref, + uint8_t def_effect_index, + uint16_t value, + uint16_t* out_value) const { + bool any_found = false; + uint8_t max_order = 10; + for (size_t z = 0; z < 9; z++) { + auto& cond = this->conditions[z]; + if (((cond_type == ConditionType::ANY) || (cond.type == cond_type)) && + ((def_effect_index == 0xFF) || (cond.card_definition_effect_index == def_effect_index)) && + ((card_ref == 0xFFFF) || (cond.card_ref == card_ref)) && + ((value == 0xFFFF) || (cond.value == value))) { + if (!any_found || (max_order < cond.order)) { + if (!out_value) { + return true; + } + *out_value = cond.value; + max_order = cond.order; + } + any_found = true; + } + } + return any_found; +} + +void ActionChainWithConds::set_action_subphase_from_card( + shared_ptr card) { + this->chain.action_subphase = card->server()->get_current_action_subphase(); +} + +bool ActionChainWithConds::unknown_8024DEC4() const { + return this->check_flag(4) ? false : (this->chain.target_card_ref_count != 0); +} + + + +ActionMetadata::ActionMetadata() { + this->clear(); +} + +void ActionMetadata::clear() { + this->card_ref = 0xFFFF; + this->target_card_ref_count = 0; + this->defense_card_ref_count = 0; + this->action_subphase = ActionSubphase::INVALID_FF; + this->defense_power = 0; + this->defense_bonus = 0; + this->attack_bonus = 0; + this->flags = 0; + this->target_card_refs.clear(0xFFFF); + this->defense_card_refs.clear(0xFFFF); + this->original_attacker_card_refs.clear(0xFFFF); +} + +void ActionMetadata::clear_FF() { + this->card_ref = 0xFFFF; + this->target_card_ref_count = 0xFF; + this->defense_card_ref_count = 0xFF; + this->action_subphase = ActionSubphase::INVALID_FF; + this->defense_power = -1; + this->defense_bonus = -1; + this->attack_bonus = -1; + this->flags = 0xFFFFFFFF; + this->target_card_refs.clear(0xFFFF); + this->defense_card_refs.clear(0xFFFF); + this->original_attacker_card_refs.clear(0xFFFF); +} + +bool ActionMetadata::check_flag(uint32_t mask) const { + return (this->flags & mask) != 0; +} + +void ActionMetadata::set_flags(uint32_t flags) { + this->flags |= flags; +} + +void ActionMetadata::clear_flags(uint32_t flags) { + this->flags &= ~flags; +} + +void ActionMetadata::clear_defense_and_attacker_card_refs() { + this->defense_card_ref_count = 0; + this->defense_card_refs.clear(0xFFFF); + this->original_attacker_card_refs.clear(0xFFFF); +} + +void ActionMetadata::clear_target_card_refs() { + this->target_card_ref_count = 0; + this->target_card_refs.clear(0xFFFF); +} + +void ActionMetadata::add_target_card_ref(uint16_t card_ref) { + if (card_ref != 0xFFFF && + this->target_card_ref_count < this->target_card_refs.size()) { + this->target_card_refs[this->target_card_ref_count++] = card_ref; + } +} + +void ActionMetadata::add_defense_card_ref( + uint16_t defense_card_ref, + shared_ptr card, + uint16_t original_attacker_card_ref) { + if ((defense_card_ref != 0xFFFF) && (this->defense_card_ref_count < 8)) { + this->defense_card_refs[this->defense_card_ref_count] = defense_card_ref; + this->original_attacker_card_refs[this->defense_card_ref_count] = original_attacker_card_ref; + this->defense_card_ref_count++; + this->action_subphase = card->server()->get_current_action_subphase(); + } +} + + + +HandAndEquipState::HandAndEquipState() { + this->clear(); +} + +void HandAndEquipState::clear() { + this->dice_results.clear(0); + this->atk_points = 0; + this->def_points = 0; + this->atk_points2 = 0; + this->unknown_a1 = 0; + this->total_set_cards_cost = 0; + this->is_cpu_player = 0; + this->assist_flags = 0; + this->hand_card_refs.clear(0xFFFF); + this->assist_card_ref = 0xFFFF; + this->set_card_refs.clear(0xFFFF); + this->sc_card_ref = 0xFFFF; + this->hand_card_refs2.clear(0xFFFF); + this->set_card_refs2.clear(0xFFFF); + this->assist_card_ref2 = 0xFFFF; + this->assist_card_set_number = 0; + this->assist_card_id = 0xFFFF; + this->assist_remaining_turns = 0; + this->assist_delay_turns = 0; + this->atk_bonuses = 0; + this->def_bonuses = 0; + this->unused2.clear(0); +} + +void HandAndEquipState::clear_FF() { + this->dice_results.clear(0xFF); + this->atk_points = 0xFF; + this->def_points = 0xFF; + this->atk_points2 = 0xFF; + this->unknown_a1 = 0xFF; + this->total_set_cards_cost = 0xFF; + this->is_cpu_player = 0xFF; + this->assist_flags = 0xFFFFFFFF; + this->hand_card_refs.clear(0xFFFF); + this->assist_card_ref = 0xFFFF; + this->set_card_refs.clear(0xFFFF); + this->sc_card_ref = 0xFFFF; + this->hand_card_refs2.clear(0xFFFF); + this->set_card_refs2.clear(0xFFFF); + this->assist_card_ref2 = 0xFFFF; + this->assist_card_set_number = 0xFFFF; + this->assist_card_id = 0xFFFF; + this->assist_remaining_turns = 0xFF; + this->assist_delay_turns = 0xFF; + this->atk_bonuses = 0xFF; + this->def_bonuses = 0xFF; + this->unused2.clear(0xFF); +} + + + +PlayerStats::PlayerStats() { + this->clear(); +} + +void PlayerStats::clear() { + this->damage_given = 0; + this->damage_taken = 0; + this->num_opponent_cards_destroyed = 0; + this->num_owned_cards_destroyed = 0; + this->total_move_distance = 0; + this->num_cards_set = 0; + this->num_item_or_creature_cards_set = 0; + this->num_attack_actions_set = 0; + this->num_tech_cards_set = 0; + this->num_assist_cards_set = 0; + this->defense_actions_set_on_self = 0; + this->defense_actions_set_on_ally = 0; + this->num_cards_drawn = 0; + this->max_attack_damage = 0; + this->max_attack_combo_size = 0; + this->num_attacks_given = 0; + this->num_attacks_taken = 0; + this->sc_damage_taken = 0; + this->action_card_negated_damage = 0; + this->unused = 0; +} + +float PlayerStats::score(size_t num_rounds) const { + // Note: This formula doesn't match the formula on PSO-World, which is: + // 35 + // + (Attack Damage - Damage Taken) + // + (Max Card Combo x 3) + // - (Story Character Damage x 1.8) + // - (Turns x 2.7) + // + (Action Card Negated Damage x 0.8) + // I don't know where that formula came from, but this one came from the USA + // Ep3 PsoV3.dol, so it's presumably correct. Is the PSO-World formula simply + // incorrect, or is it from e.g. the Japanese version, which may have a + // different rank calculation function? + return 38.0f + + 0.8f * this->action_card_negated_damage + - 2.3f * num_rounds + - 1.8f * this->sc_damage_taken + + 3.0f * this->max_attack_combo_size + + (this->damage_given - this->damage_taken); +} + +uint8_t PlayerStats::rank(size_t num_rounds) const { + return this->rank_for_score(this->score(num_rounds)); +} + +const char* PlayerStats::rank_name(size_t num_rounds) const { + return this->name_for_rank(this->rank_for_score(this->score(num_rounds))); +} + +constexpr size_t RANK_THRESHOLD_COUNT = 9; +static const float RANK_THRESHOLDS[RANK_THRESHOLD_COUNT] = { + 15.0f, 25.0f, 30.0f, 40.0f, 50.0f, 60.0f, 65.0f, 75.0f, 85.0f}; +static const char* RANK_NAMES[RANK_THRESHOLD_COUNT + 1] = { + "E", "D", "D+", "C", "C+", "B", "B+", "A", "A+", "S"}; + +uint8_t PlayerStats::rank_for_score(float score) { + size_t rank = 0; + while (rank < RANK_THRESHOLD_COUNT && RANK_THRESHOLDS[rank] <= score) { + rank++; + } + return rank; +} + +const char* PlayerStats::name_for_rank(uint8_t rank) { + if (rank >= RANK_THRESHOLD_COUNT + 1) { + throw invalid_argument("invalid rank"); + } + return RANK_NAMES[rank]; +} + + + + +bool is_card_within_range( + const parray& range, + const Location& anchor_loc, + const CardShortStatus& ss) { + if (ss.card_ref == 0xFFFF) { + return false; + } + if (range[0] == 2) { + return true; + } + + if ((ss.loc.x < anchor_loc.x - 4) || (ss.loc.x > anchor_loc.x + 4)) { + return false; + } + if ((ss.loc.y < anchor_loc.y - 4) || (ss.loc.y > anchor_loc.y + 4)) { + return false; + } + return (range[(ss.loc.x - anchor_loc.x) + ((ss.loc.y - anchor_loc.y) + 4) * 9 + 4] != 0); +} + +vector get_card_refs_within_range( + const parray& range, + const Location& loc, + const parray& short_statuses) { + vector ret; + if (is_card_within_range(range, loc, short_statuses[0])) { + ret.emplace_back(short_statuses[0].card_ref); + } + for (size_t card_index = 7; card_index < 15; card_index++) { + const auto& ss = short_statuses[card_index]; + if (is_card_within_range(range, loc, ss)) { + ret.emplace_back(ss.card_ref); + } + } + return ret; +} + + + +} // namespace Episode3 diff --git a/src/Episode3/PlayerStateSubordinates.hh b/src/Episode3/PlayerStateSubordinates.hh new file mode 100644 index 00000000..a72c2209 --- /dev/null +++ b/src/Episode3/PlayerStateSubordinates.hh @@ -0,0 +1,265 @@ +#pragma once + +#include + +#include + +#include "../Text.hh" +#include "DataIndex.hh" + +namespace Episode3 { + + + +class ServerBase; +class Server; +class Card; + +struct Condition { + ConditionType type; + uint8_t remaining_turns; + int8_t a_arg_value; + uint8_t dice_roll_value; + uint8_t flags; + uint8_t card_definition_effect_index; + le_uint16_t card_ref; + le_int16_t value; + le_uint16_t condition_giver_card_ref; + uint8_t random_percent; + int8_t value8; + uint8_t order; + uint8_t unknown_a8; + + Condition(); + bool operator==(const Condition& other) const = default; + bool operator!=(const Condition& other) const = default; + + void clear(); + void clear_FF(); +} __attribute__((packed)); + +struct EffectResult { + le_uint16_t attacker_card_ref; + le_uint16_t target_card_ref; + int8_t value; + int8_t current_hp; + int8_t ap; + int8_t tp; + uint8_t flags; + int8_t operation; // May be a negative condition number + uint8_t condition_index; + uint8_t dice_roll_value; + + EffectResult(); + bool operator==(const EffectResult& other) const = default; + bool operator!=(const EffectResult& other) const = default; + + void clear(); +} __attribute__((packed)); + +struct CardShortStatus { + le_uint16_t card_ref; + le_uint16_t current_hp; + le_uint32_t card_flags; + Location loc; + le_uint16_t unused1; + int8_t max_hp; + uint8_t unused2; + + CardShortStatus(); + bool operator==(const CardShortStatus& other) const = default; + bool operator!=(const CardShortStatus& other) const = default; + + void clear(); + void clear_FF(); +} __attribute__((packed)); + +struct ActionState { + le_uint16_t client_id; + uint8_t unused; + Direction facing_direction; + le_uint16_t attacker_card_ref; + le_uint16_t defense_card_ref; + parray target_card_refs; + parray action_card_refs; + le_uint16_t original_attacker_card_ref; + + ActionState(); + bool operator==(const ActionState& other) const = default; + bool operator!=(const ActionState& other) const = default; + + void clear(); +} __attribute__((packed)); + +struct ActionChain { + int8_t effective_ap; + int8_t effective_tp; + int8_t ap_effect_bonus; + int8_t damage; + le_uint16_t acting_card_ref; + le_uint16_t unknown_card_ref_a3; + parray attack_action_card_refs; + uint8_t attack_action_card_ref_count; + AttackMedium attack_medium; + uint8_t target_card_ref_count; + ActionSubphase action_subphase; + uint8_t strike_count; + int8_t damage_multiplier; + uint8_t attack_number; + int8_t tp_effect_bonus; + uint8_t unused1; + uint8_t unused2; + int8_t card_ap; + int8_t card_tp; + le_uint32_t flags; + parray target_card_refs; + + ActionChain(); + bool operator==(const ActionChain& other) const = default; + bool operator!=(const ActionChain& other) const = default; + + void clear(); + void clear_FF(); +} __attribute__((packed)); + +struct ActionChainWithConds { + ActionChain chain; + parray conditions; + + ActionChainWithConds(); + bool operator==(const ActionChainWithConds& other) const = default; + bool operator!=(const ActionChainWithConds& other) const = default; + + void clear(); + void clear_FF(); + void clear_inner(); + void clear_target_card_refs(); + void reset(); + + bool check_flag(uint32_t flags) const; + void clear_flags(uint32_t flags); + void set_flags(uint32_t flags); + + void add_attack_action_card_ref(uint16_t card_ref, std::shared_ptr server); + void add_target_card_ref(uint16_t card_ref); + + void compute_attack_medium(std::shared_ptr server); + bool get_condition_value( + ConditionType cond_type, + uint16_t card_ref, + uint8_t def_effect_index, + uint16_t value, + uint16_t* out_value) const; + + void set_action_subphase_from_card(std::shared_ptr card); + bool unknown_8024DEC4() const; +} __attribute__((packed)); + +struct ActionMetadata { + le_uint16_t card_ref; + uint8_t target_card_ref_count; + uint8_t defense_card_ref_count; + ActionSubphase action_subphase; + int8_t defense_power; + int8_t defense_bonus; + int8_t attack_bonus; + le_uint32_t flags; + parray target_card_refs; + parray defense_card_refs; + parray original_attacker_card_refs; + + ActionMetadata(); + bool operator==(const ActionMetadata& other) const = default; + bool operator!=(const ActionMetadata& other) const = default; + + void clear(); + void clear_FF(); + + bool check_flag(uint32_t mask) const; + void set_flags(uint32_t flags); + void clear_flags(uint32_t flags); + + void clear_defense_and_attacker_card_refs(); + void clear_target_card_refs(); + void add_target_card_ref(uint16_t card_ref); + void add_defense_card_ref( + uint16_t defense_card_ref, + std::shared_ptr card, + uint16_t original_attacker_card_ref); +} __attribute__((packed)); + +struct HandAndEquipState { + parray dice_results; + uint8_t atk_points; + uint8_t def_points; + uint8_t atk_points2; // TODO: rename this to something more appropriate + uint8_t unknown_a1; + uint8_t total_set_cards_cost; + uint8_t is_cpu_player; + le_uint32_t assist_flags; + parray hand_card_refs; + le_uint16_t assist_card_ref; + parray set_card_refs; + le_uint16_t sc_card_ref; + parray hand_card_refs2; + parray set_card_refs2; + le_uint16_t assist_card_ref2; + le_uint16_t assist_card_set_number; + le_uint16_t assist_card_id; + uint8_t assist_remaining_turns; + uint8_t assist_delay_turns; + uint8_t atk_bonuses; + uint8_t def_bonuses; + parray unused2; + + HandAndEquipState(); + bool operator==(const HandAndEquipState& other) const = default; + bool operator!=(const HandAndEquipState& other) const = default; + + void clear(); + void clear_FF(); +} __attribute__((packed)); + +struct PlayerStats { + le_uint16_t damage_given; + le_uint16_t damage_taken; + le_uint16_t num_opponent_cards_destroyed; + le_uint16_t num_owned_cards_destroyed; + le_uint16_t total_move_distance; + le_uint16_t num_cards_set; + le_uint16_t num_item_or_creature_cards_set; + le_uint16_t num_attack_actions_set; + le_uint16_t num_tech_cards_set; + le_uint16_t num_assist_cards_set; + le_uint16_t defense_actions_set_on_self; + le_uint16_t defense_actions_set_on_ally; + le_uint16_t num_cards_drawn; + le_uint16_t max_attack_damage; + le_uint16_t max_attack_combo_size; + le_uint16_t num_attacks_given; + le_uint16_t num_attacks_taken; + le_uint16_t sc_damage_taken; + le_uint16_t action_card_negated_damage; + le_uint16_t unused; + + PlayerStats(); + void clear(); + + float score(size_t num_rounds) const; + uint8_t rank(size_t num_rounds) const; + const char* rank_name(size_t num_rounds) const; + + static uint8_t rank_for_score(float score); + static const char* name_for_rank(uint8_t rank); +} __attribute__((packed)); + + + +std::vector get_card_refs_within_range( + const parray& range, + const Location& loc, + const parray& short_statuses); + + + +} // namespace Episode3 diff --git a/src/Episode3/RulerServer.cc b/src/Episode3/RulerServer.cc new file mode 100644 index 00000000..35a53cfd --- /dev/null +++ b/src/Episode3/RulerServer.cc @@ -0,0 +1,2681 @@ +#include "RulerServer.hh" + +#include "Server.hh" + +using namespace std; + +namespace Episode3 { + + + +void compute_effective_range( + parray& ret, + shared_ptr data_index, + uint16_t card_id, + const Location& loc, + shared_ptr map_and_rules) { + ret.clear(0); + + parray range_def; + if (card_id == 0xFFFE) { + // Heavy Fog: one tile directly in front + range_def[3] = 0x00000100; + } else { + shared_ptr ce; + try { + ce = data_index->definition_for_card_id(card_id); + } catch (const out_of_range&) { + return; + } + for (size_t z = 0; z < 6; z++) { + range_def[z] = ce->def.range[z]; + } + } + + if (range_def[0] == 0x000FFFFF) { + // Entire field + ret.clear(2); + return; + } + + parray decoded_range; + for (size_t y = 0; y < 6; y++) { + uint32_t row = range_def[y]; + for (size_t x = 0; x < 5; x++) { + if (row & 0x0000000F) { + decoded_range[x + (y * 9) + 2] = 1; + } + row >>= 4; + } + } + + switch (loc.direction) { + case Direction::LEFT: + for (int16_t y = 0; y < 9; y++) { + int16_t map_y = loc.y + y - 4; + if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.height))) { + for (int16_t x = 0; x < 9; x++) { + int16_t map_x = loc.x + x - 4; + if (!map_and_rules || ((map_x >= 0) && (map_x < map_and_rules->map.width))) { + ret[y * 9 + x] = decoded_range[(8 - x) * 9 + y]; + } else { + break; + } + } + } else { + break; + } + } + break; + + case Direction::RIGHT: + for (int16_t y = 0; y < 9; y++) { + int16_t map_y = loc.y + y - 4; + if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.height))) { + for (int16_t x = 0; x < 9; x++) { + int16_t map_x = loc.x + x - 4; + if (!map_and_rules || ((map_x >= 0) && (map_x < map_and_rules->map.width))) { + ret[y * 9 + x] = decoded_range[((x * 9) - y) + 8]; + } else { + break; + } + } + } else { + break; + } + } + break; + + case Direction::UP: + for (int16_t y = 0; y < 9; y++) { + int16_t map_y = loc.y + y - 4; + if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.height))) { + for (int16_t x = 0; x < 9; x++) { + int16_t map_x = loc.x + x - 4; + if (!map_and_rules || ((map_x >= 0) && (map_x < map_and_rules->map.width))) { + ret[y * 9 + x] = decoded_range[y * 9 + x]; + } else { + break; + } + } + } else { + break; + } + } + break; + + case Direction::DOWN: + for (int16_t y = 0; y < 9; y++) { + int16_t map_y = loc.y + y - 4; + if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.height))) { + for (int16_t x = 0; x < 9; x++) { + int16_t map_y = loc.x + x - 4; + if (!map_and_rules || ((map_y >= 0) && (map_y < map_and_rules->map.width))) { + ret[y * 9 + x] = decoded_range[((8 - y) * 9 - x) + 8]; + } else { + break; + } + } + } else { + break; + } + } + break; + + default: + break; + } +} + +bool card_linkage_is_valid( + shared_ptr right_ce, + shared_ptr left_ce, + shared_ptr sc_ce, + bool has_permission_effect) { + if (!right_ce) { + return false; + } + + bool sc_is_named_android_without_permission_effect = false; + bool sc_is_named_android = sc_ce->def.is_named_android_sc(); + if (sc_is_named_android && + !has_permission_effect && + (left_ce->def.type == CardType::ITEM)) { + sc_is_named_android_without_permission_effect = true; + } + + if (!left_ce) { + return false; + } + + for (size_t x = 0; x < 8; x++) { + uint8_t right_color = left_ce->def.right_colors[x]; + if ((right_color != 0) && + (!sc_is_named_android_without_permission_effect || (right_color != 3))) { + for (size_t y = 0; y < 8; y++) { + if (right_color == right_ce->def.left_colors[y]) { + return true; + } + } + } + } + + // If we get here, then the linkage does not make sense based only on the + // cards' left/right colors. It may still be allowed if Permission is in + // effect, though. + + // Ignore Permission effect if the left card is another action card (the Tech + // color linkage must make sense in that case). (The way they do this is kind + // of dumb - they should have checked that type == ACTION, but instead they + // checked that type *isn't* most of the other types... but curiously, ASSIST + // is not checked. This is probably just an oversight.) + if (has_permission_effect && + (left_ce->def.type != CardType::HUNTERS_SC) && + (left_ce->def.type != CardType::ARKZ_SC) && + (left_ce->def.type != CardType::ITEM) && + (left_ce->def.type != CardType::CREATURE)) { + has_permission_effect = false; + } + + if (has_permission_effect) { + // Permission allows a right card with left color 03 to link with anything + for (size_t z = 0; z < 8; z++) { + if (right_ce->def.left_colors[z] == 3) { + return true; + } + } + } + + return false; +} + + + +RulerServer::RulerServer(shared_ptr server) + : w_server(server), + team_id_for_client_id(0xFF), + error_code1(0), + error_code2(0), + error_code3(0) { } + +shared_ptr RulerServer::server() { + auto s = this->w_server.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +shared_ptr RulerServer::server() const { + auto s = this->w_server.lock(); + if (!s) { + throw runtime_error("server is deleted"); + } + return s; +} + +ActionChainWithConds* RulerServer::action_chain_with_conds_for_card_ref( + uint16_t card_ref) { + return const_cast(as_const(*this).action_chain_with_conds_for_card_ref(card_ref)); +} + +const ActionChainWithConds* RulerServer::action_chain_with_conds_for_card_ref( + uint16_t card_ref) const { + uint8_t client_id = client_id_for_card_ref(card_ref); + if (client_id != 0xFF) { + for (size_t z = 0; z < 9; z++) { + const auto* chain = &this->set_card_action_chains[client_id]->at(z); + if (card_ref == chain->chain.acting_card_ref) { + return chain; + } + } + } + return nullptr; +} + +bool RulerServer::any_attack_action_card_is_support_tech_or_support_pb( + const ActionState& pa) const { + if (pa.attacker_card_ref != 0xFFFF) { + for (size_t z = 0; (z < 8) && (pa.action_card_refs[z] != 0xFFFF); z++) { + uint16_t card_id = this->card_id_for_card_ref(pa.action_card_refs[z]); + if (this->card_id_is_support_tech_or_support_pb(card_id)) { + return true; + } + } + } + return false; +} + +bool RulerServer::card_has_pierce_or_rampage( + uint8_t client_id, + ConditionType cond_type, + bool* out_has_rampage, + uint16_t attacker_card_ref, + uint16_t action_card_ref, + uint8_t def_effect_index, + AttackMedium attack_medium) const { + auto short_statuses = (client_id != 0xFF) ? this->short_statuses[client_id] : nullptr; + *out_has_rampage = false; + + if (cond_type == ConditionType::NONE) { + return false; + } + bool ret = this->check_usability_or_apply_condition_for_card_refs( + action_card_ref, + attacker_card_ref, + // Original code omitted this null check and presumably could crash here + short_statuses ? short_statuses->at(0).card_ref.load() : 0xFFFF, + def_effect_index, + attack_medium); + + switch (cond_type) { + case ConditionType::RAMPAGE: + case ConditionType::UNKNOWN_20: + case ConditionType::UNKNOWN_21: + case ConditionType::MAJOR_RAMPAGE: + case ConditionType::HEAVY_RAMPAGE: + *out_has_rampage = true; + return false; + case ConditionType::PIERCE: + return ret; + case ConditionType::HEAVY_PIERCE: + if (short_statuses) { + const auto& sc_status = short_statuses->at(0); + auto ce = this->definition_for_card_ref(sc_status.card_ref); + if (ce && (ce->def.type == CardType::HUNTERS_SC)) { + size_t count = 0; + for (size_t z = 7; z < 15; z++) { + if (this->card_exists_by_status(short_statuses->at(z))) { + count++; + } + } + if (count > 2) { + return ret; + } + } + } + return false; + case ConditionType::MAJOR_PIERCE: + if (short_statuses) { + const auto& sc_status = short_statuses->at(0); + auto ce = this->definition_for_card_ref(sc_status.card_ref); + if (ce && (this->get_card_ref_max_hp(sc_status.card_ref) <= sc_status.current_hp * 2)) { + return ret; + } + } + return false; + default: + return false; + } +} + +bool RulerServer::attack_action_has_rampage_and_not_pierce( + const ActionState& pa, uint16_t card_ref) const { + uint16_t orig_card_ref; + uint16_t effective_range_card_id; + TargetMode effective_target_mode; + bool has_pierce = false; + auto attack_medium = this->get_attack_medium(pa); + + if (!this->compute_effective_range_and_target_mode_for_attack( + pa, &effective_range_card_id, &effective_target_mode, &orig_card_ref)) { + return false; + } + + if ((orig_card_ref != 0xFFFF) && (orig_card_ref != pa.attacker_card_ref) && + !this->check_usability_or_apply_condition_for_card_refs( + orig_card_ref, pa.attacker_card_ref, card_ref, 0xFF, AttackMedium::INVALID_FF)) { + return false; + } + + ssize_t x = -1; + for (size_t z = 0; (z < 8) && (pa.action_card_refs[z] != 0xFFFF); z++) { + x = z; + } + for (; x >= 0; x--) { + auto ce = this->definition_for_card_ref(pa.action_card_refs[x]); + if (ce) { + ssize_t cond_index; + for (cond_index = 0; cond_index < 3; cond_index++) { + if (ce->def.effects[cond_index].type == ConditionType::NONE) { + break; + } + } + for (; cond_index >= 0; cond_index--) { + bool has_rampage = this->check_pierce_and_rampage( + card_ref, + ce->def.effects[cond_index].type, + &has_pierce, + pa.attacker_card_ref, + pa.action_card_refs[x], + cond_index, + attack_medium); + if (has_rampage) { + return true; + } + if (has_pierce) { + return false; + } + } + } + } + + const auto* chain = this->action_chain_with_conds_for_card_ref( + pa.attacker_card_ref); + if (chain) { + for (ssize_t z = 8; z >= 0; z--) { + bool has_rampage = this->check_pierce_and_rampage( + card_ref, + chain->conditions[z].type, + &has_pierce, + pa.attacker_card_ref, + chain->conditions[z].card_ref, + chain->conditions[z].card_definition_effect_index, + attack_medium); + if (has_rampage) { + return true; + } + if (has_pierce) { + return false; + } + } + } + + return false; +} + +bool RulerServer::attack_action_has_pierce_and_not_rampage( + const ActionState& pa, uint8_t client_id) { + if ((client_id_for_card_ref(pa.attacker_card_ref) == 0xFF) || (client_id == 0xFF)) { + return false; + } + + auto attack_medium = this->get_attack_medium(pa); + auto stat = this->short_statuses[client_id]; + if (!stat || !this->card_exists_by_status(stat->at(0)) || (stat->at(0).card_ref == 0xFFFF)) { + return false; + } + + uint16_t card_ref1; + if (!this->compute_effective_range_and_target_mode_for_attack(pa, nullptr, nullptr, &card_ref1)) { + return false; + } + if ((card_ref1 != 0xFFFF) && + (card_ref1 != pa.attacker_card_ref) && + !this->check_usability_or_apply_condition_for_card_refs(card_ref1, pa.attacker_card_ref, stat->at(0).card_ref, 0xFF, AttackMedium::INVALID_FF)) { + return false; + } + + ssize_t last_action_card_index = -1; + for (size_t z = 0; (z < 8) && (pa.action_card_refs[z] != 0xFFFF); z++) { + last_action_card_index = z; + } + + for (; last_action_card_index >= 0; last_action_card_index--) { + auto ce = this->definition_for_card_ref( + pa.action_card_refs[last_action_card_index]); + if (!ce) { + continue; + } + + ssize_t last_cond_index = -1; + for (size_t z = 0; (z < 3) && (ce->def.effects[z].type != ConditionType::NONE); z++) { + last_cond_index = z; + } + + for (; last_cond_index >= 0; last_cond_index--) { + bool has_rampage = false; + if (this->card_has_pierce_or_rampage( + client_id, ce->def.effects[last_cond_index].type, &has_rampage, + pa.attacker_card_ref, pa.action_card_refs[last_action_card_index], + last_cond_index, attack_medium)) { + return true; + } + if (has_rampage) { + return false; + } + } + } + + const auto* chain = this->action_chain_with_conds_for_card_ref(pa.attacker_card_ref); + if (chain) { + for (ssize_t cond_index = 8; cond_index >= 0; cond_index--) { + bool has_rampage = false; + if (this->card_has_pierce_or_rampage( + client_id, chain->conditions[cond_index].type, &has_rampage, + pa.attacker_card_ref, chain->conditions[cond_index].card_ref, + chain->conditions[cond_index].card_definition_effect_index, + attack_medium)) { + return true; + } + if (has_rampage) { + return false; + } + } + } + + return false; +} + + +bool RulerServer::card_exists_by_status(const CardShortStatus& stat) const { + if ((stat.card_flags & 3) || (stat.card_ref == 0xFFFF)) { + return false; + } + uint8_t client_id = client_id_for_card_ref(stat.card_ref); + if ((client_id < 4) && (this->team_id_for_client_id[client_id] != 0xFF)) { + return true; + } + return false; +} + +bool RulerServer::card_has_mighty_knuckle(uint32_t card_ref) const { + auto ce = this->definition_for_card_ref(card_ref); + if (ce) { + for (size_t z = 0; z < 3; z++) { + if (ce->def.effects[z].type == ConditionType::NONE) { + return false; + } + if (ce->def.effects[z].type == ConditionType::MIGHTY_KNUCKLE) { + return true; + } + } + } + return false; +} + +uint16_t RulerServer::card_id_for_card_ref(uint16_t card_ref) const { + return this->server()->card_id_for_card_ref(card_ref); +} + +bool RulerServer::card_id_is_boss_sc(uint16_t card_id) { + return (card_id >= 0x029B) && (card_id < 0x029F); +} + +bool RulerServer::card_id_is_support_tech_or_support_pb(uint16_t card_id) { + return (card_id == 0x00E1) || + (card_id == 0x00E2) || + (card_id == 0x00E6) || + (card_id == 0x00EB) || + (card_id == 0x00EC); +} + +bool RulerServer::card_ref_can_attack(uint16_t card_ref) { + if (card_ref == 0xFFFF) { + return false; + } + + if (!this->should_allow_attacks_on_current_turn()) { + return false; + } + + auto ce = this->definition_for_card_ref(card_ref); + if (!ce) { + return false; + } + + if (ce->def.type == CardType::ACTION) { + return true; + } else if (ce->def.type == CardType::ASSIST) { + return false; + } + + uint8_t client_id = client_id_for_card_ref(card_ref); + const auto* stat = this->short_status_for_card_ref(card_ref); + if ((client_id == 0xFF) || !stat || !this->card_exists_by_status(*stat)) { + return false; + } + + if (this->find_condition_on_card_ref(card_ref, ConditionType::HOLD) || + this->find_condition_on_card_ref(card_ref, ConditionType::GUOM) || + this->find_condition_on_card_ref(card_ref, ConditionType::PARALYZE) || + this->find_condition_on_card_ref(card_ref, ConditionType::FREEZE)) { + return false; + } + + // If the card is an item and its SC has any attack-preventing condition, + // then the item also cannot attack + if ((ce->def.type == CardType::ITEM) && + (!this->short_statuses[client_id] || + (this->short_statuses[client_id]->at(0).card_ref == 0xFFFF) || + this->find_condition_on_card_ref(this->short_statuses[client_id]->at(0).card_ref, ConditionType::HOLD) || + this->find_condition_on_card_ref(this->short_statuses[client_id]->at(0).card_ref, ConditionType::GUOM) || + this->find_condition_on_card_ref(this->short_statuses[client_id]->at(0).card_ref, ConditionType::PARALYZE) || + this->find_condition_on_card_ref(this->short_statuses[client_id]->at(0).card_ref, ConditionType::FREEZE))) { + return false; + } + + if ((ce->def.card_class() == CardClass::GUARD_ITEM) && + this->find_condition_on_card_ref(card_ref, ConditionType::SHIELD_WEAPON)) { + return true; + } + + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client( + client_id); + for (size_t z = 0; z < num_assists; z++) { + if (this->assist_server->get_active_assist_by_index(z) == AssistEffect::PERMISSION) { + return true; + } + } + + return !ce->def.cannot_attack; +} + +bool RulerServer::card_ref_can_move( + uint8_t client_id, uint16_t card_ref, bool ignore_atk_points) const { + if (client_id == 0xFF) { + return false; + } + + if (client_id_for_card_ref(card_ref) != client_id) { + return false; + } + + if (!this->action_chain_with_conds_for_card_ref(card_ref)) { + return false; + } + + auto ce = this->definition_for_card_ref(card_ref); + if (!ce) { + return false; + } + + const CardShortStatus* stat = nullptr; + auto short_statuses = this->short_statuses[client_id]; + if (short_statuses->at(0).card_ref == card_ref) { // SC moving + stat = &short_statuses->at(0); + if (ce->def.type == CardType::HUNTERS_SC) { + for (size_t z = 7; z < 15; z++) { + const auto& item_stat = short_statuses->at(z); + if ((item_stat.card_ref != 0xFFFF) && this->card_exists_by_status(item_stat) && + (this->find_condition_on_card_ref(item_stat.card_ref, ConditionType::GUOM) || + this->find_condition_on_card_ref(item_stat.card_ref, ConditionType::IMMOBILE))) { + return false; + } + } + } + } else if (ce->def.type == CardType::CREATURE) { // Creature moving + for (size_t z = 7; z < 15; z++) { + const auto* creature_stat = &short_statuses->at(z); + if (creature_stat->card_ref == card_ref) { + stat = creature_stat; + } + } + } + + if (!stat || !this->card_exists_by_status(*stat) || (stat->card_flags & 0x80)) { + return false; + } + + if ((this->hand_and_equip_states[client_id]->assist_flags & 0x80)) { + return false; + } + + if (this->find_condition_on_card_ref(card_ref, ConditionType::HOLD) || + this->find_condition_on_card_ref(card_ref, ConditionType::GUOM) || + this->find_condition_on_card_ref(card_ref, ConditionType::FREEZE) || + this->find_condition_on_card_ref(card_ref, ConditionType::IMMOBILE)) { + return false; + } + + uint8_t current_atk = this->hand_and_equip_states[client_id]->atk_points; + uint8_t max_move_dist = this->max_move_distance_for_card_ref(card_ref); + if (max_move_dist == 0) { + return false; + } + + if (!ignore_atk_points) { + if (max_move_dist < current_atk) { + current_atk = max_move_dist; + } + return (current_atk != 0); + } else { + return true; + } +} + +bool RulerServer::card_ref_has_class_usability_condition( + uint16_t card_ref) const { + auto ce = this->definition_for_card_ref(card_ref); + if (ce) { + uint8_t criterion = static_cast(ce->def.usable_criterion); + if ((criterion >= 0x01) && (criterion < 0x04)) { + return true; + } + if ((criterion >= 0x09) && (criterion < 0x1D)) { + return true; + } + } + return false; +} + +bool RulerServer::card_ref_has_free_maneuver(uint16_t card_ref) const { + return this->find_condition_on_card_ref(card_ref, ConditionType::FREE_MANEUVER); +} + +bool RulerServer::card_ref_is_aerial(uint16_t card_ref) const { + const auto* stat = this->short_status_for_card_ref(card_ref); + if (!stat || !this->card_exists_by_status(*stat)) { + return false; + } + + uint8_t client_id = client_id_for_card_ref(card_ref); + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + if (this->assist_server->get_active_assist_by_index(z) == AssistEffect::FLY) { + return true; + } + } + + // Note: The original code checks equipped items here for the Aerial condition + // if card_ref is a Hunters SC, then ignores the result. We omit this check + // for obvious reasons. + return this->find_condition_on_card_ref(card_ref, ConditionType::AERIAL); +} + +bool RulerServer::card_ref_is_aerial_or_has_free_maneuver( + uint16_t card_ref) const { + return (this->card_ref_has_free_maneuver(card_ref) || this->card_ref_is_aerial(card_ref)); +} + +bool RulerServer::card_ref_is_boss_sc(uint32_t card_ref) const { + return this->card_id_is_boss_sc(this->card_id_for_card_ref(card_ref)); +} + +bool RulerServer::card_ref_or_any_set_card_has_condition_46( + uint16_t card_ref) const { + uint16_t card_id = this->card_id_for_card_ref(card_ref); + if (card_id == 0xFFFF) { + return false; + } + + uint8_t client_id = client_id_for_card_ref(card_ref); + if (this->hand_and_equip_states[client_id]->assist_flags & 0x100) { + auto ce = this->definition_for_card_id(card_id); + if (!ce) { + return false; + } + if ((ce->def.type != CardType::HUNTERS_SC) && (ce->def.type != CardType::ARKZ_SC)) { + return true; + } + } + + for (size_t z = 0; z < 4; z++) { + auto stat = this->short_statuses[z]; + if (stat) { + const auto& sc_stat = stat->at(0); + Condition cond; + if (this->card_exists_by_status(sc_stat) && + this->find_condition_on_card_ref(sc_stat.card_ref, ConditionType::UNKNOWN_46, &cond) && + (cond.value == card_id)) { + return true; + } + + for (size_t w = 7; w < 15; w++) { + const auto& item_stat = stat->at(w); + if (this->card_exists_by_status(item_stat) && + this->find_condition_on_card_ref(item_stat.card_ref, ConditionType::UNKNOWN_46, &cond) && + (cond.value == card_id)) { + return true; + } + } + } + } + + return false; +} + +bool RulerServer::card_ref_or_sc_has_fixed_range(uint16_t card_ref) const { + if (this->find_condition_on_card_ref(card_ref, ConditionType::FIXED_RANGE)) { + return true; + } + + auto ce = this->definition_for_card_ref(card_ref); + if (!ce || (ce->def.type != CardType::ITEM)) { + return false; + } + + uint8_t client_id = client_id_for_card_ref(card_ref); + if ((client_id == 0xFF) || !this->short_statuses[client_id]) { + return false; + } + + return this->find_condition_on_card_ref( + this->short_statuses[client_id]->at(0).card_ref, ConditionType::FIXED_RANGE); +} + +bool RulerServer::check_move_path_and_get_cost( + uint8_t client_id, + uint16_t card_ref, + parray* visited_map, + MovePath* out_path, + uint32_t* out_cost) const { + if (client_id == 0xFF) { + return false; + } + + const auto* chain = this->action_chain_with_conds_for_card_ref(card_ref); + if (!chain) { + return false; + } + + uint8_t atk = this->hand_and_equip_states[client_id]->atk_points; + // Note: In the original code, it seems atk was signed, which doesn't make + // much sense. We've fixed that here. + // if (atk < 0) { // Uhhh what? This is supposed to be impossible + // return false; + // } + + uint8_t max_dist = this->max_move_distance_for_card_ref(card_ref); + if (max_dist < 1) { + return false; + } + max_dist = min(max_dist, 9); + + const auto* short_status = this->short_status_for_card_ref(card_ref); + if (!short_status) { + return false; + } + + bool is_free_maneuver_or_aerial = this->card_ref_is_aerial_or_has_free_maneuver(card_ref); + bool is_aerial = this->card_ref_is_aerial(card_ref); + uint8_t x = short_status->loc.x; + uint8_t y = short_status->loc.y; + visited_map->clear(0); + this->flood_fill_move_path( + *chain, x + 1, y, Direction::RIGHT, atk, max_dist, is_free_maneuver_or_aerial, is_aerial, visited_map, out_path, 0, 0); + this->flood_fill_move_path( + *chain, x, y - 1, Direction::UP, atk, max_dist, is_free_maneuver_or_aerial, is_aerial, visited_map, out_path, 0, 0); + this->flood_fill_move_path( + *chain, x - 1, y, Direction::LEFT, atk, max_dist, is_free_maneuver_or_aerial, is_aerial, visited_map, out_path, 0, 0); + this->flood_fill_move_path( + *chain, x, y + 1, Direction::DOWN, atk, max_dist, is_free_maneuver_or_aerial, is_aerial, visited_map, out_path, 0, 0); + if (out_path) { + if (!out_path->is_valid() || (out_path->get_length_plus1() < 2)) { + if (out_cost) { + *out_cost = 99; + } + } else if (out_cost) { + *out_cost = out_path->get_cost(); + } + } + + return true; +} + +bool RulerServer::check_pierce_and_rampage( + uint16_t card_ref, + ConditionType cond_type, + bool* out_has_pierce, + uint16_t attacker_card_ref, + uint16_t action_card_ref, + uint8_t def_effect_index, + AttackMedium attack_medium) const { + *out_has_pierce = false; + + const auto* card_short_status = this->short_status_for_card_ref(card_ref); + if (cond_type == ConditionType::NONE) { + return false; + } + + if ((card_ref != 0xFFFF) && + (!card_short_status || !this->card_exists_by_status(*card_short_status))) { + return false; + } + + auto ce = this->definition_for_card_ref(card_short_status->card_ref); + if (!ce) { + return false; + } + + uint8_t client_id = client_id_for_card_ref(card_ref); + auto client_short_statuses = (client_id != 0xFF) ? this->short_statuses[client_id] : nullptr; + + if (card_ref == 0xFFFF) { + card_short_status = nullptr; + client_short_statuses = nullptr; + } + + bool apply_check_result = this->check_usability_or_apply_condition_for_card_refs( + action_card_ref, attacker_card_ref, card_ref, def_effect_index, attack_medium); + + switch (cond_type) { + case ConditionType::PIERCE: + *out_has_pierce = 1; + return false; + case ConditionType::RAMPAGE: + return apply_check_result; + case ConditionType::UNKNOWN_20: + if (card_short_status && ce && (ce->def.self_cost < 3)) { + return apply_check_result; + } + return false; + case ConditionType::UNKNOWN_21: + if (card_short_status && ce && (ce->def.self_cost > 2)) { + return apply_check_result; + } + return false; + case ConditionType::MAJOR_RAMPAGE: + if (!card_short_status) { + return apply_check_result; + } + if (client_short_statuses) { + const auto& sc_stat = client_short_statuses->at(0); + auto ce = this->definition_for_card_ref(sc_stat.card_ref); + if (ce && (ce->def.type == CardType::HUNTERS_SC) && + (this->get_card_ref_max_hp(sc_stat.card_ref) <= sc_stat.current_hp * 2)) { + return apply_check_result; + } + } + return false; + case ConditionType::MAJOR_PIERCE: + case ConditionType::HEAVY_PIERCE: + *out_has_pierce = 1; + return false; + case ConditionType::HEAVY_RAMPAGE: + if (!card_short_status) { + return apply_check_result; + } + if (client_short_statuses) { + auto ce = this->definition_for_card_ref(client_short_statuses->at(0).card_ref); + if (ce && (ce->def.type == CardType::HUNTERS_SC)) { + size_t count = 0; + for (size_t z = 7; z < 15; z++) { + if (this->card_exists_by_status(client_short_statuses->at(z))) { + count++; + } + } + if (count >= 3) { + return apply_check_result; + } + } + } + return false; + default: + return false; + } +} + +bool RulerServer::check_usability_or_apply_condition_for_card_refs( + uint16_t card_ref1, + uint16_t card_ref2, + uint16_t card_ref3, + uint8_t def_effect_index, + AttackMedium attack_medium) const { + uint8_t client_id1 = client_id_for_card_ref(card_ref1); + uint8_t client_id2 = client_id_for_card_ref(card_ref2); + uint16_t card_id1 = this->card_id_for_card_ref(card_ref1); + uint16_t card_id2 = this->card_id_for_card_ref(card_ref2); + uint16_t card_id3 = this->card_id_for_card_ref(card_ref3); + if (static_cast(attack_medium) & 0x80) { // Presumably to detect 0xFF + attack_medium = AttackMedium::UNKNOWN; + } + return this->check_usability_or_condition_apply( + client_id1, card_id1, client_id2, card_id2, card_id3, def_effect_index, false, attack_medium); +} + +bool RulerServer::check_usability_or_condition_apply( + uint8_t client_id1, + uint16_t card_id1, + uint8_t client_id2, + uint16_t card_id2, + uint16_t card_id3, + uint8_t def_effect_index, + bool is_item_usability_check, + AttackMedium attack_medium) const { + if (static_cast(attack_medium) & 0x80) { + attack_medium = AttackMedium::UNKNOWN; + } + + auto ce1 = this->definition_for_card_id(card_id1); + auto ce2 = this->definition_for_card_id(card_id2); + auto ce3 = this->definition_for_card_id(card_id3); + if (!ce1) { + return false; + } + if ((ce1->def.type == CardType::ITEM) && this->card_id_is_boss_sc(card_id2)) { + return false; + } + + CriterionCode criterion_code; + if (def_effect_index & 0xFF) { + criterion_code = ce1->def.usable_criterion; + } else { + if (def_effect_index > 2) { + return false; + } + criterion_code = ce1->def.effects[def_effect_index].apply_criterion; + } + + // For item usability checks, prevent criteria that depend on player + // positioning/team setup + if (is_item_usability_check && + ((criterion_code == CriterionCode::SAME_TEAM) || + (criterion_code == CriterionCode::SAME_PLAYER) || + (criterion_code == CriterionCode::SAME_TEAM_NOT_SAME_PLAYER) || + (criterion_code == CriterionCode::UNKNOWN_07) || + (criterion_code == CriterionCode::NOT_SC) || + (criterion_code == CriterionCode::SC))) { + criterion_code = CriterionCode::NONE; + } + + // Presumably this odd-looking expression here is used to handle two different + // cases. When checking for a condition, def_effect_index should be non-0xFF, + // so we'd return true if the criterion passes. When checking if an item or + // creature card is usable, the two client IDs should be the same or the + // second should not be given, so we'd return true if the criterion passes. If + // neither of these cases apply, we should return false as a failsafe even if + // the criterion passes. + bool ret = (!(def_effect_index & 0x80) || (client_id1 == client_id2)) || (client_id2 == 0xFF); + switch (criterion_code) { + case CriterionCode::NONE: + return ret; + case CriterionCode::HU_CLASS_SC: + if (ce2 && (ce2->def.card_class() == CardClass::HU_SC)) { + return ret; + } + break; + case CriterionCode::RA_CLASS_SC: + if (ce2 && (ce2->def.card_class() == CardClass::RA_SC)) { + return ret; + } + break; + case CriterionCode::FO_CLASS_SC: + if (ce2 && (ce2->def.card_class() == CardClass::FO_SC)) { + return ret; + } + break; + case CriterionCode::SAME_TEAM: + if ((client_id1 == client_id2) || + ((client_id1 != 0xFF) && (client_id2 != 0xFF) && + (this->team_id_for_client_id[client_id1] == this->team_id_for_client_id[client_id2]))) { + return true; + } + break; + case CriterionCode::SAME_PLAYER: + if (client_id1 != client_id2) { + return true; + } + break; + case CriterionCode::SAME_TEAM_NOT_SAME_PLAYER: + if ((client_id2 != client_id1) && (client_id1 != 0xFF) && (client_id2 != 0xFF) && + (this->team_id_for_client_id[client_id1] == this->team_id_for_client_id[client_id2])) { + return true; + } + break; + case CriterionCode::UNKNOWN_07: + // Like NOT_SC, but for ce3 instead of ce2 + if (ce3 && (ce3->def.type != CardType::HUNTERS_SC) && (ce3->def.type != CardType::ARKZ_SC)) { + return ret; + } + break; + case CriterionCode::NOT_SC: + if (ce2 && (ce2->def.type != CardType::HUNTERS_SC) && (ce2->def.type != CardType::ARKZ_SC)) { + return ret; + } + break; + case CriterionCode::SC: + if (ce2 && ((ce2->def.type == CardType::HUNTERS_SC) || (ce2->def.type == CardType::ARKZ_SC))) { + return ret; + } + break; + case CriterionCode::HU_OR_RA_CLASS_SC: + if (ce2 && ((ce2->def.card_class() == CardClass::HU_SC) || (ce2->def.card_class() == CardClass::RA_SC))) { + return ret; + } + break; + case CriterionCode::HUNTER_HUMAN_SC: { + static const unordered_set card_ids = { + 0x0001, // Orland + 0x0002, // Kranz + 0x0003, // Ino'lis + 0x0004, // Sil'fer + 0x0006, // Kylria + 0x0111, // Relmitos + 0x0112, // Viviana + 0x0115, // Glustar + 0x02AA, // H-HUmar + 0x02AB, // H-HUnewearl + 0x02AE, // H-RAmar + 0x02AF, // H-RAmarl + 0x02B2, // H-FOmar + 0x02B3, // H-FOmarl + 0x02B4, // H-FOnewm + 0x02B5, // H-FOnewearl + 0x02CC, // H-HUmar + 0x02CD, // H-RAmarl + 0x02CE, // H-FOmarl + 0x02CF, // H-HUnewearl + 0x02D1, // H-RAmarl + 0x02D5, // H-FOmar + 0x02D6, // H-FOnewearl + 0x02D9, // H-FOnewm + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_HU_CLASS_MALE_SC: { + static const unordered_set card_ids = { + 0x0001, // Orland + 0x0113, // Teifu + 0x02AA, // H-HUmar + 0x02AC, // H-HUcast + 0x02CC, // H-HUmar + 0x02D7, // H-HUcast + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_FEMALE_SC: { + static const unordered_set card_ids = { + 0x0003, // Ino'lis + 0x0004, // Sil'fer + 0x0006, // Kylria + 0x0110, // Saligun + 0x0112, // Viviana + 0x0114, // Stella + 0x02AB, // H-HUnewearl + 0x02AD, // H-HUcaseal + 0x02AF, // H-RAmarl + 0x02B1, // H-RAcaseal + 0x02B3, // H-FOmarl + 0x02B5, // H-FOnewearl + 0x02CE, // H-FOmarl + 0x02CF, // H-HUnewearl + 0x02D1, // H-RAmarl + 0x02D4, // H-HUcaseal + 0x02D6, // H-FOnewearl + 0x02D8, // H-RAcaseal + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_HU_OR_FO_CLASS_HUMAN_SC: { + static const unordered_set card_ids = { + 0x0001, // Orland + 0x0003, // Ino'lis + 0x0004, // Sil'fer + 0x0111, // Relmitos + 0x0115, // Glustar + 0x0112, // Viviana + 0x02AA, // H-HUmar + 0x02AB, // H-HUnewearl + 0x02B2, // H-FOmar + 0x02B3, // H-FOmarl + 0x02B4, // H-FOnewm + 0x02B5, // H-FOnewearl + 0x02CC, // H-HUmar + 0x02CE, // H-FOmarl + 0x02CF, // H-HUnewearl + 0x02D5, // H-FOmar + 0x02D6, // H-FOnewearl + 0x02D9, // H-FOnewm + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_HU_CLASS_ANDROID_SC: { + static const unordered_set card_ids = { + 0x0110, // Saligun + 0x0113, // Teifu + 0x02AC, // H-HUcast + 0x02AD, // H-HUcaseal + 0x02D4, // H-HUcaseal + 0x02D7, // H-HUcast + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::UNKNOWN_10: { + static const unordered_set card_ids = { + 0x0001, // Orland + 0x0003, // Ino'lis + 0x0110, // Saligun + 0x0111, // Relmitos + 0x0113, // Teifu + 0x02AA, // H-HUmar + 0x02AC, // H-HUcast + 0x02AD, // H-HUcaseal + 0x02B2, // H-FOmar + 0x02B3, // H-FOmarl + 0x02CC, // H-HUmar + 0x02CE, // H-FOmarl + 0x02D4, // H-HUcaseal + 0x02D5, // H-FOmar + 0x02D7, // H-HUcast + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::UNKNOWN_11: { + static const unordered_set card_ids = { + 0x0001, // Orland + 0x0002, // Kranz + 0x0005, // Guykild + 0x0113, // Teifu + 0x02AA, // H-HUmar + 0x02AC, // H-HUcast + 0x02AE, // H-RAmar + 0x02B0, // H-RAcast + 0x02CC, // H-HUmar + 0x02CD, // H-RAmarl + 0x02D0, // H-RAcast + 0x02D7, // H-HUcast + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_HUNEWEARL_CLASS_SC: { + static const unordered_set card_ids = { + 0x0004, // Sil'fer + 0x02AB, // H-HUnewearl + 0x02CF, // H-HUnewearl + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_RA_CLASS_MALE_SC: { + static const unordered_set card_ids = { + 0x0002, // Kranz + 0x0005, // Guykild + 0x02AE, // H-RAmar + 0x02B0, // H-RAcast + 0x02CD, // H-RAmarl + 0x02D0, // H-RAcast + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_RA_CLASS_FEMALE_SC: { + static const unordered_set card_ids = { + 0x0006, // Kylria + 0x0114, // Stella + 0x02AF, // H-RAmarl + 0x02B1, // H-RAcaseal + 0x02D1, // H-RAmarl + 0x02D2, // D-RAcaseal + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_RA_OR_FO_CLASS_FEMALE_SC: { + static const unordered_set card_ids = { + 0x0003, // Ino'lis + 0x0006, // Kylria + 0x0112, // Viviana + 0x0114, // Stella + 0x02AF, // H-RAmarl + 0x02B1, // H-RAcaseal + 0x02B3, // H-FOmarl + 0x02B5, // H-FOnewearl + 0x02CE, // H-FOmarl + 0x02D1, // H-RAmarl + 0x02D6, // H-FOnewearl + 0x02D8, // H-RAcaseal + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_HU_OR_RA_CLASS_HUMAN_SC: { + static const unordered_set card_ids = { + 0x0001, // Orland + 0x0002, // Kranz + 0x0004, // Sil'fer + 0x0006, // Kylria + 0x02AA, // H-HUmar + 0x02AB, // H-HUnewearl + 0x02AE, // H-RAmar + 0x02AF, // H-RAmarl + 0x02CC, // H-HUmar + 0x02CD, // H-RAmarl + 0x02CF, // H-HUnewearl + 0x02D1, // H-RAmarl + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_RA_CLASS_ANDROID_SC: { + static const unordered_set card_ids = { + 0x0005, // Guykild + 0x0114, // Stella + 0x02B0, // H-RAcast + 0x02B1, // H-RAcaseal + 0x02D0, // H-RAcast + 0x02D8, // H-RAcaseal + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_FO_CLASS_FEMALE_SC: { + static const unordered_set card_ids = { + 0x0003, // Ino'lis + 0x0112, // Viviana + 0x02B3, // H-FOmarl + 0x02B5, // H-FOnewearl + 0x02CE, // H-FOmarl + 0x02D6, // H-FOnewearl + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_FEMALE_HUMAN_SC: { + static const unordered_set card_ids = { + 0x0003, // Ino'lis + 0x0004, // Sil'fer + 0x0006, // Kylria + 0x0112, // Viviana + 0x02AB, // H-HUnewearl + 0x02AF, // H-RAmarl + 0x02B3, // H-FOmarl + 0x02B5, // H-FOnewearl + 0x02CE, // H-FOmarl + 0x02CF, // H-HUnewearl + 0x02D1, // H-RAmarl + 0x02D6, // H-FOnewearl + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HUNTER_ANDROID_SC: { + static const unordered_set card_ids = { + 0x0005, // Guykild + 0x0110, // Saligun + 0x0113, // Teifu + 0x0114, // Stella + 0x02AC, // H-HUcast + 0x02AD, // H-HUcaseal + 0x02B0, // H-RAcast + 0x02B1, // H-RAcaseal + 0x02D0, // H-RAcast + 0x02D4, // H-HUcaseal + 0x02D7, // H-HUcast + 0x02D8, // H-RAcaseal + }; + return ret && card_ids.count(card_id2); + } + case CriterionCode::HU_OR_FO_CLASS_SC: + if (ce2 && ((ce2->def.card_class() == CardClass::HU_SC) || (ce2->def.card_class() == CardClass::FO_SC))) { + return ret; + } + break; + case CriterionCode::RA_OR_FO_CLASS_SC: + if (ce2 && ((ce2->def.card_class() == CardClass::RA_SC) || (ce2->def.card_class() == CardClass::FO_SC))) { + return ret; + } + break; + case CriterionCode::PHYSICAL_OR_UNKNOWN_ATTACK_MEDIUM: + if ((attack_medium == AttackMedium::UNKNOWN) || (attack_medium == AttackMedium::PHYSICAL)) { + return ret; + } + break; + case CriterionCode::TECH_OR_UNKNOWN_ATTACK_MEDIUM: + if ((attack_medium == AttackMedium::UNKNOWN) || (attack_medium == AttackMedium::TECH)) { + return ret; + } + break; + case CriterionCode::PHYSICAL_OR_TECH_OR_UNKNOWN_ATTACK_MEDIUM: + if ((attack_medium == AttackMedium::UNKNOWN) || (attack_medium == AttackMedium::PHYSICAL) || (attack_medium == AttackMedium::TECH)) { + return ret; + } + break; + case CriterionCode::UNKNOWN_20: + if ((attack_medium != AttackMedium::PHYSICAL) && (attack_medium != AttackMedium::UNKNOWN)) { + return false; + } + if (!ce3 || ((ce3->def.type != CardType::HUNTERS_SC) && (ce3->def.type != CardType::ARKZ_SC))) { + return ret; + } + break; + case CriterionCode::UNKNOWN_21: + if ((attack_medium != AttackMedium::PHYSICAL) && (attack_medium != AttackMedium::TECH)) { + return false; + } + if (!ce3 || ((ce3->def.type != CardType::HUNTERS_SC) && (ce3->def.type != CardType::ARKZ_SC))) { + return ret; + } + break; + case CriterionCode::UNKNOWN_22: + if ((attack_medium != AttackMedium::UNKNOWN) && (attack_medium != AttackMedium::PHYSICAL) && (attack_medium != AttackMedium::TECH)) { + return false; + } + if (!ce3 || ((ce3->def.type != CardType::HUNTERS_SC) && (ce3->def.type != CardType::ARKZ_SC))) { + return ret; + } + } + + return false; +} + +uint16_t RulerServer::compute_attack_or_defense_costs( + const ActionState& pa, + bool allow_mighty_knuckle, + uint8_t* out_ally_cost) const { + int16_t final_cost = 1; + bool has_mighty_knuckle = false; + int16_t cost_bias = 0; + int16_t tech_cost_bias = 0; + int16_t assist_cost_bias = 0; + int16_t total_cost = 0; + int16_t total_ally_cost = 0; + + if (pa.client_id == 0xFFFF) { + return 99; + } + + if (out_ally_cost) { + *out_ally_cost = 0; + } + + auto action_type = this->get_pending_action_type(pa); + auto ce = this->definition_for_card_ref(pa.attacker_card_ref); + uint8_t client_id = client_id_for_card_ref(pa.attacker_card_ref); + + uint16_t sc_card_ref_if_item = 0xFFFF; + if ((client_id != 0xFF) && ce && (ce->def.type == CardType::ITEM) && + this->short_statuses[client_id]) { + sc_card_ref_if_item = this->short_statuses[client_id]->at(0).card_ref; + } + + if (this->find_condition_on_card_ref(pa.attacker_card_ref, ConditionType::UNKNOWN_15) || + this->find_condition_on_card_ref(sc_card_ref_if_item, ConditionType::UNKNOWN_15)) { + cost_bias = 1; + } + + if (((action_type == ActionType::ATTACK) || (action_type == ActionType::INVALID_00)) && + (this->find_condition_on_card_ref(pa.attacker_card_ref, ConditionType::BIG_SWING) || + this->find_condition_on_card_ref(sc_card_ref_if_item, ConditionType::BIG_SWING))) { + cost_bias++; + } + + if (pa.action_card_refs[0] == 0xFFFF) { + total_cost = cost_bias + 1; + } else { + if (this->find_condition_on_card_ref(pa.attacker_card_ref, ConditionType::TECH) || + this->find_condition_on_card_ref(sc_card_ref_if_item, ConditionType::TECH)) { + tech_cost_bias = -1; + } + + for (size_t z = 0; pa.action_card_refs[z] != 0xFFFF; z++) { + auto ce = this->definition_for_card_ref(pa.action_card_refs[z]); + if (has_mighty_knuckle || !ce || (ce->def.type != CardType::ACTION)) { + return 99; + } + total_cost += (ce->def.self_cost + cost_bias); + if (card_class_is_tech_like(ce->def.card_class())) { + total_cost += tech_cost_bias; + } + total_ally_cost += ce->def.ally_cost; + if (this->card_has_mighty_knuckle(pa.action_card_refs[z])) { + has_mighty_knuckle = true; + } + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(pa.client_id); + for (size_t w = 0; w < num_assists; w++) { + auto assist_effect = this->assist_server->get_active_assist_by_index(w); + if (assist_effect == AssistEffect::INFLATION) { + assist_cost_bias++; + } else if (assist_effect == AssistEffect::DEFLATION) { + assist_cost_bias--; + } + } + } + } + + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(pa.client_id); + for (size_t w = 0; w < num_assists; w++) { + auto assist_effect = this->assist_server->get_active_assist_by_index(w); + if ((assist_effect == AssistEffect::BATTLE_ROYALE) && + (pa.action_card_refs[0] == 0xFFFF)) { + total_cost = 0; + final_cost = 0; + } + } + + if (has_mighty_knuckle) { + if (!allow_mighty_knuckle) { + final_cost = 0; + } else { + final_cost = max(final_cost, this->hand_and_equip_states[pa.client_id]->atk_points); + } + } + + if (out_ally_cost) { + *out_ally_cost = total_ally_cost; + } + return max(final_cost, total_cost + assist_cost_bias); +} + +bool RulerServer::compute_effective_range_and_target_mode_for_attack( + const ActionState& pa, + uint16_t* out_effective_card_id, + TargetMode* out_effective_target_mode, + uint16_t* out_orig_card_ref) const { + size_t z; + for (z = 0; (z < 9) && (pa.action_card_refs[z] != 0xFFFF); z++) { } + if (z >= 9) { + return false; + } + uint16_t card_ref = (z == 0) ? pa.attacker_card_ref : pa.action_card_refs[z - 1]; + + uint16_t card_id = this->card_id_for_card_ref(card_ref); + if (card_id == 0xFFFF) { + return false; + } + + auto ce = this->definition_for_card_id(card_id); + uint8_t client_id = client_id_for_card_ref(pa.attacker_card_ref); + if ((client_id == 0xFF) || !ce) { + return false; + } + + if (out_orig_card_ref) { + *out_orig_card_ref = card_ref; + } + + auto target_mode = ce->def.target_mode; + if (this->card_ref_or_sc_has_fixed_range(pa.attacker_card_ref)) { + card_id = this->card_id_for_card_ref(pa.attacker_card_ref); + auto sc_ce = this->definition_for_card_id(card_id); + if (sc_ce && (static_cast(target_mode) < 6)) { + target_mode = sc_ce->def.target_mode; + } + } + + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + auto assist_effect = this->assist_server->get_active_assist_by_index(z); + if (assist_effect == AssistEffect::SIMPLE) { + card_id = this->card_id_for_card_ref(pa.attacker_card_ref); + } else if (assist_effect == AssistEffect::HEAVY_FOG) { + card_id = 0xFFFE; + } + } + + if (out_effective_target_mode) { + *out_effective_target_mode = target_mode; + } + if (out_effective_card_id) { + *out_effective_card_id = card_id; + } + return true; +} + +size_t RulerServer::count_rampage_targets_for_attack( + const ActionState& pa, uint8_t client_id) const { + if (client_id == 0xFF) { + return 0; + } + + auto stat = this->short_statuses[client_id]; + if (!stat || !this->card_exists_by_status(stat->at(0))) { + return 0; + } + + auto ce = this->definition_for_card_ref(stat->at(0).card_ref); + if (ce->def.type != CardType::HUNTERS_SC) { + return 0; + } + + size_t ret = 0; + for (size_t z = 7; z < 15; z++) { + const auto& stat_entry = stat->at(z); + if (this->card_exists_by_status(stat_entry) && + this->attack_action_has_rampage_and_not_pierce(pa, stat_entry.card_ref)) { + ret++; + } + } + return ret; +} + +bool RulerServer::defense_card_can_apply_to_attack( + uint16_t defense_card_ref, + uint16_t attacker_card_ref, + uint16_t attacker_sc_card_ref) const { + uint16_t defense_card_id = this->card_id_for_card_ref(defense_card_ref); + uint16_t attacker_sc_card_id = this->card_id_for_card_ref(attacker_sc_card_ref); + uint16_t attacker_card_id = this->card_id_for_card_ref(attacker_card_ref); + auto defense_card_ce = this->definition_for_card_id(defense_card_id); + auto attacker_sc_card_ce = this->definition_for_card_id(attacker_sc_card_id); + auto attacker_card_ce = this->definition_for_card_id(attacker_card_id); + if (!defense_card_ce) { + return false; + } + + const auto* chain = this->action_chain_with_conds_for_card_ref(attacker_card_ref); + if (!chain) { + return false; + } + + for (size_t z = 0; z < 9; z++) { + const auto& cond = chain->conditions[z]; + if (cond.type == ConditionType::DEF_DISABLE_BY_COST) { + uint8_t min_cost = cond.value / 10; + uint8_t max_cost = cond.value % 10; + if (defense_card_ce->def.self_cost >= min_cost && defense_card_ce->def.self_cost <= max_cost) { + return false; + } + } + } + + for (size_t z = 0; z < 3; z++) { + switch (defense_card_ce->def.effects[z].type) { + case ConditionType::NATIVE_SHIELD: + if ((!attacker_sc_card_ce || (attacker_sc_card_ce->def.card_class() != CardClass::NATIVE_CREATURE)) && + (!attacker_card_ce || (attacker_card_ce->def.card_class() != CardClass::NATIVE_CREATURE))) { + return false; + } + break; + case ConditionType::A_BEAST_SHIELD: + if ((!attacker_sc_card_ce || (attacker_sc_card_ce->def.card_class() != CardClass::A_BEAST_CREATURE)) && + (!attacker_card_ce || (attacker_card_ce->def.card_class() != CardClass::A_BEAST_CREATURE))) { + return false; + } + break; + case ConditionType::MACHINE_SHIELD: + if ((!attacker_sc_card_ce || (attacker_sc_card_ce->def.card_class() != CardClass::MACHINE_CREATURE)) && + (!attacker_card_ce || (attacker_card_ce->def.card_class() != CardClass::MACHINE_CREATURE))) { + return false; + } + break; + case ConditionType::DARK_SHIELD: + if ((!attacker_sc_card_ce || (attacker_sc_card_ce->def.card_class() != CardClass::DARK_CREATURE)) && + (!attacker_card_ce || (attacker_card_ce->def.card_class() != CardClass::DARK_CREATURE))) { + return false; + } + break; + case ConditionType::SWORD_SHIELD: + if ((!attacker_sc_card_ce || (attacker_sc_card_ce->def.card_class() != CardClass::SWORD_ITEM)) && + (!attacker_card_ce || (attacker_card_ce->def.card_class() != CardClass::SWORD_ITEM))) { + return false; + } + break; + case ConditionType::GUN_SHIELD: + if ((!attacker_sc_card_ce || (attacker_sc_card_ce->def.card_class() != CardClass::GUN_ITEM)) && + (!attacker_card_ce || (attacker_card_ce->def.card_class() != CardClass::GUN_ITEM))) { + return false; + } + break; + case ConditionType::CANE_SHIELD: + if ((!attacker_sc_card_ce || (attacker_sc_card_ce->def.card_class() != CardClass::CANE_ITEM)) && + (!attacker_card_ce || (attacker_card_ce->def.card_class() != CardClass::CANE_ITEM))) { + return false; + } + break; + default: + break; + } + } + + return true; +} + +bool RulerServer::defense_card_matches_any_attack_card_top_color( + const ActionState& pa) const { + auto ce = this->definition_for_card_ref(pa.action_card_refs[0]); + if (!ce) { + throw runtime_error("defense card definition is missing"); + } + const auto* chain = this->action_chain_with_conds_for_card_ref( + pa.original_attacker_card_ref); + if (chain->chain.attack_action_card_ref_count < 1) { + auto other_ce = this->definition_for_card_ref(pa.original_attacker_card_ref); + if (other_ce && other_ce->def.any_top_color_matches(ce->def)) { + return true; + } + } + + for (size_t z = 0; z < chain->chain.attack_action_card_ref_count; z++) { + auto other_ce = this->definition_for_card_ref(chain->chain.attack_action_card_refs[z]); + if (other_ce && other_ce->def.any_top_color_matches(ce->def)) { + return true; + } + } + return false; +} + +shared_ptr RulerServer::definition_for_card_ref(uint16_t card_ref) const { + uint16_t card_id = this->card_id_for_card_ref(card_ref); + if (card_id == 0xFFFF) { + return nullptr; + } + return this->definition_for_card_id(card_id); +} + +int32_t RulerServer::error_code_for_client_setting_card( + uint8_t client_id, + uint16_t card_ref, + const Location* loc, + uint8_t assist_target_client_id) const { + if (client_id > 3) { + return -0x7D; + } + auto hes = this->hand_and_equip_states[client_id]; + if (!hes) { + return -0x7D; + } + + if (hes->assist_flags & 0x80) { + return -0x76; + } + + if (!this->is_card_ref_in_hand(card_ref)) { + return -0x5E; + } + + uint16_t card_id = this->card_id_for_card_ref(card_ref); + if ((hes->assist_flags & 0x200) && (card_id != 0xFFFF)) { + for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) { + auto other_hes = this->hand_and_equip_states[other_client_id]; + if (!other_hes) { + continue; + } + for (size_t z = 0; z < 8; z++) { + if (card_id == this->card_id_for_card_ref(other_hes->set_card_refs2[z])) { + return -0x76; + } + } + if (card_id == this->card_id_for_card_ref(other_hes->assist_card_ref2)) { + return -0x76; + } + } + } + + auto ce = this->definition_for_card_id(card_id); + if (!ce || + (static_cast(ce->def.type) > 0x05) || + (ce->def.type == CardType::HUNTERS_SC) || + (ce->def.type == CardType::ARKZ_SC) || + (ce->def.type == CardType::ACTION)) { + return -0x7D; + } + + if (ce->def.type == CardType::ASSIST) { + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + if (this->assist_server->get_active_assist_by_index(z) == AssistEffect::ASSISTLESS) { + return -0x76; + } + } + + // Check for assists that can only be set on yourself + auto eff = assist_effect_number_for_card_id(ce->def.card_id); + if (((eff == AssistEffect::LEGACY) || (eff == AssistEffect::EXCHANGE)) && + (assist_target_client_id != 0xFF) && + (assist_target_client_id != client_id_for_card_ref(card_ref))) { + return -0x75; + } + + } else if (hes->assist_flags & 0x400) { // Item or creature + return -0x76; + } + + int16_t set_cost = this->set_cost_for_card(client_id, card_ref); + if (set_cost < 0) { + return set_cost; + } + if (hes->atk_points < set_cost) { + return -0x80; + } + + auto short_statuses = this->short_statuses[client_id]; + if ((short_statuses->at(0).card_ref == 0xFFFF) || + !this->card_exists_by_status(short_statuses->at(0)) || + !this->check_usability_or_apply_condition_for_card_refs( + card_ref, short_statuses->at(0).card_ref, 0xFFFF, 0xFF, AttackMedium::INVALID_FF)) { + return -0x75; + } + + bool card_in_hand = false; + for (size_t z = 1; z < 7; z++) { + if (short_statuses->at(z).card_ref == card_ref) { + card_in_hand = true; + } + } + if (!card_in_hand) { + return -0x7D; + } + + if ((ce->def.type == CardType::ITEM) || (ce->def.type == CardType::CREATURE)) { + int16_t existing_fcs_cost = 0; + bool limit_summoning_by_count = this->find_condition_on_card_ref( + short_statuses->at(0).card_ref, ConditionType::FC_LIMIT_BY_COUNT); + for (size_t z = 7; z < 15; z++) { + const auto& this_status = short_statuses->at(z); + if ((this_status.card_ref != 0xFFFF) && this->card_exists_by_status(this_status)) { + auto this_ce = this->definition_for_card_ref(this_status.card_ref); + if (!this_ce) { + return -0x7D; + } + existing_fcs_cost += limit_summoning_by_count ? 2 : this_ce->def.self_cost; + } + } + + int16_t new_fcs_cost = existing_fcs_cost + (limit_summoning_by_count ? 2 : ce->def.self_cost); + if (new_fcs_cost > 8) { + return -0x77; + } + } + + if (ce->def.type == CardType::CREATURE) { + int16_t summon_cost = ce->def.self_cost; + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + if (this->assist_server->get_active_assist_by_index(z) == AssistEffect::FLATLAND) { + summon_cost = 0; + } + } + + if (loc && !this->map_and_rules->tile_is_vacant(loc->x, loc->y)) { + return -0x7E; + } + + uint8_t team_id = this->team_id_for_client_id[client_id]; + if (team_id == 0xFF) { + return -0x78; + } + if (!loc) { + return 0; + } + + Location summon_area_loc; + uint8_t summon_area_size; + if (!this->get_creature_summon_area( + client_id, &summon_area_loc, &summon_area_size)) { + if (team_id != 1) { + if ((loc->x > 0) && (loc->x < this->map_and_rules->map.width - 1)) { + if ((loc->y < this->map_and_rules->map.height - summon_cost - 1) && + (loc->y > 0)) { + return 0; + } + if (loc->y == 1) { + return 0; + } + } + } else { + if ((loc->x > 0) && + (loc->x < this->map_and_rules->map.width - 1)) { + if ((summon_cost + 1 <= loc->y) && (loc->y < this->map_and_rules->map.height - 1)) { + return 0; + } + if (loc->y == this->map_and_rules->map.height - 2) { + return 0; + } + } + } + return -0x7E; + } + + int32_t x_offset, y_offset; + this->offsets_for_direction(summon_area_loc, &x_offset, &y_offset); + if (x_offset == 0) { + if ((loc->x < 1) && (loc->x >= this->map_and_rules->map.width - 1)) { + return -0x7E; + } + } else { + int16_t diff = max(summon_area_size - summon_cost, 0); + if (x_offset > 0) { + if (loc->x < summon_area_loc.x) { + return -0x7E; + } + if (loc->x > summon_area_loc.x + diff) { + return -0x7E; + } + } else if (x_offset < 0) { + if ((loc->x > summon_area_loc.x) || (loc->x < summon_area_loc.x - diff)) { + return -0x7E; + } + } + } + if (y_offset == 0) { + if ((loc->y < 1) && (loc->y >= this->map_and_rules->map.height - 1)) { + return -0x7E; + } + } else { + int16_t diff = max(summon_area_size - summon_cost, 0); + if (y_offset > 0) { + if (loc->y < summon_area_loc.y) { + return -0x7E; + } + if (loc->y > summon_area_loc.y + diff) { + return -0x7E; + } + } else if (y_offset < 0) { + if ((loc->y > summon_area_loc.y) || (loc->y < summon_area_loc.y - diff)) { + return -0x7E; + } + } + } + } + return 0; +} + +bool RulerServer::find_condition_on_card_ref( + uint16_t card_ref, + ConditionType cond_type, + Condition* out_se, + size_t* out_value_sum, + bool find_first_instead_of_max) const { + const auto* chain = this->action_chain_with_conds_for_card_ref(card_ref); + if (!chain) { + return false; + } + + ssize_t found_value = 0; + ssize_t found_index = -1; + ssize_t found_order = 9; + for (size_t z = 0; z < 9; z++) { + if (chain->conditions[z].type == cond_type) { + if (!find_first_instead_of_max) { + if ((found_index == -1) || (found_order < chain->conditions[z].order)) { + found_order = chain->conditions[z].order; + found_index = z; + } + } else if ((found_index == -1) || (found_value < chain->conditions[z].value)) { + found_value = chain->conditions[z].value; + found_index = z; + } + if (out_value_sum) { + *out_value_sum = *out_value_sum + chain->conditions[z].value; + } + } + } + + if (found_index >= 0) { + if (out_se) { + *out_se = chain->conditions[found_index]; + } + return true; + } else { + return false; + } +} + +bool RulerServer::flood_fill_move_path( + const ActionChainWithConds& chain, + int8_t x, + int8_t y, + Direction direction, + uint8_t max_atk_points, + int16_t max_distance, + bool is_free_maneuver_or_aerial, + bool is_aerial, + parray* visited_map, + MovePath* path, + size_t num_occupied_tiles, + size_t num_vacant_tiles) const { + auto state = this->map_and_rules; + if ((x < 1) || (x >= state->map.width - 1) || + (y < 1) || (y >= state->map.height - 1)) { + return 0; + } + + bool ret = false; + bool tile_is_occupied = !state->tile_is_vacant(x, y); + if (tile_is_occupied) { + if (!is_free_maneuver_or_aerial) { + return 0; + } + + } else { + uint32_t cost = this->get_path_cost( + chain, + num_vacant_tiles + num_occupied_tiles + 1, + is_aerial ? num_occupied_tiles : 0); + if (max_atk_points < cost) { + return 0; + } + visited_map->at(x * 0x10 + y) = 1; + if (path && (path->end_loc.x == x) && (path->end_loc.y == y) && + ((path->length == -1) || (cost < path->cost))) { + ret = true; + path->reset_totals(); + path->remaining_distance = max_distance; + path->cost = cost; + Location step_loc(x, y, direction); + path->add_step(step_loc); + } + } + + if (tile_is_occupied) { + num_occupied_tiles = num_occupied_tiles + 1; + } else { + num_vacant_tiles = num_vacant_tiles + 1; + } + + int16_t new_max_distance = max_distance - 1; + if (new_max_distance > 0) { + static const int8_t offsets[4][2] = { + {1, 0}, {0, -1}, {-1, 0}, {0, 1}}; + Direction dirs[3] = {direction, turn_left(direction), turn_right(direction)}; + for (size_t dir_index = 0; dir_index < 3; dir_index++) { + if (static_cast(dirs[dir_index]) > 3) { + throw logic_error("invalid direction"); + } + ret |= this->flood_fill_move_path( + chain, + x + offsets[static_cast(dirs[dir_index])][0], + y + offsets[static_cast(dirs[dir_index])][1], + dirs[dir_index], + max_atk_points, + new_max_distance, + is_free_maneuver_or_aerial, + is_aerial, + visited_map, + path, + num_occupied_tiles, + num_vacant_tiles); + } + } + + if (path && ret) { + Location step_loc(x, y, direction); + path->add_step(step_loc); + if (tile_is_occupied) { + path->num_occupied_tiles++; + } + } + + return ret; +} + +uint16_t RulerServer::get_ally_sc_card_ref(uint16_t card_ref) const { + uint8_t client_id = client_id_for_card_ref(card_ref); + if ((client_id != 0xFF) && this->short_statuses[client_id]) { + for (size_t z = 0; z < 4; z++) { + if ((z != client_id) && + (this->team_id_for_client_id[z] == this->team_id_for_client_id[client_id]) && + this->short_statuses[z]) { + return this->short_statuses[z]->at(0).card_ref; + } + } + } + return 0xFFFF; +} + +shared_ptr RulerServer::definition_for_card_id( + uint32_t card_id) const { + return this->server()->definition_for_card_id(card_id); +} + +uint32_t RulerServer::get_card_id_with_effective_range( + uint16_t card_ref, uint16_t card_id_override, TargetMode* out_target_mode) const { + uint16_t card_id = (card_id_override == 0xFFFF) + ? this->card_id_for_card_ref(card_ref) : card_id_override; + + if (card_id != 0xFFFF) { + auto ce = this->definition_for_card_id(card_id); + uint8_t client_id = client_id_for_card_ref(card_ref); + if ((client_id != 0xFF) && ce) { + TargetMode effective_target_mode = ce->def.target_mode; + + if (this->card_ref_or_sc_has_fixed_range(card_ref)) { + // Undo the override that may have been passed in + auto ce = this->definition_for_card_id(this->card_id_for_card_ref(card_ref)); + if (ce && (static_cast(effective_target_mode) < 6)) { + effective_target_mode = ce->def.target_mode; + } + } + + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->assist_server->get_active_assist_by_index(z); + if (eff == AssistEffect::SIMPLE) { + card_id = this->card_id_for_card_ref(card_ref); + } else if (eff == AssistEffect::HEAVY_FOG) { + card_id = 0xFFFE; + } + } + + if (out_target_mode) { + *out_target_mode = effective_target_mode; + } + } + } + + return card_id; +} + +uint8_t RulerServer::get_card_ref_max_hp(uint16_t card_ref) const { + const auto* short_status = this->short_status_for_card_ref(card_ref); + if (short_status && (short_status->max_hp > 0)) { + return short_status->max_hp; + } + auto ce = this->definition_for_card_ref(card_ref); + if (!ce) { + return 0; + } else if (((ce->def.type == CardType::HUNTERS_SC) || (ce->def.type == CardType::ARKZ_SC)) && + (this->map_and_rules->rules.char_hp > 0) && + !this->card_ref_is_boss_sc(card_ref)) { + return this->map_and_rules->rules.char_hp; + } else { + return ce->def.hp.stat; + } +} + +bool RulerServer::get_creature_summon_area( + uint8_t client_id, Location* out_loc, uint8_t* out_region_size) const { + if (!this->map_and_rules || (client_id > 3)) { + return false; + } + + Location loc; + uint8_t region_size; + loc.direction = static_cast( + (this->map_and_rules->start_facing_directions >> ((client_id & 0x0F) << 2)) & 0x000F); + switch (loc.direction) { + case Direction::LEFT: + loc.x = 1; + loc.y = 0; + region_size = this->map_and_rules->map.width - 3; + break; + case Direction::RIGHT: + loc.x = this->map_and_rules->map.width - 2; + loc.y = 0; + region_size = this->map_and_rules->map.width - 3; + break; + case Direction::UP: + loc.x = 0; + loc.y = 1; + region_size = this->map_and_rules->map.height - 3; + break; + case Direction::DOWN: + loc.x = 0; + loc.y = this->map_and_rules->map.height - 2; + region_size = this->map_and_rules->map.height - 3; + break; + default: + // This case isn't in the original code; probably it fell through to one + // of the above + return false; + } + + if (out_loc) { + *out_loc = loc; + } + if (out_region_size) { + *out_region_size = region_size; + } + return true; +} + +shared_ptr RulerServer::get_hand_and_equip_state_for_client_id( + uint8_t client_id) { + return (client_id < 4) ? this->hand_and_equip_states[client_id] : nullptr; +} + +shared_ptr RulerServer::get_hand_and_equip_state_for_client_id( + uint8_t client_id) const { + return (client_id < 4) ? this->hand_and_equip_states[client_id] : nullptr; +} + +bool RulerServer::get_move_path_length_and_cost( + uint32_t client_id, + uint32_t card_ref, + const Location& loc, + uint32_t* out_length, + uint32_t* out_cost) const { + MovePath path; + parray visited_map; + path.end_loc = loc; + if (!this->check_move_path_and_get_cost( + client_id, card_ref, &visited_map, &path, out_cost)) { + return false; + } + + bool path_is_valid = path.is_valid(); + if (out_length) { + if (!path_is_valid || (path.get_length_plus1() < 2)) { + *out_length = 99; + } else { + *out_length = path.get_length_plus1() - 1; + } + } + + return ((path_is_valid && (path.get_length_plus1() > 1))); +} + +ssize_t RulerServer::get_path_cost( + const ActionChainWithConds& chain, + ssize_t path_length, + ssize_t cost_penalty) const { + for (size_t x = 0; x < 9; x++) { + const auto& cond = chain.conditions[x]; + if (cond.type == ConditionType::UNKNOWN_12) { + path_length = 0; + } else if (cond.type == ConditionType::UNKNOWN_15) { + path_length++; + } else if (cond.type == ConditionType::HASTE) { + path_length *= cond.value; + } + } + return clamp(path_length + cost_penalty, 0, 99); +} + +ActionType RulerServer::get_pending_action_type(const ActionState& pa) const { + auto ce = this->definition_for_card_ref(pa.action_card_refs[0]); + if (!ce || (ce->def.type != CardType::ACTION)) { + if (pa.attacker_card_ref == 0xFFFF) { + return ActionType::INVALID_00; + } else { + return ActionType::ATTACK; + } + } else { + if (ce->def.card_class() == CardClass::DEFENSE_ACTION) { + return ActionType::DEFENSE; + } else { + return ActionType::ATTACK; + } + } +} + +bool RulerServer::is_attack_valid(const ActionState& pa) { + uint8_t client_id = pa.client_id; + uint16_t attacker_card_ref = pa.attacker_card_ref; + if (client_id == 0xFF) { + this->error_code3 = -0x72; + return false; + } + + if (this->hand_and_equip_states[client_id] && + (this->hand_and_equip_states[client_id]->assist_flags & 0x80)) { + this->error_code3 = -0x70; + return false; + } + + // Note: The original code has a case here that results in error code -0x5E, + // triggered by a function returning false. However, that function always + // returns true and has no side effects, so we've omitted the case here. + + const auto* attacker_card_status = this->short_status_for_card_ref(attacker_card_ref); + if (!attacker_card_status || + !this->card_ref_can_attack(attacker_card_ref) || + (attacker_card_status->card_flags & 0x500)) { + this->error_code3 = -0x6F; + return false; + } + + if (attacker_card_status->card_flags & 2) { + this->error_code3 = -0x60; + return false; + } + + auto attacker_ce = this->definition_for_card_ref(attacker_card_ref); + auto attacker_chain = this->action_chain_with_conds_for_card_ref(attacker_card_ref); + if (!attacker_chain || + (attacker_chain->chain.acting_card_ref != attacker_card_ref) || + !attacker_ce || + ((attacker_ce->def.type != CardType::HUNTERS_SC && + (attacker_ce->def.type != CardType::ARKZ_SC) && + (attacker_ce->def.type != CardType::CREATURE) && + (attacker_ce->def.type != CardType::ITEM)))) { + this->error_code3 = -0x6F; + return false; + } + + uint16_t card_ref = attacker_chain->chain.unknown_card_ref_a3; + if (card_ref == 0xFFFF) { + card_ref = attacker_card_ref; + } + + bool has_permission_effect = false; + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->assist_server->get_active_assist_by_index(z); + if (eff == AssistEffect::PERMISSION) { + has_permission_effect = true; + } else if (eff == AssistEffect::SKIP_ACT) { + this->error_code3 = -0x6E; + return false; + } + } + + size_t conditional_card_count = 0; + size_t z; + for (z = 0; z < 9; z++) { + uint16_t right_card_ref = pa.action_card_refs[z]; + if (right_card_ref == 0xFFFF) { + break; + } + + if (client_id_for_card_ref(right_card_ref) != client_id) { + this->error_code3 = -0x6D; + return false; + } + + auto left_card_ce = (z == 0) ? this->definition_for_card_ref(card_ref) : this->definition_for_card_ref(pa.action_card_refs[z - 1]); + auto right_card_ce = this->definition_for_card_ref(right_card_ref); + + if (right_card_ce->def.type != CardType::ACTION) { + this->error_code3 = -0x6C; + return false; + } + if (!left_card_ce || !right_card_ce) { + this->error_code3 = -0x6C; + return false; + } + + uint8_t attacker_client_id = client_id_for_card_ref(pa.attacker_card_ref); + auto sc_ce = (attacker_client_id != 0xFF) ? this->definition_for_card_ref(this->set_card_action_chains[attacker_client_id]->at(0).chain.acting_card_ref) : nullptr; + + if (!card_linkage_is_valid(right_card_ce, left_card_ce, sc_ce, has_permission_effect)) { + this->error_code3 = -0x6B; + return false; + } + + if (!this->check_usability_or_apply_condition_for_card_refs( + right_card_ref, attacker_card_ref, 0xFFFF, 0xFF, AttackMedium::INVALID_FF)) { + this->error_code3 = -0x6A; + return false; + } + + if (this->card_ref_has_class_usability_condition(right_card_ref)) { + conditional_card_count = conditional_card_count + 1; + } + } + + if (z >= 9) { + this->error_code3 = -0x69; + return false; + } + + if ((attacker_ce->def.type == CardType::HUNTERS_SC) && ((z == 0) || (z != conditional_card_count))) { + auto short_statuses = this->short_statuses[client_id]; + for (z = 7; z < 15; z++) { + if (this->card_ref_can_attack(short_statuses->at(z).card_ref)) { + this->error_code3 = -0x68; + return false; + } + }; + } + + return true; +} + +bool RulerServer::is_attack_or_defense_valid(const ActionState& pa) { + // This error code is present in the original code, but is no longer possible + // since we require pa instead of using a pointer. + // if (!pa) { + // this->error_code3 = -0x78; + // return false; + // } + + auto hes = this->get_hand_and_equip_state_for_client_id(pa.client_id); + if (!hes) { + this->error_code3 = -0x72; + return false; + } + + if (hes->assist_flags & 0x80) { + this->error_code3 = -0x70; + return false; + } + + int16_t cost = this->compute_attack_or_defense_costs(pa, false, nullptr); + + switch (this->get_pending_action_type(pa)) { + case ActionType::ATTACK: + if (hes->atk_points < cost) { + this->error_code3 = -0x80; + return false; + } + return this->is_attack_valid(pa); + + case ActionType::DEFENSE: + if (hes->def_points < cost) { + this->error_code3 = -0x80; + return false; + } + if (!this->is_defense_valid(pa)) { + this->error_code3 = -0x80; + return false; + } + return true; + + case ActionType::INVALID_00: + default: + this->error_code3 = -0x5F; + return false; + } +} + +bool RulerServer::is_card_ref_in_hand(uint16_t card_ref) const { + if (card_ref == 0xFFFF) { + return true; + } + + uint8_t client_id = client_id_for_card_ref(card_ref); + auto hes = this->get_hand_and_equip_state_for_client_id(client_id); + if (!hes) { + return false; + } + + for (size_t z = 0; z < 6; z++) { + if (hes->hand_card_refs2[z] == card_ref) { + return true; + } + } + + return false; +} + +bool RulerServer::is_defense_valid(const ActionState& pa) { + if ((pa.original_attacker_card_ref == 0xFFFF) || + (pa.target_card_refs[0] == 0xFFFF) || + (pa.action_card_refs[0] == 0xFFFF)) { + this->error_code3 = -0x65; + return false; + } + + if (pa.client_id > 3) { + this->error_code3 = -0x65; + return false; + } + + if (this->hand_and_equip_states[pa.client_id] && + (this->hand_and_equip_states[pa.client_id]->assist_flags & 0x80)) { + this->error_code3 = -0x64; + return false; + } + + // Note: The original code has a case here that results in error code -0x5E, + // triggered by a function returning false. However, that function always + // returns true and has no side effects, so we've omitted the case here. + + const auto* stat = this->short_status_for_card_ref(pa.target_card_refs[0]); + if ((!stat || !this->card_exists_by_status(*stat)) || (stat->card_flags & 0x800)) { + this->error_code3 = -0x63; + return false; + } + + if (!this->defense_card_matches_any_attack_card_top_color(pa)) { + this->error_code3 = -0x62; + return false; + } + + if (!this->defense_card_can_apply_to_attack( + pa.action_card_refs[0], pa.target_card_refs[0], pa.original_attacker_card_ref)) { + this->error_code3 = -0x61; + return false; + } + + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(pa.client_id); + for (size_t z = 0; z < num_assists; z++) { + if (this->assist_server->get_active_assist_by_index(z) == AssistEffect::SKIP_ACT) { + this->error_code3 = -0x64; + return false; + } + } + + if (this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::HOLD) || + this->find_condition_on_card_ref(pa.target_card_refs[0], ConditionType::UNKNOWN_07)) { + this->error_code3 = -0x63; + return false; + } + + return true; +} + +void RulerServer::link_objects( + shared_ptr map_and_rules, + shared_ptr state_flags, + shared_ptr assist_server) { + this->map_and_rules = map_and_rules; + this->state_flags = state_flags; + this->assist_server = assist_server; +} + +size_t RulerServer::max_move_distance_for_card_ref(uint32_t card_ref) const { + uint16_t card_id = this->card_id_for_card_ref(card_ref); + uint8_t client_id = client_id_for_card_ref(card_ref); + if (card_id == 0xFFFF) { + return 0; + } + + auto ce = this->definition_for_card_ref(card_ref); + if (!ce) { + return 0; + } + + ssize_t ret = ce->def.mv.stat; + + Condition cond; + if (this->find_condition_on_card_ref(card_ref, ConditionType::MV_BONUS, &cond, nullptr, true)) { + ret += cond.value; + } + if (this->find_condition_on_card_ref(card_ref, ConditionType::SET_MV, &cond, nullptr, true)) { + ret = cond.value; + } + ret = max(0, ret); + + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id); + bool has_stamina_effect = false; + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->assist_server->get_active_assist_by_index(z); + if (eff == AssistEffect::SNAIL_PACE) { + return 1; + } + if (eff == AssistEffect::STAMINA) { + has_stamina_effect = true; + } + } + + return (has_stamina_effect) ? 9 : min(9, ret); +} + +RulerServer::MovePath::MovePath() + : length(-1), + remaining_distance(0), + num_occupied_tiles(0), + cost(0) { } + +void RulerServer::MovePath::add_step(const Location& loc) { + this->step_locs[++this->length] = loc; +} + +uint32_t RulerServer::MovePath::get_cost() const { + return this->cost; +} + +uint32_t RulerServer::MovePath::get_length_plus1() const { + return this->length + 1; +} + +void RulerServer::MovePath::reset_totals() { + this->length = -1; + this->remaining_distance = 0; + this->num_occupied_tiles = 0; + this->cost = 99; +} + +bool RulerServer::MovePath::is_valid() const { + return (this->length >= 0); +} + +void RulerServer::offsets_for_direction( + const Location& loc, int32_t* out_x_offset, int32_t* out_y_offset) { + // Note: This function has opposite behavior for the UP and DOWN directions + // as compared to the global array of the same name. + // TODO: Figure out why this difference exists and document it. + switch (loc.direction) { + case Direction::LEFT: + *out_x_offset = -1; + *out_y_offset = 0; + break; + case Direction::RIGHT: + *out_x_offset = 1; + *out_y_offset = 0; + break; + case Direction::UP: + *out_x_offset = 0; + *out_y_offset = 1; + break; + case Direction::DOWN: + *out_x_offset = 0; + *out_y_offset = -1; + break; + default: + break; + } +} + +void RulerServer::register_player( + uint8_t client_id, + shared_ptr hes, + shared_ptr> short_statuses, + shared_ptr deck_entry, + shared_ptr> set_card_action_chains, + shared_ptr> set_card_action_metadatas) { + this->hand_and_equip_states[client_id] = hes; + this->short_statuses[client_id] = short_statuses; + this->deck_entries[client_id] = deck_entry; + this->set_card_action_chains[client_id] = set_card_action_chains; + this->set_card_action_metadatas[client_id] = set_card_action_metadatas; +} + +void RulerServer::replace_D1_D2_rarity_cards_with_Attack( + parray& card_ids) const { + for (size_t z = 0; z < card_ids.size(); z++) { + auto ce = this->definition_for_card_id(card_ids[z]); + if (ce && ((ce->def.rarity == CardRarity::D1) || (ce->def.rarity == CardRarity::D2))) { + card_ids[z] = 0x008A; // Attack action card + } + } +} + +AttackMedium RulerServer::get_attack_medium(const ActionState& pa) const { + for (size_t z = 0; z < 8; z++) { + uint16_t card_ref = pa.action_card_refs[z]; + if (card_ref == 0xFFFF) { + return AttackMedium::PHYSICAL; + } + auto ce = this->definition_for_card_ref(card_ref); + if (ce && card_class_is_tech_like(ce->def.card_class())) { + return AttackMedium::TECH; + } + } + return AttackMedium::PHYSICAL; +} + +void RulerServer::set_client_team_id(uint8_t client_id, uint8_t team_id) { + this->team_id_for_client_id[client_id] = team_id; +} + +int32_t RulerServer::set_cost_for_card(uint8_t client_id, uint16_t card_ref) const { + auto ce = this->definition_for_card_ref(card_ref); + if (!ce) { + return -0x7D; + } + + if ((client_id == 0xFF) || (client_id != client_id_for_card_ref(card_ref))) { + return -0x7D; + } + + auto short_statuses = this->short_statuses[client_id]; + int32_t ret = ce->def.self_cost; + if (short_statuses && + this->card_exists_by_status(short_statuses->at(0)) && + this->find_condition_on_card_ref(short_statuses->at(0).card_ref, ConditionType::UNKNOWN_69)) { + ret = 0; + } + + for (size_t z = 0; z < 4; z++) { + auto other_short_statuses = this->short_statuses[z]; + if (!other_short_statuses) { + continue; + } + + Condition cond; + if (this->card_exists_by_status(other_short_statuses->at(0)) && + this->find_condition_on_card_ref(other_short_statuses->at(0).card_ref, ConditionType::CLONE, &cond) && + (static_cast(cond.value) == ce->def.card_id)) { + ret = 0; + } + + for (size_t w = 7; w < 15; w++) { + const auto& stat = other_short_statuses->at(w); + if (this->card_exists_by_status(stat) && + this->find_condition_on_card_ref(stat.card_ref, ConditionType::CLONE, &cond) && + (static_cast(cond.value) == ce->def.card_id)) { + ret = 0; + } + } + } + + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->assist_server->get_active_assist_by_index(z); + if (eff == AssistEffect::LAND_PRICE) { + // Note: Original code had an extra addend (ret < 0 && (ret & 1) != 0), + // but ret cannot be negatve here, so we omit it. + ret += ret >> 1; + } else if (eff == AssistEffect::DEFLATION) { + ret = max(0, ret - 1); + } else if (eff == AssistEffect::INFLATION) { + ret++; + } + } + + return ret; +} + +const CardShortStatus* RulerServer::short_status_for_card_ref(uint16_t card_ref) const { + uint8_t client_id = client_id_for_card_ref(card_ref); + if (client_id != 0xFF) { + for (size_t z = 0; z < 16; z++) { + const auto* stat = &this->short_statuses[client_id]->at(z); + if (stat->card_ref == card_ref) { + return stat; + } + } + } + return nullptr; +} + +bool RulerServer::should_allow_attacks_on_current_turn() const { + return (this->state_flags && + ((this->state_flags->turn_num > 1) || + (this->state_flags->current_team_turn1 != this->state_flags->first_team_turn))); +} + +int32_t RulerServer::verify_deck( + const parray& card_ids, + const parray* owned_card_counts) const { + for (size_t z = 0; z < card_ids.size(); z++) { + if (!this->definition_for_card_id(card_ids.at(z))) { + return -0x7C; + } + } + + auto sc_card_ce = this->definition_for_card_id(card_ids.at(0)); + if (!sc_card_ce) { + return -0x80; + } + + bool is_arkz_sc; + if (sc_card_ce->def.type == CardType::ARKZ_SC) { + is_arkz_sc = true; + } else if (sc_card_ce->def.type == CardType::HUNTERS_SC) { + is_arkz_sc = false; + } else { + return -0x80; + } + + for (size_t z = 1; z < card_ids.size(); z++) { + size_t count = 0; + for (size_t w = 1; w < card_ids.size(); w++) { + if (card_ids.at(z) == card_ids.at(w)) { + count++; + } + } + if (count > 3) { + return -0x7F; + } + + if (owned_card_counts && (owned_card_counts->at(card_ids[z]) < count)) { + return -0x7B; + } + + auto ce = this->definition_for_card_id(card_ids[z]); + if (!ce) { + return -0x7A; + } + + if ((ce->def.type == CardType::HUNTERS_SC) || (ce->def.type == CardType::ARKZ_SC)) { + return -0x7A; + } else if ((ce->def.type == CardType::ITEM) && is_arkz_sc) { + return -0x7E; + } else if ((ce->def.type == CardType::CREATURE) && !is_arkz_sc) { + return -0x7D; + } + } + + return 0; +} + + + +} // namespace Episode3 diff --git a/src/Episode3/RulerServer.hh b/src/Episode3/RulerServer.hh new file mode 100644 index 00000000..ae2945df --- /dev/null +++ b/src/Episode3/RulerServer.hh @@ -0,0 +1,232 @@ +#pragma once + +#include + +#include + +#include "DataIndex.hh" +#include "PlayerState.hh" +#include "DeckState.hh" +#include "AssistServer.hh" + +namespace Episode3 { + + + +class Server; + +void compute_effective_range( + parray& ret, + std::shared_ptr data_index, + uint16_t card_id, + const Location& loc, + std::shared_ptr map_and_rules); + +bool card_linkage_is_valid( + std::shared_ptr right_def, + std::shared_ptr left_def, + std::shared_ptr sc_def, + bool has_permission_effect); + +class RulerServer { +public: + struct MovePath { + int32_t length; + uint32_t remaining_distance; + Location end_loc; + parray step_locs; + uint32_t num_occupied_tiles; + uint32_t cost; + + MovePath(); + void add_step(const Location& loc); + uint32_t get_cost() const; + uint32_t get_length_plus1() const; + void reset_totals(); + bool is_valid() const; + }; + + explicit RulerServer(std::shared_ptr server); + std::shared_ptr server(); + std::shared_ptr server() const; + + ActionChainWithConds* action_chain_with_conds_for_card_ref( + uint16_t card_ref); + const ActionChainWithConds* action_chain_with_conds_for_card_ref( + uint16_t card_ref) const; + bool any_attack_action_card_is_support_tech_or_support_pb( + const ActionState& pa) const; + bool card_has_pierce_or_rampage( + uint8_t client_id, + ConditionType cond_type, + bool* out_has_rampage, + uint16_t attacker_card_ref, + uint16_t action_card_ref, + uint8_t def_effect_index, + AttackMedium attack_medium) const; + bool attack_action_has_rampage_and_not_pierce( + const ActionState& pa, uint16_t card_ref) const; + bool attack_action_has_pierce_and_not_rampage( + const ActionState& pa, uint8_t client_id); + bool card_exists_by_status(const CardShortStatus& stat) const; + bool card_has_mighty_knuckle(uint32_t card_ref) const; + uint16_t card_id_for_card_ref(uint16_t card_ref) const; + static bool card_id_is_boss_sc(uint16_t card_id); + static bool card_id_is_support_tech_or_support_pb(uint16_t card_id); + bool card_ref_can_attack(uint16_t card_ref); + bool card_ref_can_move( + uint8_t client_id, uint16_t card_ref, bool ignore_atk_points) const; + bool card_ref_has_class_usability_condition( + uint16_t card_ref) const; + bool card_ref_has_free_maneuver(uint16_t card_ref) const; + bool card_ref_is_aerial(uint16_t card_ref) const; + bool card_ref_is_aerial_or_has_free_maneuver( + uint16_t card_ref) const; + bool card_ref_is_boss_sc(uint32_t card_ref) const; + bool card_ref_or_any_set_card_has_condition_46( + uint16_t card_ref) const; + bool card_ref_or_sc_has_fixed_range(uint16_t card_ref) const; + bool check_move_path_and_get_cost( + uint8_t client_id, + uint16_t card_ref, + parray* visited_map, + MovePath* out_path, + uint32_t* out_cost) const; + bool check_pierce_and_rampage( + uint16_t card_ref, + ConditionType cond_type, + bool* out_has_pierce, + uint16_t attacker_card_ref, + uint16_t action_card_ref, + uint8_t def_effect_index, + AttackMedium attack_medium) const; + bool check_usability_or_apply_condition_for_card_refs( + uint16_t card_ref1, + uint16_t card_ref2, + uint16_t card_ref3, + uint8_t def_effect_index, + AttackMedium attack_medium) const; + bool check_usability_or_condition_apply( + uint8_t client_id1, + uint16_t card_id1, + uint8_t client_id2, + uint16_t card_id2, + uint16_t card_id3, + uint8_t def_effect_index, + bool is_condition_check, + AttackMedium attack_medium) const; + uint16_t compute_attack_or_defense_costs( + const ActionState& pa, + bool allow_mighty_knuckle, + uint8_t* out_ally_cost) const; + bool compute_effective_range_and_target_mode_for_attack( + const ActionState& pa, + uint16_t* out_effective_card_id, + TargetMode* out_effective_target_mode, + uint16_t* out_orig_card_ref) const; + size_t count_rampage_targets_for_attack( + const ActionState& pa, uint8_t client_id) const; + bool defense_card_can_apply_to_attack( + uint16_t defense_card_ref, + uint16_t attacker_card_ref, + uint16_t attacker_sc_card_ref) const; + bool defense_card_matches_any_attack_card_top_color( + const ActionState& pa) const; + std::shared_ptr definition_for_card_ref(uint16_t card_ref) const; + int32_t error_code_for_client_setting_card( + uint8_t client_id, + uint16_t card_ref, + const Location* loc, + uint8_t assist_target_client_id) const; + bool find_condition_on_card_ref( + uint16_t card_ref, + ConditionType cond_type, + Condition* out_se = nullptr, + size_t* out_value_sum = nullptr, + bool find_first_instead_of_max = false) const; + bool flood_fill_move_path( + const ActionChainWithConds& chain, + int8_t x, + int8_t y, + Direction direction, + uint8_t max_atk_points, + int16_t max_distance, + bool is_free_maneuver_or_aerial, + bool is_aerial, + parray* visited_map, + MovePath* path, + size_t num_occupied_tiles, + size_t num_vacant_tiles) const; + uint16_t get_ally_sc_card_ref(uint16_t card_ref) const; + std::shared_ptr definition_for_card_id( + uint32_t card_id) const; + uint32_t get_card_id_with_effective_range( + uint16_t card_ref, uint16_t card_id_override, TargetMode* out_target_mode) const; + uint8_t get_card_ref_max_hp(uint16_t card_ref) const; + bool get_creature_summon_area( + uint8_t client_id, Location* out_loc, uint8_t* out_region_size) const; + std::shared_ptr get_hand_and_equip_state_for_client_id( + uint8_t client_id); + std::shared_ptr get_hand_and_equip_state_for_client_id( + uint8_t client_id) const; + bool get_move_path_length_and_cost( + uint32_t client_id, + uint32_t card_ref, + const Location& loc, + uint32_t* out_length, + uint32_t* out_cost) const; + ssize_t get_path_cost( + const ActionChainWithConds& chain, + ssize_t path_length, + ssize_t cost_penalty) const; + ActionType get_pending_action_type(const ActionState& pa) const; + bool is_attack_valid(const ActionState& pa); + bool is_attack_or_defense_valid(const ActionState& pa); + bool is_card_ref_in_hand(uint16_t card_ref) const; + bool is_defense_valid(const ActionState& pa); + void link_objects( + std::shared_ptr map_and_rules, + std::shared_ptr state_flags, + std::shared_ptr assist_server); + size_t max_move_distance_for_card_ref(uint32_t card_ref) const; + static void offsets_for_direction( + const Location& loc, int32_t* out_x_offset, int32_t* out_y_offset); + void register_player( + uint8_t client_id, + std::shared_ptr hes, + std::shared_ptr> short_statuses, + std::shared_ptr deck_entry, + std::shared_ptr> set_card_action_chains, + std::shared_ptr> set_card_action_metadatas); + void replace_D1_D2_rarity_cards_with_Attack( + parray& card_ids) const; + AttackMedium get_attack_medium(const ActionState& pa) const; + void set_client_team_id(uint8_t client_id, uint8_t team_id); + int32_t set_cost_for_card(uint8_t client_id, uint16_t card_ref) const; + const CardShortStatus* short_status_for_card_ref(uint16_t card_ref) const; + bool should_allow_attacks_on_current_turn() const; + int32_t verify_deck( + const parray& card_ids, + const parray* owned_card_counts = nullptr) const; + +private: + std::weak_ptr w_server; + +public: + std::shared_ptr hand_and_equip_states[4]; + std::shared_ptr> short_statuses[4]; + std::shared_ptr deck_entries[4]; + std::shared_ptr> set_card_action_chains[4]; + std::shared_ptr> set_card_action_metadatas[4]; + std::shared_ptr map_and_rules; + std::shared_ptr state_flags; + std::shared_ptr assist_server; + parray team_id_for_client_id; + int32_t error_code1; + int32_t error_code2; + int32_t error_code3; +}; + + + +} // namespace Episode3 diff --git a/src/Episode3/Server.cc b/src/Episode3/Server.cc new file mode 100644 index 00000000..966ea98c --- /dev/null +++ b/src/Episode3/Server.cc @@ -0,0 +1,2427 @@ +#include "Server.hh" + +#include + +#include "../SendCommands.hh" + +using namespace std; + +namespace Episode3 { + + + +static const char* VERSION_SIGNATURE = "[V1][FINAL2.0] 03/09/13 15:30 by K.Toya"; +static const char* SIGNATURE_DATE = "Jan 21 2004 18:36:47"; + + + +ServerBase::PresenceEntry::PresenceEntry() { + this->clear(); +} + +void ServerBase::PresenceEntry::clear() { + this->player_present = 0; + this->deck_valid = 0; + this->is_cpu_player = 0; +} + + + +ServerBase::ServerBase( + shared_ptr lobby, + shared_ptr data_index, + uint32_t behavior_flags, + uint32_t random_seed) + : lobby(lobby), + data_index(data_index), + behavior_flags(behavior_flags), + random_seed(random_seed) { } + +void ServerBase::init() { + this->reset(); +} + +void ServerBase::reset() { + this->map_and_rules1.reset(new MapAndRulesState()); + this->map_and_rules2.reset(new MapAndRulesState()); + this->num_clients_present = 0; + this->overlay_state.clear(); + for (size_t z = 0; z < 4; z++) { + this->presence_entries[z].clear(); + this->deck_entries[z].reset(new DeckEntry()); + this->name_entries[z].clear(); + this->name_entries_valid[z] = false; + } + this->recreate_server(); +} + +void ServerBase::recreate_server() { + this->server.reset(new Server(this->shared_from_this())); + this->server->init(); +} + + + +Server::Server(shared_ptr base) + : w_base(base), + battle_finished(false), + battle_in_progress(false), + round_num(1), + battle_phase(BattlePhase::INVALID_00), + first_team_turn(0xFF), + current_team_turn1(0xFF), + setup_phase(SetupPhase::REGISTRATION), + registration_phase(RegistrationPhase::AWAITING_NUM_PLAYERS), + action_subphase(ActionSubphase::ATTACK), + current_team_turn2(0xFF), + num_pending_attacks(0), + client_done_enqueuing_attacks(false), + player_ready_to_end_phase(false), + unknown_a10(0), + overall_time_expired(false), + battle_start_usecs(0), + should_copy_prev_states_to_current_states(0), + card_special(nullptr), + clients_done_in_mulligan_phase(false), + num_pending_attacks_with_cards(0), + unknown_a14(0), + unknown_a15(0), + defense_list_ended_for_client(false), + next_assist_card_set_number(1), + team_exp(0), + team_dice_boost(0), + team_client_count(0), + team_num_ally_fcs_destroyed(0), + team_num_cards_destroyed(0), + hard_reset_flag(false), + tournament_flag(0), + num_trap_tiles_of_type(0), + chosen_trap_tile_index_of_type(0), + has_done_pb(0), + num_6xB4x06_commands_sent(0), + prev_num_6xB4x06_commands_sent(0) { } + +void Server::init() { + this->card_special.reset(new CardSpecial(this->shared_from_this())); + + // The default PSOV2Encryption constructor in the original implementation just + // uses 0 as the seed. We'll replace this object later when the battle starts. + this->random_crypt.reset(new PSOV2Encryption(0)); + this->state_flags.reset(new StateFlags()); + + this->clear_player_flags_after_dice_phase(); + + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + + this->assist_server.reset(new AssistServer(this->shared_from_this())); + this->ruler_server.reset(new RulerServer(this->shared_from_this())); + 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); +} + +shared_ptr Server::base() { + auto s = this->w_base.lock(); + if (!s) { + throw runtime_error("server base is deleted"); + } + return s; +} + +shared_ptr Server::base() const { + auto s = this->w_base.lock(); + if (!s) { + throw runtime_error("server base is deleted"); + } + return s; +} + +void Server::send(const void* data, size_t size) const { + auto l = this->base()->lobby.lock(); + if (!l) { + throw runtime_error("lobby is deleted"); + } + send_command(l, 0xC9, 0x00, data, size); +} + +void Server::add_team_exp(uint8_t team_id, int32_t exp) { + size_t num_assists = this->assist_server->compute_num_assist_effects_for_team(team_id); + for (size_t z = 0; z < num_assists; z++) { + if (this->assist_server->get_active_assist_by_index(z) == AssistEffect::GOLD_RUSH) { + exp += (exp / 2); + } + } + + this->team_exp[team_id] = clamp( + this->team_exp[team_id] + exp, 0, this->team_client_count[team_id] * 96); + + uint8_t dice_boost = this->team_exp[team_id] / (this->team_client_count[team_id] * 12); + this->card_special->adjust_dice_boost_if_team_has_condition_52(team_id, &dice_boost, 0); + this->team_dice_boost[team_id] = min(dice_boost, 8); +} + +bool Server::advance_battle_phase() { + switch (this->battle_phase) { + case BattlePhase::DICE: + this->dice_phase_after(); + this->set_phase_before(); + break; + case BattlePhase::SET: + this->set_phase_after(); + this->move_phase_before(); + break; + case BattlePhase::MOVE: + this->move_phase_after(); + this->action_phase_before(); + break; + case BattlePhase::ACTION: + this->action_phase_after(); + this->draw_phase_before(); + break; + case BattlePhase::DRAW: + this->draw_phase_after(); + this->dice_phase_before(); + break; + default: + throw logic_error("invalid battle phase"); + } + return this->check_for_battle_end(); +} + +void Server::action_phase_after() { + this->send_6xB4x02_for_all_players_if_needed(); + this->battle_phase = BattlePhase::DRAW; +} + +void Server::draw_phase_before() { + for (size_t z = 0; z < 4; z++) { + if (this->player_states[z]) { + this->player_states[z]->unknown_80239460(); + } + } +} + +shared_ptr Server::definition_for_card_ref(uint16_t card_ref) const { + try { + return this->base()->data_index->definition_for_card_id( + this->card_id_for_card_ref(card_ref)); + } catch (const out_of_range&) { + return nullptr; + } +} + +shared_ptr Server::card_for_set_card_ref(uint16_t card_ref) { + if (card_ref == 0xFFFF) { + return nullptr; + } + uint8_t client_id = client_id_for_card_ref(card_ref); + if (client_id == 0xFF) { + return nullptr; + } + auto ps = this->player_states[client_id]; + if (!ps) { + return nullptr; + } + auto card = ps->get_sc_card(); + if (card && (card->get_card_ref() == card_ref)) { + return card; + } + for (size_t set_index = 0; set_index < 8; set_index++) { + card = ps->get_set_card(set_index); + if (card && (card->get_card_ref() == card_ref)) { + return card; + } + } + return nullptr; +} + +shared_ptr Server::card_for_set_card_ref(uint16_t card_ref) const { + // TODO: It'd be nice to deduplicate this function with the non-const version. + if (card_ref == 0xFFFF) { + return nullptr; + } + uint8_t client_id = client_id_for_card_ref(card_ref); + if (client_id == 0xFF) { + return nullptr; + } + auto ps = this->player_states[client_id]; + if (!ps) { + return nullptr; + } + auto card = ps->get_sc_card(); + if (card && (card->get_card_ref() == card_ref)) { + return card; + } + for (size_t set_index = 0; set_index < 8; set_index++) { + card = ps->get_set_card(set_index); + if (card && (card->get_card_ref() == card_ref)) { + return card; + } + } + return nullptr; +} + +uint16_t Server::card_id_for_card_ref(uint16_t card_ref) const { + uint8_t client_id = client_id_for_card_ref(card_ref); + if (client_id != 0xFF) { + if (!this->player_states[client_id]) { + return 0xFFFF; + } + auto deck = this->player_states[client_id]->get_deck(); + if (deck) { + return deck->card_id_for_card_ref(card_ref); + } + } + return 0xFFFF; +} + +bool Server::card_ref_is_empty_or_has_valid_card_id(uint16_t card_ref) const { + if (card_ref == 0xFFFF) { + return true; + } else { + return this->card_id_for_card_ref(card_ref) != 0xFFFF; + } +} + +bool Server::check_for_battle_end() { + bool ret = false; + if (this->base()->map_and_rules1->rules.hp_type == HPType::DEFEAT_TEAM) { + bool teams_defeated[2] = {true, true}; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (!ps) { + continue; + } + auto sc_card = ps->get_sc_card(); + if (sc_card && !sc_card->check_card_flag(2)) { + teams_defeated[ps->get_team_id()] = false; + } + } + + if (!teams_defeated[0] || !teams_defeated[1]) { + if (teams_defeated[0] || teams_defeated[1]) { + ret = true; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps) { + ps->assist_flags &= 0xFFFFB7FB; + if (teams_defeated[ps->get_team_id()] == 0) { + ps->assist_flags |= 4; + } + } + } + } + } else { // Both teams defeated?? I guess this is technically possible + ret = true; + this->unknown_8023D4E0(0x4000); + } + + } else { // Not DEFEAT_TEAM + bool teams_alive[2] = {false, false}; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (!ps) { + continue; + } + auto sc_card = ps->get_sc_card(); + if (sc_card && sc_card->check_card_flag(2)) { + teams_alive[ps->get_team_id()] = true; + } + } + + if (!teams_alive[0] || !teams_alive[1]) { + if (teams_alive[0] || teams_alive[1]) { + ret = true; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps) { + ps->assist_flags &= 0xFFFFB7FB; + if (!teams_alive[ps->get_team_id()]) { + ps->assist_flags |= 4; + } + } + } + } + } else { + ret = true; + this->unknown_8023D4E0(0x4000); + } + } + + if (ret) { + this->set_battle_ended(); + } + return ret; +} + +void Server::check_for_destroyed_cards_and_send_6xB4x05_6xB4x02() { + for (size_t z = 0; z < 4; z++) { + if (this->player_states[z]) { + this->player_states[z]->on_cards_destroyed(); + } + } + this->send_6xB4x05(); + this->send_6xB4x02_for_all_players_if_needed(); +} + +bool Server::check_presence_entry(uint8_t client_id) { + return (client_id < 4) + ? this->base()->presence_entries[client_id].player_present : false; +} + +void Server::clear_player_flags_after_dice_phase() { + this->player_ready_to_end_phase.clear(0); + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->assist_flags = ps->assist_flags & 0xFFFFDFFE; + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + } +} + +void Server::compute_all_map_occupied_bits() { + for (size_t y = 0; y < 0x10; y++) { + for (size_t x = 0; x < 0x10; x++) { + this->base()->map_and_rules1->clear_occupied_bit_for_tile(x, y); + } + } + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->set_map_occupied_bits_for_sc_and_creatures(); + } + } +} + +void Server::compute_team_dice_boost(uint8_t team_id) { + this->team_dice_boost[team_id] = clamp( + this->team_exp[team_id] / (this->team_client_count[team_id] * 12), 0, 8); +} + +void Server::copy_player_states_to_prev_states() { + if (this->should_copy_prev_states_to_current_states != 1) { + this->should_copy_prev_states_to_current_states = 1; + this->num_6xB4x06_commands_sent = 0; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->prev_set_card_action_chains = *ps->set_card_action_chains; + ps->prev_set_card_action_metadatas = *ps->set_card_action_metadatas; + ps->prev_card_short_statuses = *ps->card_short_statuses; + } + } + } +} + +shared_ptr Server::definition_for_card_id(uint16_t card_id) const { + try { + return this->base()->data_index->definition_for_card_id(card_id); + } catch (const out_of_range&) { + return nullptr; + } +} + +void Server::destroy_cards_with_zero_hp() { + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps) { + bool any_card_destroyed = false; + for (ssize_t set_index = -1; set_index < 8; set_index++) { + auto card = (set_index < 0) ? ps->get_sc_card() : ps->get_set_card(set_index); + if (card && !(card->card_flags & 2) && (card->get_current_hp() < 1)) { + card->destroy_set_card(card->w_destroyer_sc_card.lock()); + any_card_destroyed = true; + } + } + if (any_card_destroyed) { + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + } + } +} + +void Server::determine_first_team_turn() { + this->team_client_count[0] = this->base()->map_and_rules1->num_team0_players; + this->team_client_count[1] = this->base()->map_and_rules1->num_players - this->team_client_count[0]; + this->first_team_turn = 0xFF; + while (this->first_team_turn == 0xFF) { + uint8_t results[2] = {0, 0}; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps) { + results[ps->get_team_id()] += ps->roll_dice(1); + } + } + // Handle unbalanced team sizes by weighting the results by the other team's + // player count + results[0] *= this->team_client_count[1]; + results[1] *= this->team_client_count[0]; + if (results[1] < results[0]) { + this->first_team_turn = 0; + } else if (results[0] < results[1]) { + this->first_team_turn = 1; + } + } + this->current_team_turn1 = this->first_team_turn; + this->current_team_turn2 = this->current_team_turn1; +} + +void Server::dice_phase_after() { + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (!ps) { + continue; + } + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + auto eff = this->assist_server->get_active_assist_by_index(z); + if ((eff == AssistEffect::CHARITY_PLUS) || (eff == AssistEffect::CHARITY)) { + int16_t exp_delta = (eff == AssistEffect::CHARITY_PLUS) ? -1 : 1; + for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) { + auto other_ps = this->player_states[other_client_id]; + if (other_ps && this->current_team_turn2 == other_ps->get_team_id()) { + for (size_t die_index = 0; die_index < 2; die_index++) { + if (other_ps->get_dice_result(die_index) >= 5) { + this->add_team_exp(ps->get_team_id(), exp_delta); + } + } + } + } + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + } + } + } + this->battle_phase = BattlePhase::SET; +} + +void Server::set_phase_before() { + for (size_t z = 0; z < 4; z++) { + if (this->player_states[z]) { + this->player_states[z]->handle_before_turn_assist_effects(); + } + } + this->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02(); +} + +void Server::draw_phase_after() { + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps) { + if (ps->draw_cards_allowed()) { + ps->draw_hand(0); + } + if (ps->is_team_turn()) { + ps->compute_team_dice_boost_after_draw_phase(); + } + } + } + + this->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02(); + this->battle_phase = BattlePhase::DICE; + this->current_team_turn1 ^= 1; + this->round_num++; + + if (this->current_team_turn1 == this->first_team_turn) { + if (this->base()->map_and_rules1->rules.overall_time_limit > 0) { + // Battle time limits are specified in increments of 5 minutes. + // Note: This part is not based on the original code because the timing + // facilities used are different. + uint64_t limit_5mins = this->base()->map_and_rules1->rules.overall_time_limit; + uint64_t end_usecs = this->battle_start_usecs + (limit_5mins * 300 * 1000 * 1000); + if (now() >= end_usecs) { + this->overall_time_expired = true; + } + } + + if (this->overall_time_expired || (this->round_num >= 1000)) { + bool unknown_v1 = true; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps && (ps->assist_flags & 4)) { + unknown_v1 = false; + break; + } + } + if (unknown_v1) { + this->unknown_8023D4E0(0); + } + this->round_num--; + this->set_battle_ended(); + } + } +} + +void Server::dice_phase_before() { + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->unknown_8023C174(); + } + this->client_done_enqueuing_attacks[z] = 0; + } + this->destroy_cards_with_zero_hp(); + this->check_for_destroyed_cards_and_send_6xB4x05_6xB4x02(); + this->check_for_battle_end(); + this->send_6xB4x02_for_all_players_if_needed(); + this->action_subphase = ActionSubphase::ATTACK; + this->current_team_turn2 = this->current_team_turn1; + this->num_pending_attacks = 0; + this->num_pending_attacks_with_cards = 0; + this->unknown_a14 = 0; + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); +} + +void Server::end_attack_list_for_client(uint8_t client_id) { + if (client_id >= 4) { + return; + } + + auto ps = this->player_states[client_id]; + if (!ps) { + return; + } + + if (this->current_team_turn2 == ps->get_team_id()) { + this->client_done_enqueuing_attacks[client_id] = true; + } + + bool all_clients_done_enqueuing_attacks = true; + for (size_t other_client_id = 0; other_client_id < 4; other_client_id++) { + auto other_ps = this->player_states[other_client_id]; + if (!other_ps) { + continue; + } + auto card = other_ps->get_sc_card(); + if (card && + !card->check_card_flag(2) && + (other_ps->get_team_id() == this->current_team_turn2) && + (this->client_done_enqueuing_attacks[other_client_id] == 0)) { + all_clients_done_enqueuing_attacks = false; + } + } + + if (all_clients_done_enqueuing_attacks) { + for (size_t z = 0; z < 4; z++) { + auto other_ps = this->player_states[z]; + if (other_ps) { + other_ps->assist_flags &= 0xFFFFDFFF; + other_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + } + this->end_action_phase(); + this->client_done_enqueuing_attacks.clear(false); + + } else { + ps->assist_flags |= 0x2000; + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } +} + +void Server::end_action_phase() { + this->num_pending_attacks = 0; + this->unknown_a15 = 1; + // Annoyingly, this is the original logic. We use an enum because it appears + // that this can only ever be 0 or 2, but we may have to delete the enum if + // that turns out to be false. + this->action_subphase = static_cast(static_cast(this->action_subphase) + 2); + this->copy_player_states_to_prev_states(); + this->unknown_8023EEF4(); + this->send_set_card_updates_and_6xB4x04_if_needed(); +} + +bool Server::enqueue_attack_or_defense(uint8_t client_id, ActionState* pa) { + if (client_id >= 4) { + this->ruler_server->error_code3 = -0x78; + return false; + } + + auto ps = this->player_states[client_id]; + if (!ps) { + this->ruler_server->error_code3 = -0x72; + return false; + } + + if (pa->action_card_refs[0] == 0xFFFF) { + if (pa->defense_card_ref != 0xFFFF) { + pa->action_card_refs[0] = pa->defense_card_ref; + } + } else { + pa->defense_card_ref = pa->action_card_refs[0]; + } + + if (!this->ruler_server->is_attack_or_defense_valid(*pa)) { + return false; + } + + int16_t ally_atk_result = this->send_6xB4x33_remove_ally_atk_if_needed(*pa); + if (ally_atk_result == 1) { + return true; + } else if (ally_atk_result == -1) { + return false; + } + + if (this->num_pending_attacks >= 0x20) { + this->ruler_server->error_code3 = -0x71; + return false; + } + + size_t attack_index = this->num_pending_attacks++; + this->pending_attacks[attack_index] = *pa; + ps->set_action_cards_for_action_state(*pa); + auto card = this->card_for_set_card_ref(this->send_6xB4x06_if_card_ref_invalid( + pa->attacker_card_ref, 1)); + if (card) { + card->card_flags |= 0x400; + auto card_ps = card->player_state(); + if (card_ps) { + card_ps->send_6xB4x04_if_needed(); + } + } + card = this->card_for_set_card_ref(this->send_6xB4x06_if_card_ref_invalid( + pa->original_attacker_card_ref, 2)); + if (card) { + card = this->card_for_set_card_ref(pa->target_card_refs[0]); + if (card) { + card->card_flags |= 0x800; + card->player_state()->send_6xB4x04_if_needed(); + } + } + return true; +} + +BattlePhase Server::get_battle_phase() const { + return this->battle_phase; +} + +ActionSubphase Server::get_current_action_subphase() const { + return this->action_subphase; +} + +uint8_t Server::get_current_team_turn() const { + return this->current_team_turn1; +} + +shared_ptr Server::get_player_state(uint8_t client_id) { + if (client_id >= 4) { + return nullptr; + } + return this->player_states[client_id]; +} + +shared_ptr Server::get_player_state(uint8_t client_id) const { + if (client_id >= 4) { + return nullptr; + } + return this->player_states[client_id]; +} + +uint32_t Server::get_random(uint32_t max) { + // The original implementation was essentially: + // return (static_cast(this->random_crypt->next() >> 16) / 65536.0) * max + // This is unnecessarily complicated, so we instead just do this: + return this->random_crypt->next() % max; +} + +float Server::get_random_float_0_1() { + // This lacks some precision, but matches the original implementation. + return (static_cast(this->random_crypt->next() >> 16) / 65536.0); +} + +uint32_t Server::get_round_num() const { + return this->round_num; +} + +SetupPhase Server::get_setup_phase() const { + return this->setup_phase; +} + +uint32_t Server::get_should_copy_prev_states_to_current_states() const { + return this->should_copy_prev_states_to_current_states; +} + +bool Server::is_registration_complete() const { + return this->setup_phase != SetupPhase::REGISTRATION; +} + +void Server::move_phase_after() { + for (size_t trap_type = 0; trap_type < 5; trap_type++) { + uint8_t trap_tile_index = this->chosen_trap_tile_index_of_type[trap_type]; + if (trap_tile_index == 0xFF) { + continue; + } + + bool should_trigger = false; + int16_t trap_x = this->trap_tile_locs[trap_type][trap_tile_index][0]; + int16_t trap_y = this->trap_tile_locs[trap_type][trap_tile_index][1]; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps) { + auto sc_card = ps->get_sc_card(); + if (sc_card && (sc_card->card_flags & 0x80) && + (sc_card->loc.x == trap_x) && (sc_card->loc.y == trap_y)) { + should_trigger = true; + break; + } + } + } + if (!should_trigger) { + continue; + } + + static const uint16_t TRAP_CARD_IDS[5][5] = { + // Dice Fever, Heavy Fog, Muscular, Immortality, Snail Pace + {0x00F7, 0x010F, 0x012E, 0x013B, 0x013C}, + // Gold Rush, Charity, Requiem + {0x0131, 0x012B, 0x0133, 0x0000, 0x0000}, + // Powerless Rain, Trash 1, Empty Hand, Skip Draw + {0x00FA, 0x0125, 0x0126, 0x0137, 0x0000}, + // Brave Wind, Homesick, Fly + {0x00FB, 0x014E, 0x0107, 0x0000, 0x0000}, + // Dice+1, Battle Royale, Reverse Card, Giant Garden, Fix + {0x00F6, 0x0242, 0x014B, 0x0145, 0x012D}}; + static size_t TRAP_CARD_ID_COUNTS[5] = {5, 3, 4, 3, 5}; + + // This is the original implementation. We do something smarter instead. + // uint16_t trap_card_id = 0; + // while (trap_card_id == 0) { + // trap_card_id = TRAP_CARD_IDS[trap_type][this->get_random(5)]; + // } + size_t trap_card_id_index = this->get_random(TRAP_CARD_ID_COUNTS[trap_type]); + uint16_t trap_card_id = TRAP_CARD_IDS[trap_type][trap_card_id_index]; + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps) { + auto sc_card = ps->get_sc_card(); + if (sc_card && + (abs(sc_card->loc.x - trap_x) < 2) && + (abs(sc_card->loc.y - trap_y) < 2) && + ps->replace_assist_card_by_id(trap_card_id)) { + G_Unknown_GC_Ep3_6xB4x2C cmd; + cmd.client_id = client_id; + cmd.change_type = 0x01; + cmd.loc.direction = static_cast(trap_type); + cmd.loc.x = trap_x; + cmd.loc.y = trap_y; + cmd.unknown_a2[0] = trap_card_id; + this->send(cmd); + } + } + } + + // Note: This is the original implementation: + // if (this->num_trap_tiles_of_type[trap_type] > 1) { + // uint8_t new_index = this->chosen_trap_tile_index_of_type[trap_type]; + // while (new_index == this->chosen_trap_tile_index_of_type[trap_type]) { + // new_index = this->get_random(this->num_trap_tiles_of_type[trap_type]); + // } + // this->chosen_trap_tile_index_of_type[trap_type] = new_index; + // this->send_6xB4x50(); + // } + // We instead use an implementation that consumes a constant amount of + // randomness per pass. + if (this->num_trap_tiles_of_type[trap_type] == 2) { + this->chosen_trap_tile_index_of_type[trap_type] ^= 1; + this->send_6xB4x50(); + } else if (this->num_trap_tiles_of_type[trap_type] > 2) { + // Generate a new random index, but forbid it from matching the existing + // index + uint8_t new_index = this->get_random(this->num_trap_tiles_of_type[trap_type] - 1); + if (new_index >= this->chosen_trap_tile_index_of_type[trap_type]) { + new_index++; + } + this->chosen_trap_tile_index_of_type[trap_type] = new_index; + this->send_6xB4x50(); + } + } + + this->battle_phase = BattlePhase::ACTION; +} + +void Server::action_phase_before() { + this->unknown_a10 = 0; + this->current_team_turn2 = this->current_team_turn1; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps) { + ps->unknown_802394C4(); + } + this->has_done_pb[client_id] = false; + } +} + +void Server::send_6xB4x1C_names_update() { + G_SetPlayerNames_GC_Ep3_6xB4x1C cmd; + for (size_t z = 0; z < 4; z++) { + cmd.entries[z] = this->base()->name_entries[z]; + } + this->send(cmd); +} + +int8_t Server::send_6xB4x33_remove_ally_atk_if_needed(const ActionState& pa) { + G_SubtractAllyATKPoints_GC_Ep3_6xB4x33 cmd; + + bool has_ally_cost = false; + uint8_t ally_cost = 0; + uint8_t setter_client_id = 0xFF; + shared_ptr setter_ps = nullptr; + cmd.card_ref = 0xFFFF; + for (size_t z = 0; (z < 8) && (pa.action_card_refs[z] != 0xFFFF); z++) { + auto ce = this->definition_for_card_ref(pa.action_card_refs[z]); + if (ce && (ce->def.ally_cost > 0)) { + ally_cost = ce->def.ally_cost; + has_ally_cost = true; + setter_client_id = client_id_for_card_ref(pa.action_card_refs[z]); + setter_ps = this->get_player_state(setter_client_id); + cmd.card_ref = pa.action_card_refs[z]; + break; + } + } + + if (!has_ally_cost) { + return 0; + } + + if (!setter_ps) { + this->ruler_server->error_code3 = -0x67; + return 0; + } + + bool ally_has_sufficient_atk = false; + for (size_t z = 0; z < 4; z++) { + auto ally_ps = this->get_player_state(z); + if ((z != setter_client_id) && ally_ps) { + if ((ally_ps->get_team_id() == setter_ps->get_team_id()) && + (ally_ps->get_atk_points() >= ally_cost)) { + ally_has_sufficient_atk = true; + } + } + } + + if (!ally_has_sufficient_atk) { + this->ruler_server->error_code3 = -0x66; + return -1; + } + + this->pb_action_states[setter_client_id] = pa; + this->has_done_pb[setter_client_id] = true; + for (size_t z = 0; z < 4; z++) { + this->has_done_pb_with_client[setter_client_id][z] = false; + } + + cmd.client_id = setter_client_id; + cmd.ally_cost = ally_cost; + this->send(cmd); + return 1; +} + +void Server::send_all_state_updates() { + G_UpdateDecks_GC_Ep3_6xB4x07 cmd07; + for (size_t z = 0; z < 4; z++) { + if (!this->check_presence_entry(z)) { + cmd07.entries_present[z] = 0; + cmd07.entries[z].clear(); + cmd07.entries[z].team_id = 0xFFFFFFFF; + } else { + cmd07.entries_present[z] = 1; + cmd07.entries[z] = *this->base()->deck_entries[z]; + } + } + this->send(cmd07); + + G_UpdateMap_GC_Ep3_6xB4x05 cmd05; + cmd05.state = *this->base()->map_and_rules1; + this->send(cmd05); + + this->send_6xB4x02_for_all_players_if_needed(); +} + +void Server::send_set_card_updates_and_6xB4x04_if_needed() { + if (this->should_copy_prev_states_to_current_states == 0) { + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->send_set_card_updates(); + ps->send_6xB4x04_if_needed(); + } + } + } else { + this->should_copy_prev_states_to_current_states = 0; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + *ps->set_card_action_chains = ps->prev_set_card_action_chains; + *ps->set_card_action_metadatas = ps->prev_set_card_action_metadatas; + ps->send_set_card_updates(); + *ps->card_short_statuses = ps->prev_card_short_statuses; + ps->send_6xB4x04_if_needed(); + } + } + this->num_6xB4x06_commands_sent = 0; + } +} + +void Server::set_battle_ended() { + this->setup_phase = SetupPhase::BATTLE_ENDED; + this->send_6xB4x39(); + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); +} + +void Server::set_battle_started() { + this->setup_phase = SetupPhase::MAIN_BATTLE; + this->round_num = 1; + this->battle_phase = BattlePhase::DICE; + this->dice_phase_before(); + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + this->send_6xB4x02_for_all_players_if_needed(); + this->send_6xB4x05(); +} + +void Server::set_client_id_ready_to_advance_phase(uint8_t client_id) { + if (client_id >= 4) { + return; + } + + auto ps = this->player_states[client_id]; + if (ps && (this->current_team_turn1 == ps->get_team_id()) && + (this->setup_phase == SetupPhase::MAIN_BATTLE)) { + ps->assist_flags |= 1; + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + if (this->battle_phase == BattlePhase::DICE) { + if (!(ps->assist_flags & 0x8000) || this->base()->map_and_rules1->rules.disable_dice_boost) { + ps->assist_flags &= 0xFFFF7FFF; + ps->roll_main_dice(); + if ((ps->get_atk_points() < 3) && (ps->get_def_points() < 3)) { + ps->assist_flags |= 0x8000; + } + } else { + // TODO: It'd be nice to do this in a constant-randomness way, but I'm + // lazy, and this matches Sega's original implementation. The less-lazy + // way to do it would be to roll three dice: one in the range [1, N], + // one in the range [3, N], and on in the range [1, 2] to decide whether + // to swap the first two results. + for (size_t z = 0; z < 200; z++) { + ps->roll_main_dice(); + if ((ps->get_atk_points() >= 3) || (ps->get_def_points() >= 3)) { + break; + } + } + ps->assist_flags = ps->assist_flags & 0xFFFF7FFF; + } + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + this->player_ready_to_end_phase[client_id] = true; + + bool should_advance_phase = true; + for (size_t z = 0; z < 4; z++) { + auto other_ps = this->player_states[z]; + if (!other_ps) { + continue; + } + auto sc_card = other_ps->get_sc_card(); + if (sc_card && !sc_card->check_card_flag(2) && + (this->current_team_turn1 == other_ps->get_team_id()) && + !this->player_ready_to_end_phase[z]) { + should_advance_phase = false; + break; + } + } + + if (should_advance_phase) { + this->copy_player_states_to_prev_states(); + this->advance_battle_phase(); + this->send_set_card_updates_and_6xB4x04_if_needed(); + this->clear_player_flags_after_dice_phase(); + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + this->send_6xB4x39(); + } + } +} + + +void Server::set_phase_after() { + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps) { + auto card = ps->get_sc_card(); + if (card) { + this->card_special->apply_action_conditions(6, nullptr, card, 4, nullptr); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = ps->get_set_card(set_index); + if (card) { + this->card_special->apply_action_conditions(6, nullptr, card, 4, nullptr); + } + } + } + } + + this->send_6xB4x02_for_all_players_if_needed(); + + bool clients_with_assist_vanish[4] = {false, false, false, false}; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (!ps) { + continue; + } + size_t num_assists = this->assist_server->compute_num_assist_effects_for_client(client_id); + for (size_t z = 0; z < num_assists; z++) { + switch (this->assist_server->get_active_assist_by_index(z)) { + case AssistEffect::SHUFFLE_ALL: + case AssistEffect::SHUFFLE_GROUP: + if (!this->base()->map_and_rules1->rules.disable_deck_shuffle && + !this->base()->map_and_rules1->rules.disable_deck_loop) { + ps->discard_and_redraw_hand(); + } + break; + case AssistEffect::TRASH_1: + ps->discard_random_hand_card(); + break; + case AssistEffect::EMPTY_HAND: + ps->discard_all_attack_action_cards_from_hand(); + break; + case AssistEffect::HITMAN: + ps->discard_all_item_and_creature_cards_from_hand(); + break; + case AssistEffect::ASSIST_TRASH: + ps->discard_all_assist_cards_from_hand(); + break; + case AssistEffect::ASSIST_VANISH: + clients_with_assist_vanish[client_id] = true; + break; + default: + break; + } + } + } + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps && clients_with_assist_vanish[client_id]) { + ps->discard_set_assist_card(); + } + } + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps && + (ps->get_assist_turns_remaining() == 90) && + (ps->assist_delay_turns < 1)) { + ps->discard_set_assist_card(); + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + } + + this->battle_phase = BattlePhase::MOVE; +} + +void Server::move_phase_before() { + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps) { + ps->unknown_80239528(); + } + } +} + +void Server::set_player_deck_valid(uint8_t client_id) { + this->base()->presence_entries[client_id].deck_valid = true; +} + +void Server::setup_and_start_battle() { + this->setup_phase = SetupPhase::STARTER_ROLLS; + // Note: The original implementation uses time() as the random seed; we use a + // user-settable value in order to support replays and deterministic testing + this->random_crypt.reset(new PSOV2Encryption(this->base()->random_seed)); + + for (size_t z = 0; z < 4; z++) { + if (!this->check_presence_entry(z)) { + this->base()->name_entries[z].clear(); + } else { + this->player_states[z].reset(new PlayerState(z, this->shared_from_this())); + this->player_states[z]->init(); + } + } + + if (this->base()->map_and_rules1->rules.hp_type == HPType::COMMON_HP) { + int16_t team_hp[2] = {99, 99}; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (!ps) { + continue; + } + auto card = ps->get_sc_card(); + if (card) { + team_hp[ps->get_team_id()] = min(team_hp[ps->get_team_id()], card->get_current_hp()); + } + } + + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (!ps) { + continue; + } + auto card = ps->get_sc_card(); + if (card) { + int16_t this_team_hp = team_hp[ps->get_team_id()]; + if (this_team_hp < 99) { + card->set_current_and_max_hp(this_team_hp); + } + } + } + } + + this->base()->map_and_rules1->start_facing_directions = 0; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->set_initial_location(); + } + } + + this->determine_first_team_turn(); + this->compute_all_map_occupied_bits(); + + for (size_t y = 0; y < 0x10; y++) { + for (size_t x = 0; x < 0x10; x++) { + if (this->base()->map_and_rules1->map.tiles[y][x] > 1) { + this->base()->map_and_rules1->map.tiles[y][x] = 1; + } + } + } + + // this->__unused6__ = 0; + + for (size_t warp_type = 0; warp_type < 5; warp_type++) { + this->warp_positions[warp_type][0].clear(0xFF); + this->warp_positions[warp_type][1].clear(0xFF); + } + + for (size_t y = 0; y < 0x10; y++) { + for (size_t x = 0; x < 0x10; x++) { + uint8_t tile_spec = this->base()->overlay_state.tiles[y][x]; + uint8_t tile_type = tile_spec & 0xF0; + uint8_t tile_subtype = tile_spec & 0x0F; + if (tile_type == 0x30) { + if (this->warp_positions[tile_subtype][0][0] == 0xFF) { + this->warp_positions[tile_subtype][0][0] = x; + this->warp_positions[tile_subtype][0][1] = y; + } else if (this->warp_positions[tile_subtype][1][0] == 0xFF) { + this->warp_positions[tile_subtype][1][0] = x; + this->warp_positions[tile_subtype][1][1] = y; + } + } else if ((tile_type == 0x10) || (tile_type == 0x20) || (tile_type == 0x50)) { + this->base()->map_and_rules1->map.tiles[y][x] = 0; + } + } + } + + for (size_t trap_type = 0; trap_type < 5; trap_type++) { + this->chosen_trap_tile_index_of_type[trap_type] = 0xFF; + + size_t num_trap_tiles = 0; + for (size_t y = 0; y < 0x10; y++) { + for (size_t x = 0; x < 0x10; x++) { + if ((this->base()->overlay_state.tiles[y][x] == (trap_type | 0x40)) && + (num_trap_tiles < 8)) { + this->trap_tile_locs[trap_type][num_trap_tiles][0] = x; + this->trap_tile_locs[trap_type][num_trap_tiles][1] = y; + num_trap_tiles++; + } + } + } + this->num_trap_tiles_of_type[trap_type] = num_trap_tiles; + + if (num_trap_tiles > 0) { + this->chosen_trap_tile_index_of_type[trap_type] = this->get_random(num_trap_tiles); + } + } + + this->send_6xB4x02_for_all_players_if_needed(true); + this->send_6xB4x05(); + + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->send_set_card_updates(true); + } + } + + this->send_all_state_updates(); + this->send_6xB4x1C_names_update(); + this->registration_phase = RegistrationPhase::BATTLE_STARTED; + this->update_battle_state_flags_and_send_6xB4x03_if_needed(true); + this->send_6xB4x50(); + + G_UpdateMap_GC_Ep3_6xB4x05 cmd05; + cmd05.state = *this->base()->map_and_rules1; + cmd05.unknown_a1 = 1; + this->send(cmd05); + + this->battle_start_usecs = now(); + + G_ServerVersionStrings_GC_Ep3_6xB4x46 cmd46; + cmd46.version_signature = VERSION_SIGNATURE; + cmd46.date_str1 = SIGNATURE_DATE; + this->send(cmd46); +} + +void Server::update_battle_state_flags_and_send_6xB4x03_if_needed( + bool always_send) { + G_SetStateFlags_GC_Ep3_6xB4x03 cmd; + cmd.state.turn_num = this->round_num; + cmd.state.battle_phase = this->battle_phase; + cmd.state.current_team_turn1 = this->current_team_turn1; + cmd.state.current_team_turn2 = this->current_team_turn2; + cmd.state.action_subphase = this->action_subphase; + cmd.state.setup_phase = this->setup_phase; + cmd.state.registration_phase = this->registration_phase; + cmd.state.team_exp[0] = this->team_exp[0]; + cmd.state.team_exp[1] = this->team_exp[1]; + cmd.state.team_dice_boost[0] = this->team_dice_boost[0]; + cmd.state.team_dice_boost[1] = this->team_dice_boost[1]; + cmd.state.first_team_turn = this->first_team_turn; + cmd.state.tournament_flag = this->tournament_flag; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (!ps) { + cmd.state.client_sc_card_types[z] = CardType::INVALID_FF; + } else { + cmd.state.client_sc_card_types[z] = ps->get_sc_card_type(); + } + } + if (always_send || (*this->state_flags != cmd.state)) { + *this->state_flags = cmd.state; + this->send(cmd); + } +} + +bool Server::update_registration_phase() { + // Returns true if the battle can begin + + if (this->setup_phase != SetupPhase::REGISTRATION) { + return false; + } + + if (this->base()->map_and_rules1->num_players == 0) { + this->registration_phase = RegistrationPhase::AWAITING_NUM_PLAYERS; + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + return false; + } + + if (this->base()->map_and_rules1->num_players != this->base()->num_clients_present) { + this->registration_phase = RegistrationPhase::AWAITING_PLAYERS; + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + return false; + } + + size_t num_team0_registered_players = 0; + for (size_t z = 0; z < 4; z++) { + if (this->base()->deck_entries[z]->team_id == 0) { + num_team0_registered_players++; + } + } + + if (num_team0_registered_players != this->base()->map_and_rules1->num_team0_players) { + this->registration_phase = RegistrationPhase::AWAITING_DECKS; + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + return false; + } + + this->registration_phase = RegistrationPhase::REGISTERED; + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + return true; +} + +const unordered_map Server::subcommand_handlers({ + {0x0B, &Server::handle_6xB3x0B_mulligan_hand}, + {0x0C, &Server::handle_6xB3x0C_end_mulligan_phase}, + {0x0D, &Server::handle_6xB3x0D_end_non_action_phase}, + {0x0E, &Server::handle_6xB3x0E_discard_card_from_hand}, + {0x0F, &Server::handle_6xB3x0F_set_card_from_hand}, + {0x10, &Server::handle_6xB3x10_move_fc_to_location}, + {0x11, &Server::handle_6xB3x11_enqueue_attack_or_defense}, + {0x12, &Server::handle_6xB3x12_end_attack_list}, + {0x13, &Server::handle_6xB3x13_update_map_during_setup}, + {0x14, &Server::handle_6xB3x14_update_deck_during_setup}, + {0x15, &Server::handle_6xB3x15_unused_hard_reset_server_state}, + {0x1B, &Server::handle_6xB3x1B_update_player_name}, + {0x1D, &Server::handle_6xB3x1D_start_battle}, + {0x21, &Server::handle_6xB3x21_end_battle}, + {0x28, &Server::handle_6xB3x28_end_defense_list}, + {0x2B, &Server::handle_6xB3x2B_ignored}, + {0x34, &Server::handle_6xB3x34_subtract_ally_atk_points}, + {0x37, &Server::handle_6xB3x37_client_ready_to_advance_from_starter_roll_phase}, + {0x3A, &Server::handle_6xB3x3A_ignored}, + {0x40, &Server::handle_6xB3x40_map_list_request}, + {0x41, &Server::handle_6xB3x41_map_request}, + {0x48, &Server::handle_6xB3x48_end_turn}, + {0x49, &Server::handle_6xB3x49_card_counts}, +}); + +void Server::on_server_data_input(const string& data) { + auto header = check_size_t( + data, sizeof(G_CardBattleCommandHeader), 0xFFFF); + if (header.size * 4 < data.size()) { + throw runtime_error("command is incomplete"); + } + if (header.subcommand != 0xB3) { + throw runtime_error("server data command is not B3"); + } + + handler_t handler = nullptr; + try { + handler = this->subcommand_handlers.at(header.subsubcommand); + } catch (const out_of_range&) { + throw runtime_error("unknown CAxB3 subsubcommand"); + } + + string unmasked_data = data; + set_mask_for_ep3_game_command(unmasked_data.data(), unmasked_data.size(), 0); + + (this->*handler)(unmasked_data); + + if (this->hard_reset_flag && (this->base())) { + // In the original implementation, this command recreates the server object. + // This is possible because the dispatch function is not part of the server + // object in the original implementation; however, in our implementation, it + // is, so we don't support this. The original implementation did this: + // this->base()->recreate_server(); // Destroys *this, which we can't do here + // root_card_server = this->server; + // this->unknown_8023DC84(); + throw runtime_error("hard reset command received"); + } +} + +void Server::handle_6xB3x0B_mulligan_hand(const string& data) { + const auto& in_cmd = check_size_t(data); + + int32_t error_code = 0; + if (this->setup_phase != SetupPhase::HAND_REDRAW_OPTION) { + error_code = -0x5D; + } + if (in_cmd.client_id >= 4) { + error_code = -0x78; + } + if (error_code == 0) { + if (!this->player_states[in_cmd.client_id]) { + error_code = -0x72; + } else { + this->player_states[in_cmd.client_id]->do_mulligan(); + } + } + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd; + out_cmd.sequence_num = in_cmd.sequence_num.load(); + out_cmd.error_code = error_code; + this->send(out_cmd); +} + +void Server::handle_6xB3x0C_end_mulligan_phase(const string& data) { + const auto& in_cmd = check_size_t(data); + + int32_t error_code = 0; + if ((this->setup_phase != SetupPhase::HAND_REDRAW_OPTION) && + (this->setup_phase != SetupPhase::STARTER_ROLLS)) { + error_code = -0x5D; + } + + if (in_cmd.client_id > 4) { + error_code = -0x78; + } + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd_ack; + out_cmd_ack.sequence_num = in_cmd.sequence_num; + out_cmd_ack.response_phase = 1; + this->send(out_cmd_ack); + + if (error_code == 0) { + if (!this->player_states[in_cmd.client_id]) { + error_code = -0x72; + } else { + this->clients_done_in_mulligan_phase[in_cmd.client_id] = true; + auto ps = this->player_states[in_cmd.client_id]; + ps->assist_flags |= 1; + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + + bool all_clients_ready = true; + for (size_t z = 0; z < 4; z++) { + if (this->player_states[z] && + !this->clients_done_in_mulligan_phase[z]) { + all_clients_ready = false; + break; + } + } + if (all_clients_ready) { + this->set_battle_started(); + } + } + } + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd_fin; + out_cmd_fin.sequence_num = in_cmd.sequence_num; + out_cmd_fin.response_phase = 2; + out_cmd_fin.error_code = error_code; + this->send(out_cmd_fin); +} + +void Server::handle_6xB3x0D_end_non_action_phase(const string& data) { + const auto& in_cmd = check_size_t(data); + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd_ack; + out_cmd_ack.sequence_num = in_cmd.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.response_phase = 2; + this->send(out_cmd_fin); +} + +void Server::handle_6xB3x0E_discard_card_from_hand(const string& data) { + const auto& in_cmd = check_size_t(data); + + int32_t error_code = 0; + if (this->setup_phase != SetupPhase::MAIN_BATTLE) { + error_code = -0x5D; + } + if (this->battle_phase != BattlePhase::DRAW) { + error_code = -0x5D; + } + if (in_cmd.client_id >= 4) { + error_code = -0x78; + } + + if (error_code == 0) { + auto ps = this->player_states[in_cmd.client_id]; + if (!ps) { + error_code = -0x72; + } else if (!(ps->assist_flags & 0x80)) { + error_code = ps->discard_ref_from_hand(in_cmd.card_ref) ? 0 : 1; + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } else { + error_code = -0x70; + } + } + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd; + out_cmd.sequence_num = in_cmd.sequence_num; + out_cmd.error_code = error_code; + this->send(out_cmd); +} + +void Server::handle_6xB3x0F_set_card_from_hand(const string& data) { + const auto& in_cmd = check_size_t(data); + + int32_t error_code = 0; + if (this->setup_phase != SetupPhase::MAIN_BATTLE) { + error_code = -0x5D; + } + if (this->battle_phase != BattlePhase::SET) { + error_code = -0x5D; + } + if (in_cmd.card_ref == 0xFFFF) { + error_code = -0x78; + } + + if (in_cmd.client_id >= 4) { + error_code = -0x78; + } + if (error_code == 0) { + this->ruler_server->error_code1 = 0; + if (!this->player_states[in_cmd.client_id]) { + this->ruler_server->error_code1 = -0x72; + } else { + this->player_states[in_cmd.client_id]->set_card_from_hand( + in_cmd.card_ref, in_cmd.set_index, &in_cmd.loc, in_cmd.assist_target_player, 0); + } + } else { + this->ruler_server->error_code1 = error_code; + } + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd; + out_cmd.sequence_num = in_cmd.sequence_num; + out_cmd.error_code = this->ruler_server->error_code1; + this->send(out_cmd); +} + +void Server::handle_6xB3x10_move_fc_to_location(const string& data) { + const auto& in_cmd = check_size_t(data); + + int32_t error_code = 0; + if (this->setup_phase != SetupPhase::MAIN_BATTLE) { + error_code = -0x5D; + } + if (this->battle_phase != BattlePhase::MOVE) { + error_code = -0x5D; + } + if (in_cmd.client_id >= 4) { + error_code = -0x78; + } + if (error_code == 0) { + if (!this->player_states[in_cmd.client_id]) { + this->ruler_server->error_code2 = -0x72; + } else { + this->ruler_server->error_code2 = 0; + this->player_states[in_cmd.client_id]->move_card_to_location_by_card_index( + in_cmd.set_index, in_cmd.loc); + } + } else { + this->ruler_server->error_code2 = error_code; + } + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd; + out_cmd.sequence_num = in_cmd.sequence_num; + out_cmd.error_code = this->ruler_server->error_code2; + this->send(out_cmd); +} + +void Server::handle_6xB3x11_enqueue_attack_or_defense(const string& data) { + const auto& in_cmd = check_size_t(data); + + int32_t error_code = 0; + if (this->setup_phase != SetupPhase::MAIN_BATTLE) { + error_code = -0x5D; + } + if (this->battle_phase != BattlePhase::ACTION) { + error_code = -0x5D; + } + if (error_code == 0) { + this->ruler_server->error_code3 = 0; + ActionState pa = in_cmd.entry; + if (this->enqueue_attack_or_defense(in_cmd.client_id, &pa)) { + G_SetActionState_GC_Ep3_6xB4x09 out_cmd; + out_cmd.client_id = in_cmd.client_id; + out_cmd.state = in_cmd.entry; + this->send(out_cmd); + } + } else { + this->ruler_server->error_code3 = error_code; + } + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd; + out_cmd.sequence_num = in_cmd.sequence_num; + out_cmd.error_code = this->ruler_server->error_code3; + this->send(out_cmd); +} + +void Server::handle_6xB3x12_end_attack_list(const string& data) { + const auto& in_cmd = check_size_t(data); + + int32_t error_code = 0; + if (this->setup_phase != SetupPhase::MAIN_BATTLE) { + error_code = -0x5D; + } + if (error_code == 0) { + this->end_attack_list_for_client(in_cmd.client_id); + } + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd; + out_cmd.sequence_num = in_cmd.sequence_num; + this->send(out_cmd); +} + +void Server::handle_6xB3x13_update_map_during_setup(const string& data) { + const auto& in_cmd = check_size_t(data); + + auto b = this->base(); + if (!this->battle_in_progress && + (this->setup_phase == SetupPhase::REGISTRATION) && + (b->map_and_rules1->num_players == 0) && + (this->registration_phase != RegistrationPhase::REGISTERED) && + (this->registration_phase != RegistrationPhase::BATTLE_STARTED)) { + *b->map_and_rules1 = in_cmd.map_and_rules_state; + *b->map_and_rules2 = in_cmd.map_and_rules_state; + b->overlay_state = in_cmd.overlay_state; + if (b->behavior_flags & BehaviorFlag::DISABLE_TIME_LIMITS) { + b->map_and_rules1->rules.overall_time_limit = 0; + b->map_and_rules1->rules.phase_time_limit = 0; + b->map_and_rules2->rules.overall_time_limit = 0; + b->map_and_rules2->rules.phase_time_limit = 0; + } + if (b->map_and_rules1->rules.check_invalid_fields()) { + b->map_and_rules1->rules.check_and_reset_invalid_fields(); + } + if (b->map_and_rules1->num_players_per_team == 0) { + b->map_and_rules1->num_players_per_team = b->map_and_rules1->num_players >> 1; + } + this->update_registration_phase(); + } +} + +void Server::handle_6xB3x14_update_deck_during_setup(const string& data) { + const auto& in_cmd = check_size_t(data); + + if (!this->battle_in_progress) { + if ((this->setup_phase == SetupPhase::REGISTRATION) && + (this->registration_phase != RegistrationPhase::REGISTERED) && + (this->registration_phase != RegistrationPhase::BATTLE_STARTED)) { + if (in_cmd.client_id >= 4) { + return; + } + DeckEntry entry = in_cmd.entry; + if (!(this->base()->behavior_flags & BehaviorFlag::SKIP_DECK_VERIFY)) { + // Note: Sega's original implementation doesn't use the card counts here + if (this->base()->behavior_flags & BehaviorFlag::IGNORE_CARD_COUNTS) { + this->ruler_server->verify_deck(entry.card_ids); + } else { + this->ruler_server->verify_deck(entry.card_ids, + &this->base()->client_card_counts[in_cmd.client_id]); + } + } + if (!(this->base()->behavior_flags & BehaviorFlag::SKIP_D1_D2_REPLACE)) { + this->ruler_server->replace_D1_D2_rarity_cards_with_Attack(entry.card_ids); + } + *this->base()->deck_entries[in_cmd.client_id] = in_cmd.entry; + this->base()->presence_entries[in_cmd.client_id].player_present = true; + this->base()->presence_entries[in_cmd.client_id].is_cpu_player = in_cmd.is_cpu_player; + this->set_player_deck_valid(in_cmd.client_id); + } + + this->base()->num_clients_present = 0; + for (size_t z = 0; z < 4; z++) { + if (this->check_presence_entry(z)) { + this->base()->num_clients_present++; + } + } + + this->send_all_state_updates(); + this->update_registration_phase(); + } +} + +void Server::handle_6xB3x15_unused_hard_reset_server_state(const string& data) { + check_size_t(data); + this->hard_reset_flag = true; +} + +void Server::handle_6xB3x1B_update_player_name(const string& data) { + const auto& in_cmd = check_size_t(data); + + if (!this->is_registration_complete() && (in_cmd.entry.client_id < 4)) { + this->base()->name_entries[in_cmd.entry.client_id] = in_cmd.entry; + this->base()->name_entries_valid[in_cmd.entry.client_id] = false; + } + + G_SetPlayerNames_GC_Ep3_6xB4x1C out_cmd; + for (size_t z = 0; z < 4; z++) { + out_cmd.entries[z] = this->base()->name_entries[z]; + } + this->send(out_cmd); +} + +void Server::handle_6xB3x1D_start_battle(const string& data) { + check_size_t(data); + + if (!this->battle_in_progress) { + if (!this->update_registration_phase()) { + G_RejectBattleStartRequest_GC_Ep3_6xB4x53 out_cmd; + out_cmd.setup_phase = this->setup_phase; + out_cmd.registration_phase = this->registration_phase; + out_cmd.state = *this->base()->map_and_rules1; + this->send(out_cmd); + + for (size_t z = 0; z < 4; z++) { + this->base()->deck_entries[z]->clear(); + this->base()->presence_entries[z].clear(); + } + this->battle_in_progress = false; + } else { + this->setup_and_start_battle(); + this->battle_in_progress = true; + } + } +} + +void Server::handle_6xB3x21_end_battle(const string& data) { + check_size_t(data); + if (this->setup_phase == SetupPhase::BATTLE_ENDED) { + this->battle_finished = true; + } +} + +void Server::handle_6xB3x28_end_defense_list(const string& data) { + const auto& in_cmd = check_size_t(data); + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd_ack; + out_cmd_ack.sequence_num = in_cmd.sequence_num; + out_cmd_ack.response_phase = 1; + this->send(out_cmd_ack); + + this->defense_list_ended_for_client[in_cmd.client_id] = 1; + + bool all_defense_lists_ended = true; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps && (this->current_team_turn1 != ps->get_team_id())) { + if (!ps->get_sc_card()->check_card_flag(2) && + (this->defense_list_ended_for_client[z] == 0)) { + all_defense_lists_ended = false; + break; + } + } + } + + if (all_defense_lists_ended && (this->unknown_a10 == 0)) { + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->assist_flags &= 0xFFFFDFFF; + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + } + this->unknown_8023EE48(); + this->unknown_a10 = 1; + } else { + auto ps = this->player_states[in_cmd.client_id]; + ps->assist_flags |= 0x2000; + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + if (this->unknown_a10 != 0) { + this->unknown_8023EE80(); + this->unknown_a10 = 0; + } + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd_fin; + out_cmd_fin.sequence_num = in_cmd.sequence_num; + out_cmd_fin.response_phase = 2; + this->send(out_cmd_fin); +} + +void Server::handle_6xB3x2B_ignored(const string&) { } + +void Server::handle_6xB3x34_subtract_ally_atk_points(const string& data) { + const auto& in_cmd = check_size_t(data); + + uint8_t card_ref_client_id = client_id_for_card_ref(in_cmd.card_ref); + if (card_ref_client_id >= 4) { + return; + } + + auto this_ps = this->player_states[card_ref_client_id]; + if (this_ps && (in_cmd.ally_client_id < 4)) { + auto ally_ps = this->player_states[in_cmd.ally_client_id]; + if (ally_ps && (this->has_done_pb[card_ref_client_id])) { + + if (in_cmd.reason == 0) { + this->has_done_pb_with_client[card_ref_client_id][in_cmd.ally_client_id] = true; + bool accepted = true; + for (size_t z = 0; z < 4; z++) { + auto ally_ps = this->get_player_state(z); + if ((z != card_ref_client_id) && ally_ps) { + if (this_ps->get_team_id() == ally_ps->get_team_id()) { + if (this->has_done_pb_with_client[card_ref_client_id][z] == 0) { + accepted = false; + } + break; + } + } + } + if (accepted) { + G_PhotonBlastStatus_GC_Ep3_6xB4x35 out_cmd; + out_cmd.accepted = 0; + out_cmd.card_ref = in_cmd.card_ref; + out_cmd.client_id = card_ref_client_id; + this->send(out_cmd); + } + + } else { + auto ce = this->definition_for_card_ref(in_cmd.card_ref); + if (ce->def.ally_cost <= ally_ps->get_atk_points()) { + auto& pa = this->pb_action_states[card_ref_client_id]; + if (this->num_pending_attacks < 0x20) { + this->num_pending_attacks++; + this->pending_attacks[this->num_pending_attacks] = pa; + this_ps->set_action_cards_for_action_state(pa); + ally_ps->subtract_atk_points(ce->def.ally_cost); + if (ce->def.ally_cost > 0) { + ally_ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + auto attacker_card = this->card_for_set_card_ref(pa.attacker_card_ref); + if (attacker_card) { + attacker_card->card_flags |= 0x400; + attacker_card->player_state()->send_6xB4x04_if_needed(); + } + uint16_t card_ref = this->send_6xB4x06_if_card_ref_invalid( + pa.original_attacker_card_ref, 9); + auto orig_attacker_card = this->card_for_set_card_ref(card_ref); + auto target_card = this->card_for_set_card_ref(pa.target_card_refs[0]); + if (orig_attacker_card && target_card) { + target_card->card_flags |= 0x800; + target_card->player_state()->send_6xB4x04_if_needed(); + } + this->has_done_pb[card_ref_client_id] = false; + + G_PhotonBlastStatus_GC_Ep3_6xB4x35 out_cmd; + out_cmd.client_id = card_ref_client_id; + out_cmd.accepted = 1; + out_cmd.card_ref = in_cmd.card_ref; + this->send(out_cmd); + } + } + } + } + } +} + +void Server::handle_6xB3x37_client_ready_to_advance_from_starter_roll_phase(const string& data) { + const auto& in_cmd = check_size_t(data); + + auto ps = this->player_states[in_cmd.client_id]; + if (ps) { + ps->assist_flags |= 8; + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } + if (this->setup_phase == SetupPhase::STARTER_ROLLS) { + bool all_clients_ready = true; + for (size_t z = 0; z < 4; z++) { + auto other_ps = this->player_states[z]; + if (other_ps && !(other_ps->assist_flags & 8)) { + all_clients_ready = false; + break; + } + } + + if (all_clients_ready) { + this->setup_phase = SetupPhase::HAND_REDRAW_OPTION; + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + } + } +} + +void Server::handle_6xB3x3A_ignored(const string&) { } + +void Server::handle_6xB3x40_map_list_request(const string& data) { + check_size_t(data); + + auto l = this->base()->lobby.lock(); + if (!l) { + throw runtime_error("lobby is deleted"); + } + + const auto& list_data = this->base()->data_index->get_compressed_map_list(); + + StringWriter w; + uint32_t subcommand_size = (list_data.size() + sizeof(G_MapList_GC_Ep3_6xB6x40) + 3) & (~3); + w.put( + 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()); +} + +void Server::handle_6xB3x41_map_request(const string& data) { + const auto& cmd = check_size_t(data); + + auto l = this->base()->lobby.lock(); + if (!l) { + throw runtime_error("lobby is deleted"); + } + + auto entry = this->base()->data_index->definition_for_map_number(cmd.map_number); + const auto& compressed = entry->compressed(); + + StringWriter w; + uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3); + w.put({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, entry->map.map_number.load(), compressed.size(), 0}); + w.write(compressed); + send_command(l, 0x6C, 0x00, w.str()); +} + +void Server::handle_6xB3x48_end_turn(const string& data) { + const auto& in_cmd = check_size_t(data); + + auto ps = this->get_player_state(in_cmd.client_id); + if (ps && ps->draw_cards_allowed()) { + ps->draw_hand(0); + } + + G_ActionResult_GC_Ep3_6xB4x1E out_cmd; + out_cmd.sequence_num = in_cmd.sequence_num; + this->send(out_cmd); +} + +void Server::handle_6xB3x49_card_counts(const string& data) { + const auto& in_cmd = check_size_t(data); + + // Note: Sega's implmentation completely ignores this command. This + // implementation is not based on the original code. + auto& dest_counts = this->base()->client_card_counts[in_cmd.header.sender_client_id]; + dest_counts = in_cmd.card_id_to_count; + decrypt_trivial_gci_data(dest_counts.data(), dest_counts.bytes(), in_cmd.basis); +} + +void Server::unknown_8023D4E0(uint32_t flags) { + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->assist_flags &= 0xFFFFB7FB; + } + } + + uint32_t flags_to_add = flags | 0x804; + + // First, check which team has fewer surviving SCs + int8_t team_id = -1; + uint32_t team_counts[2] = {0, 0}; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (!ps) { + continue; + } + auto sc_card = ps->get_sc_card(); + if (sc_card && (sc_card->card_flags & 2)) { + team_counts[ps->get_team_id()]++; + } + } + if (team_counts[1] < team_counts[0]) { + team_id = 0; + } else if (team_counts[0] < team_counts[1]) { + team_id = 1; + } + + // If the SC counts match, break ties by remaining SC HP + if (team_id == -1) { + team_counts[0] = 0; + team_counts[1] = 0; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (!ps) { + continue; + } + auto sc_card = ps->get_sc_card(); + if (sc_card) { + team_counts[ps->get_team_id()] += sc_card->get_current_hp(); + } + } + if (team_counts[0] < team_counts[1]) { + team_id = 0; + } else if (team_counts[1] < team_counts[0]) { + team_id = 1; + } + } + + // If still tied, break ties by number of opponent cards destroyed + if (team_id == -1) { + team_counts[0] = 0; + team_counts[1] = 0; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (!ps) { + continue; + } + team_counts[ps->get_team_id()] += ps->stats.num_opponent_cards_destroyed; + } + if (team_counts[0] < team_counts[1]) { + team_id = 0; + } else if (team_counts[1] < team_counts[0]) { + team_id = 1; + } + } + + // If still tied, break ties by amount of damage given + if (team_id == -1) { + team_counts[0] = 0; + team_counts[1] = 0; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (!ps) { + continue; + } + team_counts[ps->get_team_id()] += ps->stats.damage_given; + } + if (team_counts[0] < team_counts[1]) { + team_id = 0; + } else if (team_counts[1] < team_counts[0]) { + team_id = 1; + } + } + + // If STILL tied, roll dice and arbitrarily make one team the winner + if (team_id == -1) { + while (team_id == -1) { + team_counts[1] = 0; + team_counts[0] = 0; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (!ps) { + continue; + } + team_counts[ps->get_team_id()] += ps->roll_dice(1); + } + 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; + } else if (team_counts[1] < team_counts[0]) { + team_id = 1; + } + } + flags_to_add = flags | 0x1004; + } + + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (!ps) { + continue; + } + if (team_id != ps->get_team_id()) { + ps->assist_flags |= flags_to_add; + } + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(); + } +} + +uint32_t Server::get_team_exp(uint8_t team_id) const { + return this->team_exp[team_id]; +} + +uint32_t Server::send_6xB4x06_if_card_ref_invalid( + uint16_t card_ref, int16_t negative_value) { + if (this->card_special) { + return this->card_special->send_6xB4x06_if_card_ref_invalid( + card_ref, -negative_value); + } + return card_ref; +} + +void Server::unknown_8023EEF4() { + if (this->unknown_a14 >= 0x20) { + return; + } + + while (this->unknown_a14 < this->num_pending_attacks_with_cards) { + auto card = this->attack_cards[this->unknown_a14]; + if (this->get_current_team_turn() == card->get_team_id()) { + ActionState as = this->pending_attacks_with_cards[this->unknown_a14]; + this->replace_targets_due_to_destruction_or_conditions(&as); + if (this->any_target_exists_for_attack(as)) { + break; + } + } + this->unknown_a14++; + } + + if (this->unknown_a14 < this->num_pending_attacks_with_cards) { + this->defense_list_ended_for_client.clear(false); + + G_SetActionState_GC_Ep3_6xB4x29 cmd; + cmd.unknown_a1 = this->unknown_a14; + cmd.state = this->pending_attacks_with_cards[this->unknown_a14]; + this->replace_targets_due_to_destruction_or_conditions(&cmd.state); + ActionState as = cmd.state; + this->send(cmd); + + this->card_special->unknown_8024AAB8(as); + this->attack_cards[this->unknown_a14]->compute_action_chain_results(1, 0); + this->attack_cards[this->unknown_a14]->unknown_80236374(this->attack_cards[this->unknown_a14], &as); + if (!this->attack_cards[this->unknown_a14]->action_chain.check_flag(0x40)) { + this->card_special->unknown_8024945C(this->attack_cards[this->unknown_a14], as); + } + this->attack_cards[this->unknown_a14]->compute_action_chain_results(1,0); + this->attack_cards[this->unknown_a14]->unknown_80236374(this->attack_cards[this->unknown_a14], &as); + if (!this->attack_cards[this->unknown_a14]->action_chain.check_flag(0x40)) { + this->card_special->unknown_8024966C(this->attack_cards[this->unknown_a14], &as); + } + this->attack_cards[this->unknown_a14]->compute_action_chain_results(1, 0); + this->attack_cards[this->unknown_a14]->unknown_80236374(this->attack_cards[this->unknown_a14], &as); + this->attack_cards[this->unknown_a14]->send_6xB4x4E_4C_4D_if_needed(); + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->send_set_card_updates(); + } + } + + } else { + this->unknown_a15 = 0; + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps && (this->current_team_turn1 == ps->get_team_id())) { + this->set_client_id_ready_to_advance_phase(z); + } + } + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + this->send_6xB4x02_for_all_players_if_needed(); + } + this->update_battle_state_flags_and_send_6xB4x03_if_needed(); + this->send_6xB4x02_for_all_players_if_needed(); +} + +void Server::execute_bomb_assist_effect() { + int16_t max_hp = -999; + int16_t min_hp = 999; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps && !this->assist_server->should_block_assist_effects_for_client(client_id)) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = ps->get_set_card(set_index); + if (card && !(card->card_flags & 2)) { + max_hp = max(max_hp, card->get_current_hp()); + min_hp = min(min_hp, card->get_current_hp()); + } + } + } + } + + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + // Possible bug: shouldn't we check should_block_assist_effects_for_client + // here too? If the player has a card with the same HP as another one that + // would be destroyed, it looks like the card can be destroyed even if the + // client should be immune to assist effects here. + if (ps) { + for (size_t set_index = 0; set_index < 8; set_index++) { + auto card = ps->get_set_card(set_index); + if (card && !(card->card_flags & 2) && + ((card->get_current_hp() == max_hp) || (card->get_current_hp() == min_hp))) { + card->player_state()->handle_homesick_assist_effect(card); + } + } + } + } +} + +void Server::replace_targets_due_to_destruction_or_conditions( + ActionState* as) { + auto attacker_card = this->card_for_set_card_ref( + this->send_6xB4x06_if_card_ref_invalid(as->attacker_card_ref, 3)); + if (!attacker_card) { + as->target_card_refs[0] = 0xFFFF; + return; + } + + vector phase1_replaced_card_refs; + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->get_player_state(client_id); + if (!attacker_card->action_chain.check_flag(0x200 << client_id)) { + for (size_t target_index = 0; target_index < 4 * 9; target_index++) { + uint32_t target_card_ref = as->target_card_refs[target_index]; + if (target_card_ref == 0xFFFF) { + break; + } + if (client_id == client_id_for_card_ref(target_card_ref)) { + auto target_card = this->card_for_set_card_ref(this->send_6xB4x06_if_card_ref_invalid(target_card_ref, 5)); + auto ce = this->definition_for_card_ref(target_card_ref); + if (ce && ps) { + if (!target_card || (target_card->card_flags & 2)) { + if (ce->def.type == CardType::ITEM) { + for (size_t set_index = 0; set_index < 8; set_index++) { + target_card = ps->get_set_card(set_index); + if (target_card && + (target_card->get_card_ref() != target_card_ref) && + !(target_card->card_flags & 2) && + target_card->is_guard_item()) { + break; + } + target_card = nullptr; + } + auto replaced_target_card = target_card; + if (!target_card) { + for (size_t set_index = 0; set_index < 8; set_index++) { + replaced_target_card = ps->get_set_card(set_index); + if (replaced_target_card && + (replaced_target_card->get_card_ref() != target_card_ref) && + !(replaced_target_card->card_flags & 2)) { + break; + } + replaced_target_card = target_card; + } + } + if (!replaced_target_card) { + target_card = ps->get_sc_card(); + if (target_card) { + phase1_replaced_card_refs.emplace_back(target_card->get_card_ref()); + } + } else { + phase1_replaced_card_refs.emplace_back(replaced_target_card->get_card_ref()); + } + } + } else { + phase1_replaced_card_refs.emplace_back(target_card_ref); + } + } + } + } + + } else { + if (ps) { + size_t present_target_count = 0; + size_t missing_target_count = 0; + size_t set_card_count = ps->count_set_cards(); + for (size_t target_index = 0; (target_index < 4 * 9) && (as->target_card_refs[target_index] != 0xFFFF); target_index++) { + if (client_id == client_id_for_card_ref(as->target_card_refs[target_index])) { + auto target_card = this->card_for_set_card_ref(as->target_card_refs[target_index]); + if (!target_card || (target_card->card_flags & 2)) { + missing_target_count++; + } else { + present_target_count++; + phase1_replaced_card_refs.emplace_back(target_card->get_card_ref()); + } + } + } + auto sc_card = ps->get_sc_card(); + if ((present_target_count == 0) && + (missing_target_count > 0) && + (set_card_count == 0) && + sc_card && + sc_card->get_definition() && + (sc_card->get_definition()->def.type == CardType::HUNTERS_SC)) { + phase1_replaced_card_refs.emplace_back(sc_card->get_card_ref()); + } + } + } + } + + // Note: The original code only writes a single FFFF after the last card ref + // in this array; we instead clear the entire array. + as->target_card_refs.clear(0xFFFF); + for (size_t z = 0; z < phase1_replaced_card_refs.size(); z++) { + as->target_card_refs[z] = this->send_6xB4x06_if_card_ref_invalid(phase1_replaced_card_refs[z], 4); + } + // as->target_card_refs[phase1_replaced_card_refs.size()] = 0xFFFF; + + vector phase2_replaced_card_refs; + for (size_t z = 0; (z < 4 * 9) && (as->target_card_refs[z] != 0xFFFF); z++) { + uint16_t target_card_ref = this->send_6xB4x06_if_card_ref_invalid(as->target_card_refs[z], 7); + auto target_card = this->card_for_set_card_ref(target_card_ref); + if (target_card) { + auto replaced_target = this->card_special->compute_replaced_target_based_on_conditions( + target_card->get_card_ref(), 1, 0, as->attacker_card_ref, 0xFFFF, 1, + 0, 0xFF, 0, 0xFFFF); + if (!replaced_target) { + replaced_target = target_card; + } + phase2_replaced_card_refs.emplace_back(this->send_6xB4x06_if_card_ref_invalid(replaced_target->get_card_ref(), 8)); + } + } + + // Note: This is different from the original code in the same way as above: we + // clear the entire array first. + as->target_card_refs.clear(0xFFFF); + for (size_t z = 0; z < phase2_replaced_card_refs.size(); z++) { + as->target_card_refs[z] = this->send_6xB4x06_if_card_ref_invalid(phase2_replaced_card_refs[z], 4); + } + // as->target_card_refs[phase2_replaced_card_refs.size()] = 0xFFFF; +} + +bool Server::any_target_exists_for_attack(const ActionState& as) { + auto card = this->card_for_set_card_ref(as.attacker_card_ref); + if (!card || (card->card_flags & 2)) { + return false; + } + + for (size_t z = 0; (z < 4 * 9) && (as.target_card_refs[z] != 0xFFFF); z++) { + card = this->card_for_set_card_ref(as.target_card_refs[z]); + if (!card) { + break; + } + if (!(card->card_flags & 2)) { + return true; + } + } + return false; +} + +uint8_t Server::get_current_team_turn2() const { + return this->current_team_turn2; +} + +void Server::unknown_8023EE48() { + this->unknown_802402F4(); + this->send_6xB4x02_for_all_players_if_needed(); +} + +void Server::unknown_8023EE80() { + if (this->unknown_a14 < this->num_pending_attacks_with_cards) { + this->attack_cards[this->unknown_a14]->unknown_80237734(); + this->unknown_a14++; + } + this->check_for_battle_end(); + this->copy_player_states_to_prev_states(); + this->unknown_8023EEF4(); + this->send_set_card_updates_and_6xB4x04_if_needed(); +} + +void Server::unknown_802402F4() { + for (size_t client_id = 0; client_id < 4; client_id++) { + auto ps = this->player_states[client_id]; + if (ps && (this->current_team_turn2 == ps->get_team_id())) { + auto card = ps->get_sc_card(); + if (card) { + card->compute_action_chain_results(1, 0); + } + for (size_t set_index = 0; set_index < 8; set_index++) { + card = ps->get_set_card(set_index); + if (card) { + card->compute_action_chain_results(1, 0); + } + } + } + } +} + +vector> Server::const_cast_set_cards_v( + const vector>& cards) { + // TODO: This is dumb. Figure out a not-dumb way to do this. + vector> ret; + for (auto const_card : cards) { + auto mutable_card = this->card_for_set_card_ref(const_card->get_card_ref()); + if (mutable_card.get() != const_card.get()) { + throw logic_error("inconsistent set cards index"); + } + ret.emplace_back(mutable_card); + } + return ret; +} + +void Server::send_6xB4x39() const { + G_UpdateAllPlayerStatistics_GC_Ep3_6xB4x39 cmd; + for (size_t z = 0; z < 4; z++) { + if (this->player_states[z]) { + cmd.stats[z] = this->player_states[z]->stats; + } + } + this->send(cmd); +} + +void Server::send_6xB4x05() { + this->compute_all_map_occupied_bits(); + G_UpdateMap_GC_Ep3_6xB4x05 cmd; + cmd.state = *this->base()->map_and_rules1; + this->send(cmd); +} + + +void Server::send_6xB4x02_for_all_players_if_needed(bool always_send) { + for (size_t z = 0; z < 4; z++) { + auto ps = this->player_states[z]; + if (ps) { + ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed(always_send); + } + } +} + +void Server::send_6xB4x50() const { + G_SetTrapTileLocations_GC_Ep3_6xB4x50 cmd; + for (size_t trap_type = 0; trap_type < 5; trap_type++) { + uint8_t trap_index = this->chosen_trap_tile_index_of_type[trap_type]; + if (trap_index != 0xFF) { + cmd.locations[trap_type] = this->trap_tile_locs[trap_type][trap_index]; + } else { + cmd.locations[trap_type].clear(0xFF); + } + } + this->send(cmd); +} + + + +} // namespace Episode3 diff --git a/src/Episode3/Server.hh b/src/Episode3/Server.hh new file mode 100644 index 00000000..2d12b3f7 --- /dev/null +++ b/src/Episode3/Server.hh @@ -0,0 +1,280 @@ +#pragma once + +#include + +#include + +#include "../Text.hh" +#include "../CommandFormats.hh" +#include "AssistServer.hh" +#include "CardSpecial.hh" +#include "MapState.hh" +#include "PlayerState.hh" +#include "RulerServer.hh" + +struct Lobby; + +namespace Episode3 { + + + +/** + * This implementation of Episode 3 battles (contained in all files in the + * src/Episode3 directory, except for DataIndex.hh/cc) is derived from Sega's + * original server implementation, reverse-engineered from the Episode 3 client + * executable. The control flow, function breakdown, and structure definitions + * in these files map very closely to how their server implementation was + * written; notable differences (due to necessary environment differences or bug + * fixes) are described in the comments therein. + * + * There are likely undiscovered bugs in this code, some originally written by + * Sega, but more written by me as I manually transcribed and updated this code. + */ + +// Class ownership levels (classes may only contain weak_ptrs, not shared_ptrs, +// to classes at the same or higher level): +// - ServerBase +// - - Server +// - - - RulerServer +// - - - - AssistServer +// - - - - CardSpecial +// - - - - - StateFlags +// - - - - - DeckEntry +// - - - - - PlayerState +// - - - - - - Card +// - - - - - - - CardShortStatus +// - - - - - - - DeckState +// - - - - - - - HandAndEquipState +// - - - - - - - MapAndRulesState / OverlayState +// - - - - - - - - Everything within DataIndex + +class Server; + + + +enum BehaviorFlag { + SKIP_DECK_VERIFY = 0x00000001, + IGNORE_CARD_COUNTS = 0x00000002, + SKIP_D1_D2_REPLACE = 0x00000004, + DISABLE_TIME_LIMITS = 0x00000008, +}; + +class ServerBase : public std::enable_shared_from_this { +public: + ServerBase( + std::shared_ptr lobby, + std::shared_ptr data_index, + uint32_t behavior_flags, + uint32_t random_seed); + void init(); + void reset(); + void recreate_server(); + + struct PresenceEntry { + uint8_t player_present; + uint8_t deck_valid; + uint8_t is_cpu_player; + PresenceEntry(); + void clear(); + } __attribute__((packed)); + + std::weak_ptr lobby; + std::shared_ptr data_index; + uint32_t behavior_flags; + uint32_t random_seed; + + std::shared_ptr map_and_rules1; + std::shared_ptr map_and_rules2; + std::shared_ptr deck_entries[4]; + std::shared_ptr server; + parray presence_entries; + uint8_t num_clients_present; + parray name_entries; + parray name_entries_valid; + OverlayState overlay_state; + parray, 4> client_card_counts; +}; + +class Server : public std::enable_shared_from_this { +public: + explicit Server(std::shared_ptr base); + void init(); + std::shared_ptr base(); + std::shared_ptr base() const; + + template + void send(const T& cmd) const { + if (cmd.header.size != sizeof(cmd) / 4) { + throw std::logic_error("outbound command size field is incorrect"); + } + if (cmd.header.subsubcommand == 0x06) { + this->num_6xB4x06_commands_sent++; + this->prev_num_6xB4x06_commands_sent = this->num_6xB4x06_commands_sent; + if (this->num_6xB4x06_commands_sent > 0x100) { + return; + } + } + this->send(&cmd, cmd.header.size * 4); + } + void send(const void* data, size_t size) const; + + void add_team_exp(uint8_t team_id, int32_t exp); + bool advance_battle_phase(); + void action_phase_after(); + void draw_phase_before(); + std::shared_ptr definition_for_card_ref(uint16_t card_ref) const; + std::shared_ptr card_for_set_card_ref(uint16_t card_ref); + std::shared_ptr card_for_set_card_ref(uint16_t card_ref) const; + uint16_t card_id_for_card_ref(uint16_t card_ref) const; + bool card_ref_is_empty_or_has_valid_card_id(uint16_t card_ref) const; + bool check_for_battle_end(); + void check_for_destroyed_cards_and_send_6xB4x05_6xB4x02(); + bool check_presence_entry(uint8_t client_id); + void clear_player_flags_after_dice_phase(); + void compute_all_map_occupied_bits(); + void compute_team_dice_boost(uint8_t team_id); + void copy_player_states_to_prev_states(); + std::shared_ptr definition_for_card_id(uint16_t card_id) const; + void destroy_cards_with_zero_hp(); + void determine_first_team_turn(); + void dice_phase_after(); + void set_phase_before(); + void draw_phase_after(); + void dice_phase_before(); + void end_attack_list_for_client(uint8_t client_id); + void end_action_phase(); + bool enqueue_attack_or_defense(uint8_t client_id, ActionState* pa); + BattlePhase get_battle_phase() const; + ActionSubphase get_current_action_subphase() const; + uint8_t get_current_team_turn() const; + std::shared_ptr get_player_state(uint8_t client_id); + std::shared_ptr get_player_state(uint8_t client_id) const; + uint32_t get_random(uint32_t max); + float get_random_float_0_1(); + uint32_t get_round_num() const; + SetupPhase get_setup_phase() const; + uint32_t get_should_copy_prev_states_to_current_states() const; + bool is_registration_complete() const; + void move_phase_after(); + void action_phase_before(); + void send_6xB4x1C_names_update(); + int8_t send_6xB4x33_remove_ally_atk_if_needed(const ActionState& pa); + void send_all_state_updates(); + void send_set_card_updates_and_6xB4x04_if_needed(); + void set_battle_ended(); + void set_battle_started(); + void set_client_id_ready_to_advance_phase(uint8_t client_id); + void set_phase_after(); + void move_phase_before(); + void set_player_deck_valid(uint8_t client_id); + void setup_and_start_battle(); + void update_battle_state_flags_and_send_6xB4x03_if_needed( + bool always_send = false); + bool update_registration_phase(); + void on_server_data_input(const std::string& data); + void handle_6xB3x0B_mulligan_hand(const std::string& data); + void handle_6xB3x0C_end_mulligan_phase(const std::string& data); + void handle_6xB3x0D_end_non_action_phase(const std::string& data); + void handle_6xB3x0E_discard_card_from_hand(const std::string& data); + void handle_6xB3x0F_set_card_from_hand(const std::string& data); + void handle_6xB3x10_move_fc_to_location(const std::string& data); + void handle_6xB3x11_enqueue_attack_or_defense(const std::string& data); + void handle_6xB3x12_end_attack_list(const std::string& data); + void handle_6xB3x13_update_map_during_setup(const std::string& data); + void handle_6xB3x14_update_deck_during_setup(const std::string& data); + void handle_6xB3x15_unused_hard_reset_server_state(const std::string& data); + void handle_6xB3x1B_update_player_name(const std::string& data); + void handle_6xB3x1D_start_battle(const std::string& data); + void handle_6xB3x21_end_battle(const std::string& data); + void handle_6xB3x28_end_defense_list(const std::string& data); + void handle_6xB3x2B_ignored(const std::string&); + void handle_6xB3x34_subtract_ally_atk_points(const std::string& data); + void handle_6xB3x37_client_ready_to_advance_from_starter_roll_phase(const std::string& data); + void handle_6xB3x3A_ignored(const std::string& data); + void handle_6xB3x40_map_list_request(const std::string& data); + void handle_6xB3x41_map_request(const std::string& data); + void handle_6xB3x48_end_turn(const std::string& data); + void handle_6xB3x49_card_counts(const std::string& data); + void unknown_8023D4E0(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); + void unknown_8023EEF4(); + void execute_bomb_assist_effect(); + void replace_targets_due_to_destruction_or_conditions( + ActionState* as); + bool any_target_exists_for_attack(const ActionState& as); + uint8_t get_current_team_turn2() const; + void unknown_8023EE48(); + void unknown_8023EE80(); + void unknown_802402F4(); + void send_6xB4x39() const; + void send_6xB4x05(); // Recomputes the map occupied bits, so can't be const + void send_6xB4x02_for_all_players_if_needed(bool always_send = false); + void send_6xB4x50() const; + + std::vector> const_cast_set_cards_v( + const std::vector>& cards); +private: + typedef void (Server::*handler_t)(const std::string&); + static const std::unordered_map subcommand_handlers; + + std::weak_ptr w_base; + +public: + uint32_t battle_finished; + uint32_t battle_in_progress; + uint32_t round_num; + BattlePhase battle_phase; + uint8_t first_team_turn; + uint8_t current_team_turn1; + SetupPhase setup_phase; + RegistrationPhase registration_phase; + ActionSubphase action_subphase; + uint8_t current_team_turn2; + ActionState pending_attacks[0x20]; + uint32_t num_pending_attacks; + parray client_done_enqueuing_attacks; + parray player_ready_to_end_phase; + std::shared_ptr random_crypt; + uint32_t unknown_a10; + uint32_t overall_time_expired; + // Note: In the original implementation, this is a uint32_t and is measured in + // seconds. In our environment, the simplest implementation uses now(), which + // returns microseconds, so we use a uint64_t instead. + uint64_t battle_start_usecs; + uint32_t should_copy_prev_states_to_current_states; + std::shared_ptr card_special; + std::shared_ptr state_flags; + std::shared_ptr player_states[4]; + parray clients_done_in_mulligan_phase; + uint32_t num_pending_attacks_with_cards; + std::shared_ptr attack_cards[0x20]; + ActionState pending_attacks_with_cards[0x20]; + uint32_t unknown_a14; + uint32_t unknown_a15; + parray defense_list_ended_for_client; + std::shared_ptr assist_server; + uint16_t next_assist_card_set_number; + std::shared_ptr ruler_server; + parray, 2>, 5> warp_positions; // Array indexes are (type, end, x/y) + parray team_exp; + parray team_dice_boost; + parray team_client_count; + parray team_num_ally_fcs_destroyed; + parray team_num_cards_destroyed; + uint32_t hard_reset_flag; + uint8_t tournament_flag; + parray num_trap_tiles_of_type; + parray chosen_trap_tile_index_of_type; + parray, 8>, 5> trap_tile_locs; + ActionState pb_action_states[4]; + parray has_done_pb; + parray, 4> has_done_pb_with_client; + mutable uint32_t num_6xB4x06_commands_sent; + mutable uint32_t prev_num_6xB4x06_commands_sent; +}; + + + +} // namespace Episode3 diff --git a/src/Lobby.hh b/src/Lobby.hh index e609e578..800886b3 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -17,6 +17,7 @@ #include "Text.hh" #include "Quest.hh" #include "Items.hh" +#include "Episode3/Server.hh" struct Lobby { enum Flag { @@ -71,6 +72,7 @@ struct Lobby { uint32_t random_seed; std::shared_ptr random; std::shared_ptr common_item_creator; + std::shared_ptr ep3_server_base; // lobby stuff uint8_t event; diff --git a/src/Main.cc b/src/Main.cc index 8fe89291..df187769 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -148,6 +148,18 @@ void populate_state_from_config(shared_ptr s, s->episode_3_send_function_call_enabled = false; } + try { + s->catch_handler_exceptions = d.at("CatchHandlerExceptions")->as_bool(); + } catch (const out_of_range&) { + s->catch_handler_exceptions = true; + } + + try { + s->ep3_behavior_flags = d.at("Episode3BehaviorFlags")->as_int(); + } catch (const out_of_range&) { + s->ep3_behavior_flags = 0; + } + shared_ptr log_levels_json; try { log_levels_json = d.at("LogLevels"); @@ -292,6 +304,8 @@ The options are:\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\ @@ -322,6 +336,7 @@ enum class Behavior { DECOMPRESS_BC0, ENCRYPT_DATA, DECRYPT_DATA, + DECRYPT_TRIVIAL_DATA, FIND_DECRYPTION_SEED, DECODE_QUEST_FILE, DECODE_SJIS, @@ -339,6 +354,7 @@ static bool behavior_takes_input_filename(Behavior b) { (b == Behavior::DECOMPRESS_BC0) || (b == Behavior::ENCRYPT_DATA) || (b == Behavior::DECRYPT_DATA) || + (b == Behavior::DECRYPT_TRIVIAL_DATA) || (b == Behavior::DECODE_QUEST_FILE) || (b == Behavior::DECODE_SJIS) || (b == Behavior::EXTRACT_GSL) || @@ -353,6 +369,7 @@ static bool behavior_takes_output_filename(Behavior b) { (b == Behavior::DECOMPRESS_BC0) || (b == Behavior::ENCRYPT_DATA) || (b == Behavior::DECRYPT_DATA) || + (b == Behavior::DECRYPT_TRIVIAL_DATA) || (b == Behavior::DECODE_SJIS); } @@ -381,6 +398,7 @@ int main(int argc, char** argv) { const char* replay_required_access_key = ""; 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")) { @@ -398,6 +416,8 @@ int main(int argc, char** argv) { 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")) { @@ -446,6 +466,9 @@ int main(int argc, char** argv) { 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")) { @@ -476,10 +499,14 @@ int main(int argc, char** argv) { auto read_input_data = [&]() -> string { string data; if (input_filename && strcmp(input_filename, "-")) { - return load_file(input_filename); + data = load_file(input_filename); } else { - return read_all(stdin); + data = read_all(stdin); } + if (parse_data) { + data = parse_data_string(data); + } + return data; }; auto write_output_data = [&](const void* data, size_t size) { @@ -500,10 +527,6 @@ int main(int argc, char** argv) { case Behavior::COMPRESS_BC0: case Behavior::DECOMPRESS_BC0: { string data = read_input_data(); - if (parse_data) { - data = parse_data_string(data); - } - size_t input_bytes = data.size(); if (behavior == Behavior::COMPRESS_PRS) { data = prs_compress(data); @@ -548,9 +571,6 @@ int main(int argc, char** argv) { } string data = read_input_data(); - if (parse_data) { - data = parse_data_string(data); - } size_t original_size = data.size(); data.resize((data.size() + 7) & (~7), '\0'); @@ -584,6 +604,14 @@ int main(int argc, char** argv) { break; } + case Behavior::DECRYPT_TRIVIAL_DATA: { + uint8_t basis = stoul(seed, nullptr, 16); + string data = read_input_data(); + decrypt_trivial_gci_data(data.data(), data.size(), basis); + write_output_data(data.data(), data.size()); + break; + } + case Behavior::FIND_DECRYPTION_SEED: { if (find_decryption_seed_plaintexts.empty() || !find_decryption_seed_ciphertext) { throw runtime_error("both --encrypted and --decrypted must be specified"); @@ -681,9 +709,6 @@ int main(int argc, char** argv) { case Behavior::DECODE_SJIS: { string data = read_input_data(); - if (parse_data) { - data = parse_data_string(data); - } auto decoded = decode_sjis(data); write_output_data(decoded.data(), decoded.size() * sizeof(decoded[0])); break; @@ -725,26 +750,33 @@ int main(int argc, char** argv) { case Behavior::SHOW_EP3_DATA: { config_log.info("Collecting Episode 3 data"); - Ep3DataIndex index("system/ep3", true); + Episode3::DataIndex index("system/ep3", true); - auto map_ids = index.all_map_ids(); - log_info("%zu maps", map_ids.size()); - for (uint32_t map_id : map_ids) { - auto map = index.get_map(map_id); - string name = map->map.name; - string location = map->map.location_name; - log_info("(Map %08" PRIX32 ") %s @ %s", map_id, name.c_str(), location.c_str()); - // TODO: Print more information about the map here - } + if (ep3_card_id == 0xFFFF) { + auto map_ids = index.all_map_ids(); + log_info("%zu maps", map_ids.size()); + for (uint32_t map_id : map_ids) { + auto map = index.definition_for_map_number(map_id); + string s = map->map.str(&index); + fprintf(stdout, "%s\n", s.c_str()); + } - auto card_ids = index.all_card_ids(); - log_info("%zu card definitions", card_ids.size()); - for (uint32_t card_id : card_ids) { - auto entry = index.get_card_definition(card_id); + auto card_ids = index.all_card_ids(); + log_info("%zu card definitions", card_ids.size()); + for (uint32_t card_id : card_ids) { + auto entry = index.definition_for_card_id(card_id); + string s = entry->def.str(); + string tags = entry->debug_tags.empty() ? "(none)" : join(entry->debug_tags, ", "); + string text = entry->text.empty() ? "(No text available)" : entry->text; + fprintf(stdout, "%s\nTags: %s\n%s\n\n", s.c_str(), tags.c_str(), text.c_str()); + } + + } else { + auto entry = index.definition_for_card_id(ep3_card_id); string s = entry->def.str(); string tags = entry->debug_tags.empty() ? "(none)" : join(entry->debug_tags, ", "); string text = entry->text.empty() ? "(No text available)" : entry->text; - log_info("%s\nTags: %s\n%s\n", s.c_str(), tags.c_str(), text.c_str()); + fprintf(stdout, "%s\nTags: %s\n%s\n", s.c_str(), tags.c_str(), text.c_str()); } break; @@ -824,7 +856,7 @@ int main(int argc, char** argv) { state->load_bb_file("ItemRT.rel"))); config_log.info("Collecting Episode 3 data"); - state->ep3_data_index.reset(new Ep3DataIndex("system/ep3")); + state->ep3_data_index.reset(new Episode3::DataIndex("system/ep3")); config_log.info("Collecting quest metadata"); state->quest_index.reset(new QuestIndex("system/quests")); diff --git a/src/PSOEncryption.cc b/src/PSOEncryption.cc index 2330e418..31425291 100644 --- a/src/PSOEncryption.cc +++ b/src/PSOEncryption.cc @@ -833,4 +833,15 @@ shared_ptr PSOBBMultiKeyImitatorEncryption::ensure_crypt() { } } return this->active_crypt; -} \ No newline at end of file +} + + + +void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis) { + uint8_t* bytes = reinterpret_cast(data); + uint8_t key = basis + 0x80; + for (size_t z = 0; z < size; z++) { + key = (key * 5) + 1; + bytes[z] ^= key; + } +} diff --git a/src/PSOEncryption.hh b/src/PSOEncryption.hh index ce78b7b3..9a7dc22b 100644 --- a/src/PSOEncryption.hh +++ b/src/PSOEncryption.hh @@ -226,3 +226,7 @@ protected: std::string seed; bool jsd1_use_detector_seed; }; + + + +void decrypt_trivial_gci_data(void* data, size_t size, uint8_t basis); diff --git a/src/Player.hh b/src/Player.hh index e51f3a99..59ff9b27 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -11,7 +11,7 @@ #include "LevelTable.hh" #include "Version.hh" #include "Text.hh" -#include "Episode3.hh" +#include "Episode3/DataIndex.hh" @@ -407,7 +407,7 @@ struct PSOPlayerDataGCEp3 { // For command 61 parray blocked_senders; le_uint32_t auto_reply_enabled; char auto_reply[0xAC]; - Ep3Config ep3_config; + Episode3::PlayerConfig ep3_config; } __attribute__((packed)); struct PSOPlayerDataBB { // For command 61 @@ -506,7 +506,7 @@ public: std::unique_ptr pending_card_trade; // Null unless the client is Episode 3 and has sent its config already - std::shared_ptr ep3_config; + std::shared_ptr ep3_config; // These are only used if the client is BB std::string bb_username; diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 88a74a29..892875d8 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -878,9 +878,9 @@ static HandlerResult S_6x(shared_ptr, string map_data = prs_decompress( data.data() + sizeof(cmd), data.size() - sizeof(cmd)); save_file(filename, map_data); - if (map_data.size() != sizeof(Ep3Map)) { + if (map_data.size() != sizeof(Episode3::MapDefinition)) { session.log.warning("Wrote %zu bytes to %s (expected %zu bytes; the file may be invalid)", - map_data.size(), filename.c_str(), sizeof(Ep3Map)); + map_data.size(), filename.c_str(), sizeof(Episode3::MapDefinition)); } else { session.log.info("Wrote %zu bytes to %s", map_data.size(), filename.c_str()); } @@ -1016,7 +1016,7 @@ static HandlerResult S_13_A7(shared_ptr, if (sf->f.get()) { session.log.info("Writing %" PRIu32 " bytes to %s", cmd.data_size.load(), sf->output_filename.c_str()); - fwritex(sf->f.get(), cmd.data, cmd.data_size); + fwritex(sf->f.get(), cmd.data.data(), cmd.data_size); } sf->remaining_bytes -= cmd.data_size; diff --git a/src/Quest.cc b/src/Quest.cc index 0dc035ab..cccfe2dc 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -459,10 +459,10 @@ Quest::Quest(const string& bin_filename) case GameVersion::XB: case GameVersion::GC: { if (this->category == QuestCategory::EPISODE_3) { - if (bin_decompressed.size() != sizeof(Ep3Map)) { + if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) { throw invalid_argument("file is incorrect size"); } - auto* header = reinterpret_cast(bin_decompressed.data()); + auto* header = reinterpret_cast(bin_decompressed.data()); this->joinable = false; this->episode = 0xFF; this->name = decode_sjis(header->name); @@ -677,21 +677,14 @@ string Quest::decode_gci( // wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the // remaining bytes completely unencrypted (but still compressed). size_t unscramble_size = min(0x100, data.size()); - { - uint8_t key = 0x80; // Technically basis + 0x80, but basis is zero - for (size_t z = 0; z < unscramble_size; z++) { - key = (key * 5) + 1; - data[z] ^= key; - } - } + decrypt_trivial_gci_data(data.data(), unscramble_size, 0); size_t decompressed_size = prs_decompress_size(data); - if (decompressed_size != sizeof(Ep3Map)) { + if (decompressed_size != sizeof(Episode3::MapDefinition)) { throw runtime_error(string_printf( "decompressed quest is 0x%zX bytes; expected 0x%zX bytes", - decompressed_size, sizeof(Ep3Map))); + decompressed_size, sizeof(Episode3::MapDefinition))); } - return data; } else { @@ -800,7 +793,7 @@ static pair decode_qst_t(FILE* f) { if (header.flag != dest->size() / 0x400) { throw runtime_error("qst contains chunks out of order"); } - dest->append(reinterpret_cast(cmd.data), cmd.data_size); + dest->append(reinterpret_cast(cmd.data.data()), cmd.data_size); } else { throw runtime_error("invalid command in qst file"); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index cd1b8fb3..843718ad 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -907,98 +907,24 @@ static void on_ep3_server_data_request(shared_ptr s, shared_ptr( - data, sizeof(G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5), 0xFFFF); + const auto& header = check_size_t( + data, sizeof(G_CardBattleCommandHeader), 0xFFFF); - // TODO: We can support this since set_mask_for_ep3_game_command already - // exists; I just don't want to make a copy of the data string - if (header.mask_key != 0) { - throw runtime_error("Episode 3 server data request has nonzero mask key"); - } - - if (header.basic_header.subcommand != 0xB3) { + if (header.subcommand != 0xB3) { throw runtime_error("unknown Episode 3 server data request"); } - switch (header.subsubcommand) { - // Phase 1: map select - case 0x40: - check_size_t(data); - send_ep3_map_list(s, l); - break; - case 0x41: { - const auto& cmd = check_size_t(data); - send_ep3_map_data(s, l, cmd.map_number); - break; + if (!l->ep3_server_base || l->ep3_server_base->server->battle_finished) { + if (!l->ep3_server_base) { + l->log.info("Creating Episode 3 server state"); + } else { + l->log.info("Recreating Episode 3 server state"); } - - /* What follows is some raw code that has survived since the days of khyller - * (approx. 2004). Much more research and engineering is needed to get - * Episode III battles to work, but this could be used as a starting point. - - // phase 2: deck/name entry - case 0x13: - ti = FindTeam(s, c->teamID); - memcpy(&ti->ep3game, ((DWORD)c->bufferin + 0x14), 0x2AC); - CommandEp3InitChangeState(s, c, 1); - break; - case 0x1B: - ti = FindTeam(s, c->teamID); - memcpy(&ti->ep3names[*(BYTE*)((DWORD)c->bufferin + 0x24)], ((DWORD)c->bufferin + 0x14), 0x14); // NOTICE: may be 0x26 instead of 0x24 - CommandEp3InitSendNames(s, c); - break; - case 0x14: - ti = FindTeam(s, c->teamID); - memcpy(&ti->ep3decks[*(BYTE*)((DWORD)c->bufferin + 0x14)], ((DWORD)c->bufferin + 0x18), 0x58); // NOTICE: may be 0x16 instead of 0x14 - Ep3FillHand(&ti->ep3game, &ti->ep3decks[*(BYTE*)((DWORD)c->bufferin + 0x14)], &ti->ep3pcs[*(BYTE*)((DWORD)c->bufferin + 0x14)]); - //Ep3RollDice(&ti->ep3game, &ti->ep3pcs[*(BYTE*)((DWORD)c->bufferin + 0x14)]); - CommandEp3InitSendDecks(s, c); - CommandEp3InitSendMapLayout(s, c); - for (x = 0, param = 0; x < 4; x++) if ((ti->ep3decks[x].clientID != 0xFFFFFFFF) && (ti->ep3names[x].clientID != 0xFF)) param++; - if (param >= ti->ep3game.numPlayers) CommandEp3InitChangeState(s, c, 3); - break; - // phase 3: hands & game states - case 0x1D: - ti = FindTeam(s, c->teamID); - Ep3ReprocessMap(&ti->ep3game); - CommandEp3SendMapData(s, c, ti->ep3game.mapID); - for (y = 0, x = 0; x < 4; x++) - { - if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue; - Ep3EquipCard(&ti->ep3game, &ti->ep3decks[x], &ti->ep3pcs[x], 0); // equip SC card - CommandEp3InitHandUpdate(s, c, x); - CommandEp3InitStatUpdate(s, c, x); - y++; - } - CommandEp3Init_B4_06(s, c, (y == 4) ? true : false); - CommandEp3InitSendMapLayout(s, c); - for (x = 0; x < 4; x++) - { - if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue; - CommandEp3Init_B4_4E(s, c, x); - CommandEp3Init_B4_4C(s, c, x); - CommandEp3Init_B4_4D(s, c, x); - CommandEp3Init_B4_4F(s, c, x); - } - CommandEp3InitSendDecks(s, c); - CommandEp3InitSendMapLayout(s, c); - for (x = 0; x < 4; x++) - { - if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue; - CommandEp3InitHandUpdate(s, c, x); - } - CommandEp3InitSendNames(s, c); - CommandEp3InitChangeState(s, c, 4); - CommandEp3Init_B4_50(s, c); - CommandEp3InitSendMapLayout(s, c); - CommandEp3Init_B4_39(s, c); // MISSING: 60 00 AC 00 B4 2A 00 00 39 56 00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - CommandEp3InitBegin(s, c); - break; */ - - default: - c->log.warning("Unknown Episode III server data request: %02X", - header.subsubcommand); + l->ep3_server_base = make_shared( + l, s->ep3_data_index, s->ep3_behavior_flags, l->random_seed); + l->ep3_server_base->init(); } + l->ep3_server_base->server->on_server_data_input(data); } static void on_ep3_tournament_control(shared_ptr, shared_ptr c, @@ -1860,7 +1786,7 @@ static void on_player_data(shared_ptr s, shared_ptr c, throw runtime_error("non-Episode 3 client sent Episode 3 player data"); } const auto* pd3 = &check_size_t(data); - c->game_data.ep3_config.reset(new Ep3Config(pd3->ep3_config)); + c->game_data.ep3_config.reset(new Episode3::PlayerConfig(pd3->ep3_config)); pd = reinterpret_cast(pd3); } else { pd = &check_size_t(data, sizeof(PSOPlayerDataV3), diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 36a54528..56b60d56 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -209,8 +209,8 @@ static void on_subcommand_forward_check_size_ep3_game(shared_ptr, static void on_subcommand_ep3_battle_subs(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& orig_data) { - check_size_sc( - orig_data, sizeof(G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5), 0xFFFF); + check_size_sc( + orig_data, sizeof(G_CardBattleCommandHeader), 0xFFFF); if (!l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { return; } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index d6ee7853..12a58a74 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -303,7 +303,7 @@ void send_quest_buffer_overflow( S_WriteFile_13_A7 cmd; cmd.filename = filename; - memcpy(cmd.data, fn->code.data(), fn->code.size()); + memcpy(cmd.data.data(), fn->code.data(), fn->code.size()); if (fn->code.size() < 0x400) { memset(&cmd.data[fn->code.size()], 0, 0x400 - fn->code.size()); } @@ -1719,28 +1719,6 @@ void send_ep3_rank_update(shared_ptr c) { send_command_t(c, 0xB7, 0x00, cmd); } -void send_ep3_map_list(shared_ptr s, shared_ptr l) { - const auto& data = s->ep3_data_index->get_compressed_map_list(); - - StringWriter w; - uint32_t subcommand_size = (data.size() + sizeof(G_MapList_GC_Ep3_6xB6x40) + 3) & (~3); - w.put( - G_MapList_GC_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, data.size(), 0}); - w.write(data); - send_command(l, 0x6C, 0x00, w.str()); -} - -void send_ep3_map_data(shared_ptr s, shared_ptr l, uint32_t map_id) { - auto entry = s->ep3_data_index->get_map(map_id); - const auto& compressed = entry->compressed(); - - StringWriter w; - uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3); - w.put({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, entry->map.map_number.load(), compressed.size(), 0}); - w.write(compressed); - send_command(l, 0x6C, 0x00, w.str()); -} - void send_ep3_card_battle_table_state(shared_ptr l, uint16_t table_number) { S_CardBattleTableState_GC_Ep3_E4 cmd; for (size_t z = 0; z < 4; z++) { @@ -1778,8 +1756,8 @@ void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) { throw logic_error("Episode 3 game command is too short for masking"); } - auto* header = reinterpret_cast(vdata); - size_t command_bytes = header->basic_header.size * 4; + auto* header = reinterpret_cast(vdata); + size_t command_bytes = header->size * 4; if (command_bytes != size) { throw runtime_error("command size field does not match actual size"); } @@ -1826,7 +1804,7 @@ void send_quest_file_chunk( S_WriteFile_13_A7 cmd; cmd.filename = filename; - memcpy(cmd.data, data, size); + memcpy(cmd.data.data(), data, size); if (size < 0x400) { memset(&cmd.data[size], 0, 0x400 - size); } diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 132c2e1a..8235fd3e 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -278,10 +278,6 @@ void send_ep3_media_update( uint32_t which, const std::string& compressed_data); void send_ep3_rank_update(std::shared_ptr c); -void send_ep3_map_list( - std::shared_ptr s, std::shared_ptr l); -void send_ep3_map_data( - std::shared_ptr s, std::shared_ptr l, uint32_t map_id); void send_ep3_card_battle_table_state(std::shared_ptr l, uint16_t table_number); // Pass mask_key = 0 to unmask the command diff --git a/src/Server.cc b/src/Server.cc index 6a3ef81a..86b6dfc9 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -144,11 +144,15 @@ void Server::on_client_input(Channel& ch, uint16_t command, uint32_t flag, std:: if (c->should_disconnect) { server->disconnect_client(c); } else { - try { + if (server->state->catch_handler_exceptions) { + try { + on_command(server->state, c, command, flag, data); + } catch (const exception& e) { + server_log.warning("Error processing client command: %s", e.what()); + c->should_disconnect = true; + } + } else { on_command(server->state, c, command, flag, data); - } catch (const exception& e) { - server_log.warning("Error processing client command: %s", e.what()); - c->should_disconnect = true; } if (c->should_disconnect) { server->disconnect_client(c); diff --git a/src/ServerState.cc b/src/ServerState.cc index d76f29d7..03853eef 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -23,6 +23,8 @@ ServerState::ServerState() allow_saving(true), item_tracking_enabled(true), episode_3_send_function_call_enabled(false), + catch_handler_exceptions(true), + ep3_behavior_flags(0), run_shell_behavior(RunShellBehavior::DEFAULT), next_lobby_id(1), pre_lobby_event(0), ep3_menu_song(-1) { diff --git a/src/ServerState.hh b/src/ServerState.hh index c2b99ca2..bae36be8 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -50,13 +50,15 @@ struct ServerState { bool allow_saving; bool item_tracking_enabled; bool episode_3_send_function_call_enabled; + bool catch_handler_exceptions; + uint32_t ep3_behavior_flags; RunShellBehavior run_shell_behavior; std::vector> bb_private_keys; std::shared_ptr function_code_index; std::shared_ptr pc_patch_file_index; std::shared_ptr bb_patch_file_index; std::shared_ptr dol_file_index; - std::shared_ptr ep3_data_index; + std::shared_ptr ep3_data_index; std::shared_ptr quest_index; std::shared_ptr level_table; std::shared_ptr battle_params; diff --git a/src/Text.hh b/src/Text.hh index 1ef92983..94c1c965 100644 --- a/src/Text.hh +++ b/src/Text.hh @@ -186,11 +186,18 @@ template struct parray { ItemT items[Count]; + parray(ItemT v) { + this->clear(v); + } template ::value, bool> = true> parray() { this->clear(0); } - template ::value, bool> = true> + template ::value, bool> = true> + parray() { + this->clear(nullptr); + } + template ::value && !std::is_pointer::value, bool> = true> parray() { } parray(const parray& other) { @@ -203,10 +210,10 @@ struct parray { this->operator=(s); } - constexpr size_t size() { + constexpr static size_t size() { return Count; } - constexpr size_t bytes() { + constexpr static size_t bytes() { return Count * sizeof(ItemT); } ItemT* data() { @@ -220,17 +227,58 @@ struct parray { if (index >= Count) { throw std::out_of_range("array index out of bounds"); } - return this->items[index]; + // Note: This looks really dumb, but apparently works around an issue in GCC + // that causes a "returning address of temporary" error here. + return *&this->items[index]; } const ItemT& operator[](size_t index) const { if (index >= Count) { throw std::out_of_range("array index out of bounds"); } - return this->items[index]; + return *&this->items[index]; + } + + ItemT& at(size_t index) { + return this->operator[](index); + } + const ItemT& at(size_t index) const { + return this->operator[](index); + } + + ItemT* sub_ptr(size_t offset = 0, size_t count = Count) { + if (offset + count > Count) { + throw std::out_of_range("sub-array out of range"); + } + return &this->items[offset]; + } + const ItemT* sub_ptr(size_t offset = 0, size_t count = Count) const { + if (offset + count > Count) { + throw std::out_of_range("sub-array out of range"); + } + return &this->items[offset]; + } + + template + parray& sub(size_t offset = 0) { + if (offset + SubCount > Count) { + throw std::out_of_range("sub-array out of range"); + } + return *reinterpret_cast*>(&this->items[offset]); + } + template + const parray& sub(size_t offset = 0) const { + if (offset + SubCount > Count) { + throw std::out_of_range("sub-array out of range"); + } + return *reinterpret_cast*>(&this->items[offset]); + } + + void assign_range(const ItemT* new_items, size_t count = Count, size_t start_offset = 0) { + for (size_t x = start_offset; (x < Count) && (x < start_offset + count); x++) { + this->items[x] = new_items[x]; + } } - // TODO: These can be made faster by only clearing the unused space after the - // strncpy_t (if any) instead of clearing all the space every time parray& operator=(const parray& s) { for (size_t x = 0; x < Count; x++) { this->items[x] = s.items[x]; @@ -284,6 +332,11 @@ struct parray { this->items[x] = v; } } + void clear() { + for (size_t x = 0; x < Count; x++) { + this->items[x] = ItemT(); + } + } void clear_after(size_t position, ItemT v = 0) { for (size_t x = position; x < Count; x++) { this->items[x] = v; diff --git a/system/config.example.json b/system/config.example.json index 96a87e31..19d42168 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -248,12 +248,22 @@ // they are at the newserv main menu. If set, this value must be an integer. // "Episode3MenuSong": 0, + // Episode 3 battle behavior flags. When set to zero, battles behave as they + // did on the original Sega servers. + // TODO: Document what nonzero values do here. + "Episode3BehaviorFlags": 0, + // Whether to enable patches on Episode 3 USA. This functionality depends on // exploiting a bug in Episode 3, and while it seems to work reliably on // Dolphin, it hasn't been tested on a real GameCube. So, newserv doesn't // enable Episode 3 patches by default; it only does so if this option is on. // "EnableEpisode3SendFunctionCall": true, + // Whether to enable certain exception handling. Disabling this is generally + // only useful for debugging newserv itself, and it should usually be left on + // (which is the default behavior). + "CatchHandlerExceptions": true, + // By default, the server keeps track of items in all games, even for versions // other than Blue Burst. This enables use of the $what command, as well as // protection against item duplication cheats (the cheater is disconnected