#pragma once #include #include #include #include #include #include #include #include #include "ChoiceSearch.hh" #include "FileContentsCache.hh" #include "ItemData.hh" #include "LevelTable.hh" #include "PSOEncryption.hh" #include "Text.hh" #include "Version.hh" class Client; class ItemParameterTable; // PSO V2 stored some extra data in the character structs in a format that I'm // sure Sega thought was very clever for backward compatibility, but for us is // just plain annoying. Specifically, they used the third and fourth bytes of // the InventoryItem struct to store some things not present in V1. The game // stores arrays of bytes striped across these structures. In newserv, we call // those fields extension_data. They contain: // items[0].extension_data1 through items[19].extension_data1: // Extended technique levels. The values in the technique_levels_v1 array // only go up to 14 (tech level 15); if the player has a technique above // level 15, the corresponding extension_data1 field holds the remaining // levels (so a level 20 tech would have 14 in technique_levels_v1 and 5 // in the corresponding item's extension_data1 field). // items[0].extension_data2 through items[3].extension_data2: // The flags field from the PSOGCCharacterFile::Character struct; see // SaveFileFormats.hh for details. // items[4].extension_data2 through items[7].extension_data2: // The timestamp when the character was last saved, in seconds since // January 1, 2000. Stored little-endian, so items[4] contains the LSB. // items[8].extension_data2 through items[12].extension_data2: // Number of power materials, mind materials, evade materials, def // materials, and luck materials (respectively) used by the player. // items[13].extension_data2 through items[15].extension_data2: // Unknown. These are not an array, but do appear to be related. struct PlayerInventoryItem { /* 00 */ uint8_t present = 0; /* 01 */ uint8_t unknown_a1 = 0; // See note above about these fields /* 02 */ uint8_t extension_data1 = 0; /* 03 */ uint8_t extension_data2 = 0; /* 04 */ le_uint32_t flags = 0; // 8 = equipped /* 08 */ ItemData data; /* 1C */ PlayerInventoryItem() = default; explicit PlayerInventoryItem(const ItemData& item, bool equipped = false); } __attribute__((packed)); struct PlayerBankItem { /* 00 */ ItemData data; /* 14 */ le_uint16_t amount = 0; /* 16 */ le_uint16_t present = 0; /* 18 */ inline bool operator<(const PlayerBankItem& other) const { return this->data < other.data; } } __attribute__((packed)); struct PlayerInventory { /* 0000 */ uint8_t num_items = 0; /* 0001 */ uint8_t hp_from_materials = 0; /* 0002 */ uint8_t tp_from_materials = 0; /* 0003 */ uint8_t language = 0; /* 0004 */ parray items; /* 034C */ size_t find_item(uint32_t item_id) const; size_t find_item_by_primary_identifier(uint32_t primary_identifier) const; size_t find_equipped_item(EquipSlot slot) const; bool has_equipped_item(EquipSlot slot) const; void equip_item_id(uint32_t item_id, EquipSlot slot, bool allow_overwrite); void equip_item_index(size_t index, EquipSlot slot, bool allow_overwrite); void unequip_item_id(uint32_t item_id); void unequip_item_slot(EquipSlot slot); void unequip_item_index(size_t index); size_t remove_all_items_of_type(uint8_t data0, int16_t data1 = -1); void decode_from_client(std::shared_ptr c); void encode_for_client(std::shared_ptr c); } __attribute__((packed)); struct PlayerBank { /* 0000 */ le_uint32_t num_items = 0; /* 0004 */ le_uint32_t meseta = 0; /* 0008 */ parray items; /* 12C8 */ void add_item(const ItemData& item, Version version); ItemData remove_item(uint32_t item_id, uint32_t amount, Version version); size_t find_item(uint32_t item_id); void sort(); void assign_ids(uint32_t base_id); } __attribute__((packed)); struct PlayerDispDataBB; struct PlayerVisualConfig { /* 00 */ pstring name; /* 10 */ parray unknown_a2; /* 18 */ le_uint32_t name_color = 0xFFFFFFFF; // ARGB /* 1C */ uint8_t extra_model = 0; /* 1D */ parray unused; // See compute_name_color_checksum for details on how this is computed. If the // value is incorrect, V1 and V2 will ignore the name_color field and use the // default color instead. This field is ignored on GC; on BB (and presumably // Xbox), if this has a nonzero value, the "Change Name" option appears in the // character selection menu. /* 2C */ le_uint32_t name_color_checksum = 0; /* 30 */ uint8_t section_id = 0; /* 31 */ uint8_t char_class = 0; // validation_flags specifies that some parts of this structure are not valid // and should be ignored. The bits are: // -----FCS // F = class_flags is incorrect for the character's char_class value // C = char_class is out of range // S = section_id is out of range /* 32 */ uint8_t validation_flags = 0; /* 33 */ uint8_t version = 0; // class_flags specifies features of the character's class. The bits are: // -------- -------- -------- FRHANMfm // F = force, R = ranger, H = hunter // A = android, N = newman, M = human // f = female, m = male /* 34 */ le_uint32_t class_flags = 0; /* 38 */ le_uint16_t costume = 0; /* 3A */ le_uint16_t skin = 0; /* 3C */ le_uint16_t face = 0; /* 3E */ le_uint16_t head = 0; /* 40 */ le_uint16_t hair = 0; /* 42 */ le_uint16_t hair_r = 0; /* 44 */ le_uint16_t hair_g = 0; /* 46 */ le_uint16_t hair_b = 0; /* 48 */ le_float proportion_x = 0.0; /* 4C */ le_float proportion_y = 0.0; /* 50 */ static uint32_t compute_name_color_checksum(uint32_t name_color); void compute_name_color_checksum(); void enforce_lobby_join_limits_for_version(Version v); } __attribute__((packed)); struct PlayerDispDataDCPCV3 { /* 00 */ PlayerStats stats; /* 24 */ PlayerVisualConfig visual; /* 74 */ parray config; /* BC */ parray technique_levels_v1; /* D0 */ void enforce_lobby_join_limits_for_version(Version v); PlayerDispDataBB to_bb(uint8_t to_language, uint8_t from_language) const; } __attribute__((packed)); struct PlayerDispDataBBPreview { /* 00 */ le_uint32_t experience = 0; /* 04 */ le_uint32_t level = 0; // The name field in this structure is used for the player's Guild Card // number, apparently (possibly because it's a char array and this is BB) /* 08 */ PlayerVisualConfig visual; /* 58 */ pstring name; /* 78 */ uint32_t play_time = 0; /* 7C */ } __attribute__((packed)); // BB player appearance and stats data struct PlayerDispDataBB { /* 0000 */ PlayerStats stats; /* 0024 */ PlayerVisualConfig visual; /* 0074 */ pstring name; /* 008C */ le_uint32_t play_time = 0; /* 0090 */ uint32_t unknown_a3 = 0; /* 0094 */ parray config; /* 017C */ parray technique_levels_v1; /* 0190 */ void enforce_lobby_join_limits_for_version(Version v); PlayerDispDataDCPCV3 to_dcpcv3(uint8_t to_language, uint8_t from_language) const; PlayerDispDataBBPreview to_preview() const; void apply_preview(const PlayerDispDataBBPreview&); void apply_dressing_room(const PlayerDispDataBBPreview&); } __attribute__((packed)); struct GuildCardDC { /* 00 */ le_uint32_t player_tag = 0; /* 04 */ le_uint32_t guild_card_number = 0; /* 08 */ pstring name; /* 20 */ pstring description; /* 68 */ parray unused2; /* 79 */ uint8_t present = 0; /* 7A */ uint8_t language = 0; /* 7B */ uint8_t section_id = 0; /* 7C */ uint8_t char_class = 0; /* 7D */ } __attribute__((packed)); struct GuildCardPC { /* 00 */ le_uint32_t player_tag = 0; /* 04 */ le_uint32_t guild_card_number = 0; // TODO: Is the length of the name field correct here? /* 08 */ pstring name; /* 38 */ pstring description; /* EC */ uint8_t present = 0; /* ED */ uint8_t language = 0; /* EE */ uint8_t section_id = 0; /* EF */ uint8_t char_class = 0; /* F0 */ } __attribute__((packed)); struct GuildCardGC { /* 00 */ le_uint32_t player_tag = 0; /* 04 */ le_uint32_t guild_card_number = 0; /* 08 */ pstring name; /* 20 */ pstring description; /* 8C */ uint8_t present = 0; /* 8D */ uint8_t language = 0; /* 8E */ uint8_t section_id = 0; /* 8F */ uint8_t char_class = 0; /* 90 */ } __attribute__((packed)); struct GuildCardXB { /* 0000 */ le_uint32_t player_tag = 0; /* 0004 */ le_uint32_t guild_card_number = 0; /* 0008 */ le_uint32_t xb_user_id_high = 0; /* 000C */ le_uint32_t xb_user_id_low = 0; /* 0010 */ pstring name; /* 0028 */ pstring description; /* 0228 */ uint8_t present = 0; /* 0229 */ uint8_t language = 0; /* 022A */ uint8_t section_id = 0; /* 022B */ uint8_t char_class = 0; /* 022C */ } __attribute__((packed)); struct GuildCardBB { /* 0000 */ le_uint32_t guild_card_number = 0; /* 0004 */ pstring name; /* 0034 */ pstring team_name; /* 0054 */ pstring description; /* 0104 */ uint8_t present = 0; /* 0105 */ uint8_t language = 0; /* 0106 */ uint8_t section_id = 0; /* 0107 */ uint8_t char_class = 0; /* 0108 */ void clear(); } __attribute__((packed)); struct PlayerLobbyDataPC { le_uint32_t player_tag = 0; le_uint32_t guild_card_number = 0; // There's a strange behavior (bug? "feature"?) in Episode 3 where the start // button does nothing in the lobby (hence you can't "quit game") if the // client's IP address is zero. So, we fill it in with a fake nonzero value to // avoid this behavior, and to be consistent, we make IP addresses fake and // nonzero on all other versions too. be_uint32_t ip_address = 0x7F000001; le_uint32_t client_id = 0; pstring name; void clear(); } __attribute__((packed)); struct PlayerLobbyDataDCGC { le_uint32_t player_tag = 0; le_uint32_t guild_card_number = 0; be_uint32_t ip_address = 0x7F000001; le_uint32_t client_id = 0; pstring name; void clear(); } __attribute__((packed)); struct XBNetworkLocation { /* 00 */ le_uint32_t internal_ipv4_address = 0x0A0A0A0A; /* 04 */ le_uint32_t external_ipv4_address = 0x23232323; /* 08 */ le_uint16_t port = 9500; /* 0A */ parray mac_address = 0x77; /* 10 */ le_uint32_t unknown_a1; /* 14 */ le_uint32_t unknown_a2; /* 18 */ le_uint64_t account_id = 0xFFFFFFFFFFFFFFFF; /* 20 */ parray unknown_a3; /* 24 */ void clear(); } __attribute__((packed)); struct PlayerLobbyDataXB { /* 00 */ le_uint32_t player_tag = 0; /* 04 */ le_uint32_t guild_card_number = 0; /* 08 */ XBNetworkLocation netloc; /* 2C */ le_uint32_t client_id = 0; /* 30 */ pstring name; /* 40 */ void clear(); } __attribute__((packed)); struct PlayerLobbyDataBB { /* 00 */ le_uint32_t player_tag = 0; /* 04 */ le_uint32_t guild_card_number = 0; /* 08 */ le_uint32_t team_master_guild_card_number = 0; /* 0C */ le_uint32_t team_id = 0; /* 10 */ parray unknown_a1; /* 1C */ le_uint32_t client_id = 0; /* 20 */ pstring name; // If this field is zero, the "Press F1 for help" prompt appears in the corner // of the screen in the lobby and on Pioneer 2. /* 40 */ le_uint32_t hide_help_prompt = 1; /* 44 */ void clear(); } __attribute__((packed)); template struct ChallengeAwardState { using U32T = typename std::conditional::type; U32T rank_award_flags = 0; ChallengeTime maximum_rank; } __attribute__((packed)); template struct PlayerRecordsDCPC_Challenge { /* 00 */ le_uint16_t title_color = 0x7FFF; /* 02 */ parray unknown_u0; /* 04 */ pstring rank_title; /* 10 */ parray, 9> times_ep1_online; // TODO: This might be offline times /* 34 */ uint8_t grave_stage_num = 0; /* 35 */ uint8_t grave_floor = 0; /* 36 */ le_uint16_t grave_deaths = 0; // grave_time is encoded with the following bit fields: // YYYYMMMM DDDDDDDD HHHHHHHH mmmmmmmm // Y = year after 2000 (clamped to [0, 15]) // M = month // D = day // H = hour // m = minute /* 38 */ le_uint32_t grave_time = 0; /* 3C */ le_uint32_t grave_defeated_by_enemy_rt_index = 0; /* 40 */ le_float grave_x = 0.0f; /* 44 */ le_float grave_y = 0.0f; /* 48 */ le_float grave_z = 0.0f; /* 4C */ pstring grave_team; /* 60 */ pstring grave_message; /* 78 */ parray, 9> times_ep1_offline; // TODO: This might be online times /* 9C */ parray unknown_l4; /* A0 */ } __attribute__((packed)); struct PlayerRecordsDC_Challenge : PlayerRecordsDCPC_Challenge { } __attribute__((packed)); struct PlayerRecordsPC_Challenge : PlayerRecordsDCPC_Challenge { } __attribute__((packed)); template struct PlayerRecordsV3_Challenge { using U16T = typename std::conditional::type; using U32T = typename std::conditional::type; using FloatT = typename std::conditional::type; // Offsets are (1) relative to start of C5 entry, and (2) relative to start // of save file structure struct Stats { /* 00:1C */ U16T title_color = 0x7FFF; // XRGB1555 /* 02:1E */ parray unknown_u0; /* 04:20 */ parray, 9> times_ep1_online; /* 28:44 */ parray, 5> times_ep2_online; /* 3C:58 */ parray, 9> times_ep1_offline; /* 60:7C */ uint8_t grave_is_ep2 = 0; /* 61:7D */ uint8_t grave_stage_num = 0; /* 62:7E */ uint8_t grave_floor = 0; /* 63:7F */ uint8_t unknown_g0 = 0; /* 64:80 */ U16T grave_deaths = 0; /* 66:82 */ parray unknown_u4; /* 68:84 */ U32T grave_time = 0; // Encoded as in PlayerRecordsDCPC_Challenge /* 6C:88 */ U32T grave_defeated_by_enemy_rt_index = 0; /* 70:8C */ FloatT grave_x = 0.0f; /* 74:90 */ FloatT grave_y = 0.0f; /* 78:94 */ FloatT grave_z = 0.0f; /* 7C:98 */ pstring grave_team; /* 90:AC */ pstring grave_message; /* B0:CC */ parray unknown_m5; /* B4:D0 */ parray unknown_t6; /* C0:DC */ ChallengeAwardState ep1_online_award_state; /* C8:E4 */ ChallengeAwardState ep2_online_award_state; /* D0:EC */ ChallengeAwardState ep1_offline_award_state; /* D8:F4 */ } __attribute__((packed)); /* 0000:001C */ Stats stats; // On Episode 3, there are special cases that apply to this field - if the // text ends with certain strings, the player will have particle effects // emanate from their character in the lobby every 2 seconds. The effects are: // Ends with ":GOD" => blue circle // Ends with ":KING" => white particles // Ends with ":LORD" => rising yellow sparkles // Ends with ":CHAMP" => green circle /* 00D8:00F4 */ pstring rank_title; /* 00E4:0100 */ parray unknown_l7; /* 0100:011C */ } __attribute__((packed)); struct PlayerRecordsBB_Challenge { /* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555 /* 0002 */ parray unknown_u0; /* 0004 */ parray, 9> times_ep1_online; /* 0028 */ parray, 5> times_ep2_online; /* 003C */ parray, 9> times_ep1_offline; /* 0060 */ uint8_t grave_is_ep2 = 0; /* 0061 */ uint8_t grave_stage_num = 0; /* 0062 */ uint8_t grave_floor = 0; /* 0063 */ uint8_t unknown_g0 = 0; /* 0064 */ le_uint16_t grave_deaths = 0; /* 0066 */ parray unknown_u4; /* 0068 */ le_uint32_t grave_time = 0; // Encoded as in PlayerRecordsDCPC_Challenge /* 006C */ le_uint32_t grave_defeated_by_enemy_rt_index = 0; /* 0070 */ le_float grave_x = 0.0f; /* 0074 */ le_float grave_y = 0.0f; /* 0078 */ le_float grave_z = 0.0f; /* 007C */ pstring grave_team; /* 00A4 */ pstring grave_message; /* 00E4 */ parray unknown_m5; /* 00E8 */ parray unknown_t6; /* 00F4 */ ChallengeAwardState ep1_online_award_state; /* 00FC */ ChallengeAwardState ep2_online_award_state; /* 0104 */ ChallengeAwardState ep1_offline_award_state; /* 010C */ pstring rank_title; // Encrypted; see decrypt_challenge_rank_text /* 0124 */ parray unknown_l7; /* 0140 */ PlayerRecordsBB_Challenge() = default; PlayerRecordsBB_Challenge(const PlayerRecordsBB_Challenge& other) = default; PlayerRecordsBB_Challenge& operator=(const PlayerRecordsBB_Challenge& other) = default; PlayerRecordsBB_Challenge(const PlayerRecordsDC_Challenge& rec); PlayerRecordsBB_Challenge(const PlayerRecordsPC_Challenge& rec); PlayerRecordsBB_Challenge(const PlayerRecordsV3_Challenge& rec); operator PlayerRecordsDC_Challenge() const; operator PlayerRecordsPC_Challenge() const; operator PlayerRecordsV3_Challenge() const; } __attribute__((packed)); template struct PlayerRecords_Battle { using U16T = typename std::conditional::type; // On Episode 3, place_counts[0] is win count and [1] is loss count /* 00 */ parray place_counts; /* 08 */ U16T disconnect_count = 0; /* 0A */ parray unknown_a1; /* 10 */ parray unknown_a2; /* 18 */ } __attribute__((packed)); template DestT convert_player_disp_data(const SrcT&, uint8_t, uint8_t) { static_assert(always_false::v, "unspecialized convert_player_disp_data should never be called"); } template <> inline PlayerDispDataDCPCV3 convert_player_disp_data(const PlayerDispDataDCPCV3& src, uint8_t, uint8_t) { return src; } template <> inline PlayerDispDataDCPCV3 convert_player_disp_data( const PlayerDispDataBB& src, uint8_t to_language, uint8_t from_language) { return src.to_dcpcv3(to_language, from_language); } template <> inline PlayerDispDataBB convert_player_disp_data( const PlayerDispDataDCPCV3& src, uint8_t to_language, uint8_t from_language) { return src.to_bb(to_language, from_language); } template <> inline PlayerDispDataBB convert_player_disp_data( const PlayerDispDataBB& src, uint8_t, uint8_t) { return src; } struct QuestFlagsForDifficulty { parray data; inline bool get(uint16_t flag_index) const { size_t byte_index = flag_index >> 3; uint8_t mask = 0x80 >> (flag_index & 7); return !!(this->data[byte_index] & mask); } inline void set(uint16_t flag_index) { size_t byte_index = flag_index >> 3; uint8_t mask = 0x80 >> (flag_index & 7); this->data[byte_index] |= mask; } inline void clear(uint16_t flag_index) { size_t byte_index = flag_index >> 3; uint8_t mask = 0x80 >> (flag_index & 7); this->data[byte_index] &= (~mask); } inline void update_all(bool set) { if (set) { this->data.clear(0xFF); } else { this->data.clear(0x00); } } } __attribute__((packed)); struct QuestFlags { parray data; inline bool get(uint8_t difficulty, uint16_t flag_index) const { return this->data[difficulty].get(flag_index); } inline void set(uint8_t difficulty, uint16_t flag_index) { this->data[difficulty].set(flag_index); } inline void clear(uint8_t difficulty, uint16_t flag_index) { this->data[difficulty].clear(flag_index); } inline void update_all(uint8_t difficulty, bool set) { this->data[difficulty].update_all(set); } inline void update_all(bool set) { for (size_t z = 0; z < 4; z++) { this->update_all(z, set); } } } __attribute__((packed)); struct BattleRules { enum class TechDiskMode : uint8_t { ALLOW = 0, FORBID_ALL = 1, LIMIT_LEVEL = 2, }; enum class WeaponAndArmorMode : uint8_t { ALLOW = 0, CLEAR_AND_ALLOW = 1, FORBID_ALL = 2, FORBID_RARES = 3, }; enum class MagMode : uint8_t { ALLOW = 0, FORBID_ALL = 1, }; enum class ToolMode : uint8_t { ALLOW = 0, CLEAR_AND_ALLOW = 1, FORBID_ALL = 2, }; enum class TrapMode : uint8_t { DEFAULT = 0, ALL_PLAYERS = 1, }; enum class MesetaMode : uint8_t { ALLOW = 0, FORBID_ALL = 1, CLEAR_AND_ALLOW = 2, }; enum class RespawnMode : uint8_t { ALLOW = 0, FORBID = 1, LIMIT_LIVES = 2, }; // Set by quest opcode F812, but values are remapped. // F812 00 => FORBID_ALL // F812 01 => ALLOW // F812 02 => LIMIT_LEVEL /* 00 */ TechDiskMode tech_disk_mode = TechDiskMode::ALLOW; // Set by quest opcode F813, but values are remapped. // F813 00 => FORBID_ALL // F813 01 => ALLOW // F813 02 => CLEAR_AND_ALLOW // F813 03 => FORBID_RARES /* 01 */ WeaponAndArmorMode weapon_and_armor_mode = WeaponAndArmorMode::ALLOW; // Set by quest opcode F814, but values are remapped. // F814 00 => FORBID_ALL // F814 01 => ALLOW /* 02 */ MagMode mag_mode = MagMode::ALLOW; // Set by quest opcode F815, but values are remapped. // F815 00 => FORBID_ALL // F815 01 => ALLOW // F815 02 => CLEAR_AND_ALLOW /* 03 */ ToolMode tool_mode = ToolMode::ALLOW; // Set by quest opcode F816. Values are not remapped. // F816 00 => DEFAULT // F816 01 => ALL_PLAYERS /* 04 */ TrapMode trap_mode = TrapMode::DEFAULT; // Set by quest opcode F817. Value appears to be unused in all PSO versions. /* 05 */ uint8_t unused_F817 = 0; // Set by quest opcode F818, but values are remapped. // F818 00 => 01 // F818 01 => 00 // F818 02 => 02 /* 06 */ RespawnMode respawn_mode = RespawnMode::ALLOW; // Set by quest opcode F819. /* 07 */ uint8_t replace_char = 0; // Set by quest opcode F81A, but value is inverted. /* 08 */ uint8_t drop_weapon = 0; // Set by quest opcode F81B. /* 09 */ uint8_t is_teams = 0; // Set by quest opcode F852. /* 0A */ uint8_t hide_target_reticle = 0; // Set by quest opcode F81E. Values are not remapped. // F81E 00 => ALLOW // F81E 01 => FORBID_ALL // F81E 02 => CLEAR_AND_ALLOW /* 0B */ MesetaMode meseta_mode = MesetaMode::ALLOW; // Set by quest opcode F81D. /* 0C */ uint8_t death_level_up = 0; // Set by quest opcode F851. The trap type is remapped: // F851 00 XX => set count to XX for trap type 00 // F851 01 XX => set count to XX for trap type 02 // F851 02 XX => set count to XX for trap type 03 // F851 03 XX => set count to XX for trap type 01 /* 0D */ parray trap_counts; // Set by quest opcode F85E. /* 11 */ uint8_t enable_sonar = 0; // Set by quest opcode F85F. /* 12 */ uint8_t sonar_count = 0; // Set by quest opcode F89E. /* 13 */ uint8_t forbid_scape_dolls = 0; // This value does not appear to be set by any quest opcode. /* 14 */ le_uint32_t unknown_a1 = 0; // Set by quest opcode F86F. /* 18 */ le_uint32_t lives = 0; // Set by quest opcode F870. /* 1C */ le_uint32_t max_tech_level = 0; // Set by quest opcode F871. /* 20 */ le_uint32_t char_level = 0; // Set by quest opcode F872. /* 24 */ le_uint32_t time_limit = 0; // Set by quest opcode F8A8. /* 28 */ le_uint16_t death_tech_level_up = 0; /* 2A */ parray unused; // Set by quest opcode F86B. /* 2C */ le_uint32_t box_drop_area = 0; /* 30 */ BattleRules() = default; explicit BattleRules(const JSON& json); JSON json() const; bool operator==(const BattleRules& other) const = default; bool operator!=(const BattleRules& other) const = default; } __attribute__((packed)); struct ChallengeTemplateDefinition { uint32_t level; std::vector items; struct TechLevel { uint8_t tech_num; uint8_t level; }; std::vector tech_levels; }; const ChallengeTemplateDefinition& get_challenge_template_definition(Version version, uint32_t class_flags, size_t index); struct SymbolChat { // Bits: ----------------------DMSSSCCCFF // S = sound, C = face color, F = face shape, D = capture, M = mute sound /* 00 */ le_uint32_t spec = 0; // Corner objects are specified in reading order ([0] is the top-left one). // Bits (each entry): ---VHCCCZZZZZZZZ // V = reverse vertical, H = reverse horizontal, C = color, Z = object // If Z is all 1 bits (0xFF), no corner object is rendered. /* 04 */ parray corner_objects; struct FacePart { uint8_t type = 0xFF; // FF = no part in this slot uint8_t x = 0; uint8_t y = 0; // Bits: ------VH (V = reverse vertical, H = reverse horizontal) uint8_t flags = 0; } __attribute__((packed)); /* 0C */ parray face_parts; /* 3C */ SymbolChat(); } __attribute__((packed));