diff --git a/src/Client.hh b/src/Client.hh index b64ebbfa..e9b902b0 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -250,7 +250,7 @@ public: std::unique_ptr pending_item_trade; std::unique_ptr pending_card_trade; uint32_t telepipe_lobby_id; - G_SetTelepipeState_6x68 telepipe_state; + TelepipeState telepipe_state; std::shared_ptr ep3_config; // Null for non-Ep3 int8_t bb_character_index; ItemData bb_identify_result; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index d5971f5e..eb10d3f0 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -3834,7 +3834,7 @@ struct S_SetShutdownCommand_BB_01EF { // are laid out similarly as above. These structs start with G_ to indicate that // they are (usually) bidirectional, and are (usually) generated by clients and // consumed by clients. Generally in newserv source, these commands are referred -// to as (for example) 6x02, etc., referencing the fact that they are almost +// to as (for example) 6x0B, etc., referencing the fact that they are almost // always sent via a command starting with the hex digit 6. // All game subcommands have the same header format, which is one of: @@ -3849,11 +3849,15 @@ struct S_SetShutdownCommand_BB_01EF { // Multiple subcommands may be sent in the same 6x command. It seems the client // never sends commands like this, but newserv generates commands containing // multiple subcommands in some situations (for example, the implementation of -// infinite HP does this). +// infinite HP does this). All versions of the client support this behavior. // If any subcommand or group thereof is longer than 0x400 bytes, the 6C or 6D -// commands must be used. The 60 and 62 commands exhibit undefined behavior if +// command must be used. The 60 and 62 commands exhibit undefined behavior if // this limit is exceeded. +// Many subcommands have different numbers on DC NTE and 11/2000, or simply +// don't exist on those versions. See the subcommand_definitions table in +// ReceiveSubcommands.cc for a full listing. + // Some subcommands are "protected" on V3 and later (not including GC NTE); // these commands are blocked by the client if they affect the local player. If // a V3 or later client receives a protected subcommand that would affect its @@ -3994,23 +3998,27 @@ struct G_SymbolChat_6x07 { // 6x08: Invalid subcommand // 6x09: Unknown -// header.entity_id is expected to be an enemy ID. +// header.entity_id is expected to be an enemy ID, but is also expected to be +// in the range [0x00, 0x80) since it writes to an array of 0x80 entries. This +// duality makes no sense because enemy IDs are greater than or equal to +// 0x1000, so any valid enemy ID would be far outside the array's range, and +// the write is not bounds-checked. For this reason, newserv unconditionally +// blocks this command. struct G_Unknown_6x09 { G_EntityIDHeader header; } __packed_ws__(G_Unknown_6x09, 4); // 6x0A: Update enemy state +// In Ultimate mode, the low 7 bits of game_flags are ignored, and 6x9C is used +// to update those instead. template struct G_UpdateEnemyStateT_6x0A { G_EntityIDHeader header; le_uint16_t enemy_index = 0; // [0, 0xB50) le_uint16_t total_damage = 0; - // Flags: - // 00000400 - should play hit animation - // 00000800 - is dead - typename std::conditional_t flags = 0; + typename std::conditional_t game_flags = 0; } __packed__; using G_UpdateEnemyState_GC_6x0A = G_UpdateEnemyStateT_6x0A; using G_UpdateEnemyState_DC_PC_XB_BB_6x0A = G_UpdateEnemyStateT_6x0A; @@ -4060,25 +4068,25 @@ struct G_RemoveStatusEffect_6x0D { le_uint32_t unused = 0; } __packed_ws__(G_RemoveStatusEffect_6x0D, 0x0C); -// 6x0E: Unknown (protected on V3/V4) +// 6x0E: Clear all negative status effects (protected on V3/V4) +// It seems that the client never sends this command. -struct G_Unknown_6x0E { +struct G_ClearNegativeStatusEffects_6x0E { G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x0E, 4); +} __packed_ws__(G_ClearNegativeStatusEffects_6x0E, 4); // 6x0F: Invalid subcommand -// 6x10: Unknown (not valid on Episode 3) +// 6x10: Dragon boss actions (not valid on Episode 3) +// 6x11: Dragon boss actions (not valid on Episode 3) +// It seems that the client never sends these commands. -struct G_Unknown_6x10_6x11_6x14 { +struct G_DragonBossActions_6x10_6x11 { G_EntityIDHeader header; le_uint16_t unknown_a2 = 0; le_uint16_t unknown_a3 = 0; le_uint32_t unknown_a4 = 0; -} __packed_ws__(G_Unknown_6x10_6x11_6x14, 0x0C); - -// 6x11: Unknown (not valid on Episode 3) -// Same format as 6x10 +} __packed_ws__(G_DragonBossActions_6x10_6x11, 0x0C); // 6x12: Dragon boss actions (not valid on Episode 3) @@ -4105,7 +4113,13 @@ struct G_DeRolLeBossActions_6x13 { } __packed_ws__(G_DeRolLeBossActions_6x13, 8); // 6x14: De Rol Le boss actions (not valid on Episode 3) -// Same format as 6x10 + +struct G_DeRolLeBossActions_6x14 { + G_EntityIDHeader header; + le_uint16_t unknown_a2 = 0; + le_uint16_t unknown_a3 = 0; + le_uint32_t unknown_a4 = 0; +} __packed_ws__(G_DeRolLeBossActions_6x14, 0x0C); // 6x15: Vol Opt boss actions (not valid on Episode 3) @@ -4188,7 +4202,7 @@ struct G_SetPosition_6x20 { G_ClientIDHeader header; le_int32_t floor = 0; VectorXYZF pos; - le_uint32_t unknown_a1 = 0; + le_uint32_t angle_y = 0; } __packed_ws__(G_SetPosition_6x20, 0x18); // 6x21: Inter-level warp (protected on V3/V4) @@ -4210,7 +4224,7 @@ struct G_SetPlayerVisibility_6x22_6x23 { struct G_TeleportPlayer_6x24 { G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; + le_uint32_t angle_y = 0; VectorXYZF pos; } __packed_ws__(G_TeleportPlayer_6x24, 0x14); @@ -4266,8 +4280,9 @@ struct G_DropItem_6x2A { VectorXYZF pos; } __packed_ws__(G_DropItem_6x2A, 0x18); -// 6x2B: Create item in inventory (e.g. via tekker or bank withdraw) (protected on V3/V4) +// 6x2B: Create item in inventory (tekker/bank) (protected on V3/V4) // On BB, the 6xBE command is used instead of 6x2B to create inventory items. +// If equip_item is nonzero, the item is equipped immediately. struct G_CreateInventoryItem_DC_6x2B { G_ClientIDHeader header; @@ -4276,19 +4291,22 @@ struct G_CreateInventoryItem_DC_6x2B { struct G_CreateInventoryItem_PC_V3_BB_6x2B : G_CreateInventoryItem_DC_6x2B { uint8_t unused1 = 0; - uint8_t unknown_a2 = 0; // Does something with equipped items, but logic isn't the same as 6x25 + uint8_t equip_item = 0; parray unused2 = 0; } __packed_ws__(G_CreateInventoryItem_PC_V3_BB_6x2B, 0x1C); // 6x2C: Talk to NPC (protected on V3/V4) +// This updates G_6x70_NPCTalkState in the TObjPlayer struct, but the format is +// not the same. The names here match the fields in G_6x70_NPCTalkState, hence +// the unusual numbering. struct G_TalkToNPC_6x2C { G_ClientIDHeader header; le_uint16_t unknown_a1 = 0; le_uint16_t unknown_a2 = 0; - le_float unknown_a3 = 0.0f; - le_float unknown_a4 = 0.0f; le_float unknown_a5 = 0.0f; + le_float unknown_a6 = 0.0f; + le_float unknown_a4 = 0.0f; } __packed_ws__(G_TalkToNPC_6x2C, 0x14); // 6x2D: Done talking to NPC (protected on V3/V4) @@ -4308,9 +4326,9 @@ struct G_SetOrClearPlayerFlags_6x2E { // 6x2F: Change player HP struct G_ChangePlayerHP_6x2F { - G_UnusedHeader header; - le_uint32_t type = 0; // 0 = set HP, 1 = add/subtract HP, 2 = add/sub fixed HP - le_uint16_t amount = 0; + G_ClientIDHeader header; + le_uint32_t type = 0; // 0 = set HP, 1 = add/subtract HP, 2 = reset to max HP + le_int16_t amount = 0; le_uint16_t client_id = 0; } __packed_ws__(G_ChangePlayerHP_6x2F, 0x0C); @@ -4355,12 +4373,19 @@ struct G_RevivePlayer_6x33 { } __packed_ws__(G_RevivePlayer_6x33, 8); // 6x34: Unknown -// This subcommand is completely ignored (at least, by PSO GC). +// This subcommand is ignored by all versions of PSO. // 6x35: Invalid subcommand // 6x36: Unknown (supported; game only) -// This subcommand is completely ignored (at least, by PSO GC). +// This subcommand is completely ignored on V3. +// TODO: It is not ignored on V2, and presumably earlier versions as well. +// Figure out what it does. It's something related to PBs. + +struct G_Unknown_6x36 { + // The parameter seems to be expected to be in the range [0, 12). + G_ParameterHeader header; +} __packed_ws__(G_Unknown_6x36, 4); // 6x37: Photon blast (protected on V3/V4) @@ -4379,22 +4404,26 @@ struct G_DonateToPhotonBlast_6x38 { } __packed_ws__(G_DonateToPhotonBlast_6x38, 8); // 6x39: Photon blast ready (protected on V3/V4) +// This is sent when a player's PB meter reaches 100. -struct G_PhotonBlastReady_6x38 { +struct G_SetPhotonBlastReadyFlag_6x39 { G_ClientIDHeader header; -} __packed_ws__(G_PhotonBlastReady_6x38, 4); +} __packed_ws__(G_SetPhotonBlastReadyFlag_6x39, 4); -// 6x3A: Unknown (supported; game only) (protected on V3/V4) +// 6x3A: Clear photon blast ready flag (protected on V3/V4) +// This is sent when a player's PB meter drops below 100. This can happen if +// a player donates to a PB instead of joining it. -struct G_Unknown_6x3A { +struct G_ClearPhotonBlastReadyFlag_6x3A { G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x3A, 4); +} __packed_ws__(G_ClearPhotonBlastReadyFlag_6x3A, 4); -// 6x3B: Unknown (supported; lobby & game) (protected on V3/V4) +// 6x3B: Clear temporary photon blast state flags (protected on V3/V4) +// This is usually sent when a player changes floors. -struct G_Unknown_6x3B { +struct G_ClearTemporaryPhotonBlastStateFlags_6x3B { G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x3B, 4); +} __packed_ws__(G_ClearTemporaryPhotonBlastStateFlags_6x3B, 4); // 6x3C: Unknown (DCv1 and earlier) // This command has a handler, but it does nothing, even on DC NTE. @@ -4405,7 +4434,7 @@ struct G_Unknown_6x3B { struct G_StopAtPosition_6x3E { G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; + le_uint16_t is_near_enemy = 0; // Sets game_flag 0x00100000 (v3) le_uint16_t angle = 0; le_int16_t floor = 0; le_int16_t room = 0; @@ -4416,7 +4445,7 @@ struct G_StopAtPosition_6x3E { struct G_SetPosition_6x3F { G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; + le_uint16_t is_near_enemy = 0; // Sets game_flag 0x00100000 (v3) le_uint16_t angle = 0; le_int16_t floor = 0; le_int16_t room = 0; @@ -4480,9 +4509,9 @@ struct G_CastTechnique_6x47 { uint8_t unused = 0; // Must not be negative // Note: The level here isn't the actual tech level that was cast, if the // actual level is > 15. In that case, a 6x8D is sent first, which contains - // the additional level which is added to this level at cast time. They - // probably did this for legacy reasons when dealing with v1/v2 - // compatibility, and never cleaned it up. + // the additional level which is added to this level at cast time. Sega + // probably did this as part of implementing v1/v2 compatibility and never + // cleaned it up. uint8_t level = 0; uint8_t target_count = 0; // Must be in [0, 10] // The client may send a shorter command if not all of these are used. @@ -4533,7 +4562,7 @@ struct G_HitByEnemy_6x4B_6x4C { struct G_PlayerDied_6x4D { G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; + le_uint32_t death_flags = 0; // Same as 6x70's death_flags field } __packed_ws__(G_PlayerDied_6x4D, 8); // 6x4E: Player is dead can be revived (protected on V3/V4) @@ -4553,7 +4582,7 @@ struct G_PlayerRevived_6x4F { struct G_SwitchInteraction_6x50 { G_ClientIDHeader header; - le_uint32_t unknown_a1 = 0; + le_uint32_t angle_y = 0; } __packed_ws__(G_SwitchInteraction_6x50, 8); // 6x51: Set player angle @@ -4597,8 +4626,8 @@ struct G_Unknown_6x54 { struct G_IntraMapWarp_6x55 { G_ClientIDHeader header; le_uint32_t unknown_a1 = 0; - VectorXYZF pos1; - VectorXYZF pos2; + VectorXYZF pos; + VectorXYZF unknown_a2; } __packed_ws__(G_IntraMapWarp_6x55, 0x20); // 6x56: Set player position and angle (protected on V3/V4) @@ -4656,7 +4685,7 @@ struct G_PickUpItemRequest_6x5A { struct G_DropStackedItem_DC_6x5D { G_ClientIDHeader header; le_uint16_t floor = 0; - le_uint16_t unknown_a2 = 0; // Corresponds to FloorItem::unknown_a2 + le_uint16_t room_id = 0; VectorXZF pos; ItemData item_data; } __packed_ws__(G_DropStackedItem_DC_6x5D, 0x24); @@ -4679,7 +4708,7 @@ struct FloorItem { /* 01 */ uint8_t source_type = 0; // 1 = enemy, 2 = box /* 02 */ le_uint16_t entity_index = 0; // < 0x0B50 if source_type == 1; otherwise < 0x0BA0 /* 04 */ VectorXZF pos; - /* 0C */ le_uint16_t unknown_a2 = 0; + /* 0C */ le_uint16_t room_id = 0; // The drop number is scoped to the floor and increments by 1 each time an // item is dropped. The last item dropped in each floor has drop_number equal // to total_items_dropped_per_floor[floor - 1] - 1. @@ -4763,13 +4792,7 @@ struct G_TriggerSetEvent_6x67 { struct G_SetTelepipeState_6x68 { G_UnusedHeader header; - le_uint16_t client_id2 = 0; - le_uint16_t floor = 0; - le_uint16_t unknown_b1 = 0; - uint8_t unknown_b2 = 0; - uint8_t unknown_b3 = 0; - VectorXYZF pos; - le_uint32_t unknown_a3 = 0; + TelepipeState state; } __packed_ws__(G_SetTelepipeState_6x68, 0x1C); // 6x69: NPC control @@ -4915,7 +4938,7 @@ struct G_SetQuestFlags_V2_V3_6x6F { struct G_SetQuestFlags_BB_6x6F { G_UnusedHeader header; QuestFlags quest_flags; - // If use_apply_mask is 1, only the flags set in bb_quest_flag_apply_mask + // If use_apply_mask is 1, only the flags set in BB_QUEST_FLAG_APPLY_MASK // (in PlayerSubordinates.cc) are overwritten on the receiving client's end. // The client always sends this with use_apply_mask = 1. le_uint32_t use_apply_mask = 1; @@ -4927,33 +4950,11 @@ struct G_SetQuestFlags_BB_6x6F { // structure also includes transient state (e.g. current HP). struct G_6x70_Sub_Telepipe { - /* 00 */ le_uint16_t owner_client_id = 0xFFFF; - /* 02 */ le_uint16_t floor = 0; - /* 04 */ le_uint32_t unknown_a1 = 0; - /* 08 */ VectorXYZF pos; - /* 14 */ le_uint32_t unknown_a3 = 0; + /* 00 */ TelepipeState state; /* 18 */ le_uint32_t unknown_a4 = 0x0000FFFF; /* 1C */ } __packed_ws__(G_6x70_Sub_Telepipe, 0x1C); -struct G_6x70_Sub_UnknownA1 { - // This is used in all versions of this command except DCNTE and 11/2000. - /* 00 */ le_uint16_t unknown_a1 = 0; - /* 02 */ le_uint16_t unknown_a2 = 0; - /* 04 */ le_uint32_t unknown_a3 = 0; - /* 08 */ le_float unknown_a4 = 0.0f; - /* 0C */ le_uint32_t unknown_a5 = 0; - /* 10 */ le_uint32_t unknown_a6 = 0; - /* 14 */ -} __packed_ws__(G_6x70_Sub_UnknownA1, 0x14); - -struct G_6x70_StatusEffectState { - /* 00 */ le_uint32_t effect_type = 0; - /* 04 */ le_float multiplier = 0.0f; - /* 08 */ le_uint32_t remaining_frames = 0; - /* 0C */ -} __packed_ws__(G_6x70_StatusEffectState, 0x0C); - struct G_6x70_Base_DCNTE { /* 0000 */ le_uint16_t client_id = 0; /* 0002 */ le_uint16_t room_id = 0; @@ -4974,10 +4975,10 @@ struct G_SyncPlayerDispAndInventory_DCNTE_6x70 { /* 0030 */ le_uint32_t unknown_a5 = 0; /* 0034 */ le_uint32_t unknown_a6 = 0; /* 0038 */ G_6x70_Sub_Telepipe telepipe; - /* 0054 */ le_uint32_t unknown_a8 = 0; - /* 0058 */ parray unknown_a9; + /* 0054 */ le_uint32_t death_flags = 0; + /* 0058 */ NPCTalkState_DCProtos npc_talk_state; /* 0068 */ le_uint32_t area = 0; - /* 006C */ le_uint32_t flags2 = 0; + /* 006C */ le_uint32_t game_flags = 0; /* 0070 */ PlayerVisualConfig visual; /* 00C0 */ PlayerStats stats; /* 00E4 */ le_uint32_t num_items = 0; @@ -4993,10 +4994,10 @@ struct G_SyncPlayerDispAndInventory_DC112000_6x70 { /* 0032 */ le_uint16_t bonus_tp_from_materials = 0; /* 0034 */ parray unknown_a5; /* 0044 */ G_6x70_Sub_Telepipe telepipe; - /* 0060 */ le_uint32_t unknown_a8 = 0; - /* 0064 */ parray unknown_a9; + /* 0060 */ le_uint32_t death_flags = 0; + /* 0064 */ NPCTalkState_DCProtos npc_talk_state; /* 0074 */ le_uint32_t area = 0; - /* 0078 */ le_uint32_t flags2 = 0; + /* 0078 */ le_uint32_t game_flags = 0; /* 007C */ PlayerVisualConfig visual; /* 00CC */ PlayerStats stats; /* 00F0 */ le_uint32_t num_items = 0; @@ -5008,21 +5009,21 @@ struct G_6x70_Base_V1 { /* 0000 */ G_6x70_Base_DCNTE base; /* 0024 */ le_uint16_t bonus_hp_from_materials = 0; /* 0026 */ le_uint16_t bonus_tp_from_materials = 0; - /* 0028 */ G_6x70_StatusEffectState permanent_status_effect; - /* 0034 */ G_6x70_StatusEffectState temporary_status_effect; - /* 0040 */ G_6x70_StatusEffectState attack_status_effect; - /* 004C */ G_6x70_StatusEffectState defense_status_effect; - /* 0058 */ G_6x70_StatusEffectState unused_status_effect; + /* 0028 */ StatusEffectState permanent_status_effect; + /* 0034 */ StatusEffectState temporary_status_effect; + /* 0040 */ StatusEffectState attack_status_effect; + /* 004C */ StatusEffectState defense_status_effect; + /* 0058 */ StatusEffectState unused_status_effect; /* 0064 */ le_uint32_t language = 0; /* 0068 */ le_uint32_t player_tag = 0; /* 006C */ le_uint32_t guild_card_number = 0; /* 0070 */ le_uint32_t unknown_a6 = 0; // Probably battle-related (assigned together with battle_team_number) /* 0074 */ le_uint32_t battle_team_number = 0; /* 0078 */ G_6x70_Sub_Telepipe telepipe; - /* 0094 */ le_uint32_t unknown_a8 = 0; - /* 0098 */ G_6x70_Sub_UnknownA1 unknown_a9; + /* 0094 */ le_uint32_t death_flags = 0; // Only a few bits are used. 4 = player is dead + /* 0098 */ NPCTalkState npc_talk_state; /* 00AC */ le_uint32_t area = 0; - /* 00B0 */ le_uint32_t flags2 = 0; + /* 00B0 */ le_uint32_t game_flags = 0; /* 00B4 */ parray technique_levels_v1 = 0xFF; // Last byte is uninitialized /* 00C8 */ PlayerVisualConfig visual; /* 0118 */ @@ -5166,23 +5167,22 @@ struct G_GogoBall_6x79 { G_UnusedHeader header; le_uint32_t unknown_a1 = 0; le_uint32_t unknown_a2 = 0; - le_float unknown_a3 = 0.0f; - le_float unknown_a4 = 0.0f; + VectorXZF ball_pos; uint8_t unknown_a5 = 0; parray unused; } __packed_ws__(G_GogoBall_6x79, 0x18); -// 6x7A: Unknown (protected on V3/V4) +// 6x7A: Enable Stealth Suit effect (protected on V3/V4) -struct G_Unknown_6x7A { +struct G_EnableStealthSuitEffect_6x7A { G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x7A, 4); +} __packed_ws__(G_EnableStealthSuitEffect_6x7A, 4); -// 6x7B: Unknown (protected on V3/V4) +// 6x7B: Disable Stealth Suit effect (protected on V3/V4) -struct G_Unknown_6x7B { +struct G_DisableStealthSuitEffect_6x7B { G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x7B, 4); +} __packed_ws__(G_DisableStealthSuitEffect_6x7B, 4); // 6x7C: Set Challenge records (not valid on Episode 3) @@ -5225,7 +5225,7 @@ struct G_SetBattleModeData_6x7D { } __packed_ws__(G_SetBattleModeData_6x7D, 0x18); // 6x7E: Unknown (not valid on Episode 3) -// This subcommand is completely ignored (at least, by PSO GC). +// This subcommand is valid on DC 08/2001 (v2) and later, but it does nothing. // 6x7F: Battle scores and places (not valid on Episode 3) @@ -5292,26 +5292,29 @@ struct G_VolOptBossActions_6x84 { le_uint16_t unused = 0; } __packed_ws__(G_VolOptBossActions_6x84, 0x10); -// 6x85: Unknown (supported; game only; not valid on Episode 3) +// 6x85: Vol Opt phase 2 boss actions (not valid on Episode 3) -struct G_Unknown_6x85 { +struct G_VolOptPhase2BossActions_6x85 { G_UnusedHeader header; le_uint16_t unknown_a1 = 0; // Command is ignored unless this is 0 parray unknown_a2; // Only the first 3 appear to be used -} __packed_ws__(G_Unknown_6x85, 0x14); +} __packed_ws__(G_VolOptPhase2BossActions_6x85, 0x14); // 6x86: Hit destructible object (not valid on Episode 3) +// This command only has an effect for TOVS2Wall01 objects. struct G_HitDestructibleObject_6x86 : G_UpdateObjectState_6x0B { - le_uint16_t unknown_a3 = 0; - le_uint16_t unknown_a4 = 0; + le_uint16_t is_destroyed = 0; + le_uint16_t remaining_hits = 0; } __packed_ws__(G_HitDestructibleObject_6x86, 0x10); // 6x87: Shrink player (protected on V3/V4) +// This command is sent when the F838 quest opcode is executed. struct G_ShrinkPlayer_6x87 { G_ClientIDHeader header; - le_float unknown_a1 = 0.0f; + // This field contains the value set by the preceding F8A7 quest opcode. + le_float size = 1.0f; } __packed_ws__(G_ShrinkPlayer_6x87, 8); // 6x88: Restore shrunken player (protected on V3/V4) @@ -5381,24 +5384,24 @@ struct G_SetPlayerBattleTeam_6x90 { le_uint32_t team_number = 0; // 0 or 1 } __packed_ws__(G_SetPlayerBattleTeam_6x90, 8); -// 6x91: Unknown (supported; game only) -// TODO: Deals with TOAttackableCol objects. Figure out exactly what it does. +// 6x91: Hit destructible object +// This command only has an effect for TOAttackableCol objects. struct G_UpdateAttackableColState_6x91 : G_UpdateObjectState_6x0B { - le_uint16_t unknown_a3 = 0; - le_uint16_t unknown_a4 = 0; + le_uint16_t is_destroyed = 0; + le_uint16_t remaining_hits = 0; le_uint16_t switch_flag_num = 0; - uint8_t should_set = 0; // The switch flag is only set if this is equal to 1; otherwise it's cleared + uint8_t should_set = 0; // The switch flag is only set if this is equal to 1 uint8_t floor = 0; } __packed_ws__(G_UpdateAttackableColState_6x91, 0x14); // 6x92: Unknown (not valid on Episode 3) -// This does something with the TObjOnlineEndingHexMove object. TODO: Figure -// out exactly what. +// TODO: It looks like this sets the heights of players in the online victory +// screen? Figure out if this is actually what it does. struct G_Unknown_6x92 { G_UnusedHeader header; - le_uint32_t unknown_a1 = 0; + le_uint32_t client_id = 0; le_float unknown_a2 = 0.0f; } __packed_ws__(G_Unknown_6x92, 0x0C); @@ -5480,14 +5483,16 @@ struct G_LevelUpAllTechniques_6x9B { parray unused; } __packed_ws__(G_LevelUpAllTechniques_6x9B, 8); -// 6x9C: Unknown (supported; game only; not valid on Episode 3) -// This command only has an effect in Ultimate mode. -// TODO: Figure out what this does. +// 6x9C: Set enemy low game flags (not valid on Episode 3) +// This command only has an effect in Ultimate mode. It sets the low 7 bits of +// game_flags (those that match 0x0000003F). -struct G_Unknown_6x9C { +struct G_SetEnemyLowGameFlagsUltimate_6x9C { G_EntityIDHeader header; - le_uint32_t unknown_a1 = 0; -} __packed_ws__(G_Unknown_6x9C, 8); + // A virtual function is called on the enemy if low_game_flags is equal to + // any of 0x02, 0x04, 0x10, or 0x20. + le_uint32_t low_game_flags = 0; +} __packed_ws__(G_SetEnemyLowGameFlagsUltimate_6x9C, 8); // 6x9D: Set dead flag (Challenge mode; not valid on Episode 3) // This command causes the specified client to appear in the dead players list @@ -5641,13 +5646,12 @@ struct G_Unknown_GCNTE_6xAB { struct G_CreateLobbyChair_6xAB { G_ClientIDHeader header; - le_uint16_t unknown_a1 = 0; - le_uint16_t unknown_a2 = 0; + le_uint16_t unused = 0; + le_uint16_t flags = 0; // Only the low two bits are used } __packed_ws__(G_CreateLobbyChair_6xAB, 8); -// 6xAC: Unknown (not valid on pre-V3) (protected on V3/V4) -// This command appears to be different on GC NTE than on any other version. It -// also seems that no version (except perhaps GC NTE) ever sends this command. +// 6xAC: Unknown (not valid on pre-V3) +// This command appears to be different on GC NTE than on any other version. struct G_Unknown_GCNTE_6xAC { G_EntityIDHeader header; @@ -5656,11 +5660,17 @@ struct G_Unknown_GCNTE_6xAC { le_uint32_t unknown_a3 = 0; } __packed_ws__(G_Unknown_GCNTE_6xAC, 0x0C); -struct G_Unknown_6xAC { +// 6xAC: Delete multiple inventory items (protected on V3/V4) +// This command appears to delete multiple items from a player's inventory. +// It's not clear when or why this would be used; the client never sends it. +// The disassembly also is somewhat confusing, and it's not clear that it even +// works properly. + +struct G_DeleteMultipleInventoryItems_6xAC { G_ClientIDHeader header; le_uint32_t num_items = 0; parray item_ids; -} __packed_ws__(G_Unknown_6xAC, 0x80); +} __packed_ws__(G_DeleteMultipleInventoryItems_6xAC, 0x80); // 6xAD: Olga Flow subordinate boss actions (not valid on pre-V3, Episode 3, or // GC Trial Edition) @@ -5704,14 +5714,15 @@ struct G_MoveLobbyChair_6xB0 { // 6xB2: Play sound from player (not valid on pre-V3 or GC Trial Edition) // This command is sent when a snapshot is taken on PSO GC, but it can be used -// to play any sound, centered on the local player. If localize is FFFF, then -// the sound is not centered on the local player and is just played globally. +// to play any sound. The sound is centered on the local player (even if the +// local player does not match client_id), or if client_id is FFFF, it is not +// localized at all and it just played globally. struct G_PlaySoundFromPlayer_6xB2 { G_UnusedHeader header; uint8_t floor = 0; uint8_t unused = 0; - le_uint16_t localize = 0; + le_uint16_t client_id = 0; le_uint32_t sound_id = 0; // 0x00051720 = camera shutter sound } __packed_ws__(G_PlaySoundFromPlayer_6xB2, 0x0C); diff --git a/src/Map.cc b/src/Map.cc index ab37a62e..6cb66829 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -1230,21 +1230,22 @@ MapFile::MapFile(std::shared_ptr data) { throw runtime_error("section floor number too large"); } + size_t section_offset = r.where(); switch (header.type()) { case SectionHeader::Type::OBJECT_SETS: - this->set_object_sets_for_floor(header.floor, r.getv(header.data_size), header.data_size); + this->set_object_sets_for_floor(header.floor, section_offset, r.getv(header.data_size), header.data_size); break; case SectionHeader::Type::ENEMY_SETS: - this->set_enemy_sets_for_floor(header.floor, r.getv(header.data_size), header.data_size); + this->set_enemy_sets_for_floor(header.floor, section_offset, r.getv(header.data_size), header.data_size); break; case SectionHeader::Type::EVENTS: - this->set_events_for_floor(header.floor, r.getv(header.data_size), header.data_size, true); + this->set_events_for_floor(header.floor, section_offset, r.getv(header.data_size), header.data_size, true); break; case SectionHeader::Type::RANDOM_ENEMY_LOCATIONS: - this->set_random_enemy_locations_for_floor(header.floor, r.getv(header.data_size), header.data_size); + this->set_random_enemy_locations_for_floor(header.floor, section_offset, r.getv(header.data_size), header.data_size); break; case SectionHeader::Type::RANDOM_ENEMY_DEFINITIONS: - this->set_random_enemy_definitions_for_floor(header.floor, r.getv(header.data_size), header.data_size); + this->set_random_enemy_definitions_for_floor(header.floor, section_offset, r.getv(header.data_size), header.data_size); break; default: throw runtime_error("invalid section type"); @@ -1260,15 +1261,15 @@ MapFile::MapFile( std::shared_ptr events_data) { if (objects_data) { this->link_data(objects_data); - this->set_object_sets_for_floor(floor, objects_data->data(), objects_data->size()); + this->set_object_sets_for_floor(floor, 0, objects_data->data(), objects_data->size()); } if (enemies_data) { this->link_data(enemies_data); - this->set_enemy_sets_for_floor(floor, enemies_data->data(), enemies_data->size()); + this->set_enemy_sets_for_floor(floor, 0, enemies_data->data(), enemies_data->size()); } if (events_data) { this->link_data(events_data); - this->set_events_for_floor(floor, events_data->data(), events_data->size(), false); + this->set_events_for_floor(floor, 0, events_data->data(), events_data->size(), false); } this->compute_floor_start_indexes(); } @@ -1282,7 +1283,7 @@ void MapFile::link_data(std::shared_ptr data) { } } -void MapFile::set_object_sets_for_floor(uint8_t floor, const void* data, size_t size) { +void MapFile::set_object_sets_for_floor(uint8_t floor, size_t file_offset, const void* data, size_t size) { auto& floor_sections = this->sections_for_floor.at(floor); if (floor_sections.object_sets) { throw runtime_error("multiple object sets sections for same floor"); @@ -1292,9 +1293,11 @@ void MapFile::set_object_sets_for_floor(uint8_t floor, const void* data, size_t } floor_sections.object_sets = reinterpret_cast(data); floor_sections.object_set_count = size / sizeof(ObjectSetEntry); + floor_sections.object_sets_file_offset = file_offset; + floor_sections.object_sets_file_size = size; } -void MapFile::set_enemy_sets_for_floor(uint8_t floor, const void* data, size_t size) { +void MapFile::set_enemy_sets_for_floor(uint8_t floor, size_t file_offset, const void* data, size_t size) { auto& floor_sections = this->sections_for_floor.at(floor); if (floor_sections.enemy_sets) { throw runtime_error("multiple enemy sets sections for same floor"); @@ -1307,9 +1310,11 @@ void MapFile::set_enemy_sets_for_floor(uint8_t floor, const void* data, size_t s } floor_sections.enemy_sets = reinterpret_cast(data); floor_sections.enemy_set_count = size / sizeof(EnemySetEntry); + floor_sections.enemy_sets_file_offset = file_offset; + floor_sections.enemy_sets_file_size = size; } -void MapFile::set_events_for_floor(uint8_t floor, const void* data, size_t size, bool allow_evt2) { +void MapFile::set_events_for_floor(uint8_t floor, size_t file_offset, const void* data, size_t size, bool allow_evt2) { auto& floor_sections = this->sections_for_floor.at(floor); if (floor_sections.events_data || floor_sections.events1 || floor_sections.events2 || floor_sections.event_action_stream) { throw runtime_error("multiple events sections for same floor"); @@ -1317,6 +1322,9 @@ void MapFile::set_events_for_floor(uint8_t floor, const void* data, size_t size, floor_sections.events_data = data; floor_sections.events_data_size = size; + floor_sections.events_file_offset = file_offset; + floor_sections.events_file_size = size; + phosg::StringReader r(data, size); const auto& events_header = r.get(); floor_sections.event_count = events_header.entry_count; @@ -1339,7 +1347,7 @@ void MapFile::set_events_for_floor(uint8_t floor, const void* data, size_t size, events_header.action_stream_offset, floor_sections.event_action_stream_bytes); } -void MapFile::set_random_enemy_locations_for_floor(uint8_t floor, const void* data, size_t size) { +void MapFile::set_random_enemy_locations_for_floor(uint8_t floor, size_t file_offset, const void* data, size_t size) { auto& floor_sections = this->sections_for_floor.at(floor); if (floor_sections.random_enemy_locations_data) { throw runtime_error("multiple random enemy locations sections for same floor"); @@ -1347,10 +1355,12 @@ void MapFile::set_random_enemy_locations_for_floor(uint8_t floor, const void* da floor_sections.random_enemy_locations_data = data; floor_sections.random_enemy_locations_data_size = size; + floor_sections.random_enemy_locations_file_offset = file_offset; + floor_sections.random_enemy_locations_file_size = size; this->has_any_random_sections = true; } -void MapFile::set_random_enemy_definitions_for_floor(uint8_t floor, const void* data, size_t size) { +void MapFile::set_random_enemy_definitions_for_floor(uint8_t floor, size_t file_offset, const void* data, size_t size) { auto& floor_sections = this->sections_for_floor.at(floor); if (floor_sections.random_enemy_definitions_data) { throw runtime_error("multiple random enemy locations sections for same floor"); @@ -1358,6 +1368,8 @@ void MapFile::set_random_enemy_definitions_for_floor(uint8_t floor, const void* floor_sections.random_enemy_definitions_data = data; floor_sections.random_enemy_definitions_data_size = size; + floor_sections.random_enemy_definitions_file_offset = file_offset; + floor_sections.random_enemy_definitions_file_size = size; this->has_any_random_sections = true; } @@ -1383,15 +1395,15 @@ std::shared_ptr MapFile::materialize_random_sections(uint32_t random_se const auto& this_sf = this->sections_for_floor[floor]; if (this_sf.object_sets) { - new_map->set_object_sets_for_floor(floor, this_sf.object_sets, this_sf.object_set_count * sizeof(ObjectSetEntry)); + new_map->set_object_sets_for_floor(floor, 0, this_sf.object_sets, this_sf.object_set_count * sizeof(ObjectSetEntry)); } if (this_sf.enemy_sets) { - new_map->set_enemy_sets_for_floor(floor, this_sf.enemy_sets, this_sf.enemy_set_count * sizeof(EnemySetEntry)); + new_map->set_enemy_sets_for_floor(floor, 0, this_sf.enemy_sets, this_sf.enemy_set_count * sizeof(EnemySetEntry)); } if (this_sf.events1) { - new_map->set_events_for_floor(floor, this_sf.events_data, this_sf.events_data_size, false); + new_map->set_events_for_floor(floor, 0, this_sf.events_data, this_sf.events_data_size, false); } else if (this_sf.events2) { if (!this_sf.random_enemy_locations_data || !this_sf.random_enemy_definitions_data) { throw runtime_error("cannot materialize random enemies; evt2 section present but one or both random data sections are missing"); @@ -1547,11 +1559,11 @@ std::shared_ptr MapFile::materialize_random_sections(uint32_t random_se auto enemy_sets_sec_data = make_shared(std::move(enemy_sets_w.str())); new_map->link_data(enemy_sets_sec_data); - new_map->set_enemy_sets_for_floor(floor, enemy_sets_sec_data->data(), enemy_sets_sec_data->size()); + new_map->set_enemy_sets_for_floor(floor, 0, enemy_sets_sec_data->data(), enemy_sets_sec_data->size()); auto events1_sec_data = make_shared(std::move(events1_sec_w.str())); new_map->link_data(events1_sec_data); - new_map->set_events_for_floor(floor, events1_sec_data->data(), events1_sec_data->size(), false); + new_map->set_events_for_floor(floor, 0, events1_sec_data->data(), events1_sec_data->size(), false); } } @@ -1670,44 +1682,60 @@ string MapFile::disassemble() const { phosg::StringReader as_r(sf.event_action_stream, sf.event_action_stream_bytes); if (sf.object_sets) { - ret.emplace_back(phosg::string_printf(".object_sets %hhu", floor)); + ret.emplace_back(phosg::string_printf(".object_sets %hhu /* 0x%zX in file; 0x%zX bytes */", + floor, sf.object_sets_file_offset, sf.object_sets_file_size)); for (size_t z = 0; z < sf.object_set_count; z++) { size_t k_id = z + sf.first_object_set_index; ret.emplace_back(phosg::string_printf("/* K-%03zX */ ", k_id) + sf.object_sets[z].str()); } } if (sf.enemy_sets) { - ret.emplace_back(phosg::string_printf(".enemy_sets %hhu", floor)); + ret.emplace_back(phosg::string_printf(".enemy_sets %hhu /* 0x%zX in file; 0x%zX bytes */", + floor, sf.enemy_sets_file_offset, sf.enemy_sets_file_size)); for (size_t z = 0; z < sf.enemy_set_count; z++) { size_t s_id = z + sf.first_enemy_set_index; ret.emplace_back(phosg::string_printf("/* S-%03zX */ ", s_id) + sf.enemy_sets[z].str()); } } if (sf.events1) { - ret.emplace_back(phosg::string_printf(".events %hhu", floor)); + ret.emplace_back(phosg::string_printf(".events %hhu /* 0x%zX in file; 0x%zX bytes; 0x%zX bytes in action stream */", + floor, sf.events_file_offset, sf.events_file_size, sf.event_action_stream_bytes)); for (size_t z = 0; z < sf.event_count; z++) { - const auto& ev = sf.events2[z]; + const auto& ev = sf.events1[z]; size_t w_id = z + sf.first_event_set_index; ret.emplace_back(phosg::string_printf("/* W-%03zX */ ", w_id) + ev.str()); + if (ev.action_stream_offset >= sf.event_action_stream_bytes) { + ret.emplace_back(phosg::string_printf( + " // WARNING: Event action stream offset (0x%" PRIX32 ") is outside of this section", + ev.action_stream_offset.load())); + } size_t as_size = as_r.size() - ev.action_stream_offset; ret.emplace_back(this->disassemble_action_stream(as_r.pgetv(ev.action_stream_offset, as_size), as_size)); } } if (sf.events2) { - ret.emplace_back(phosg::string_printf(".random_events %hhu", floor)); + ret.emplace_back(phosg::string_printf(".random_events %hhu /* 0x%zX in file; 0x%zX bytes; 0x%zX bytes in action stream */", + floor, sf.events_file_offset, sf.events_file_size, sf.event_action_stream_bytes)); for (size_t z = 0; z < sf.event_count; z++) { const auto& ev = sf.events2[z]; ret.emplace_back(phosg::string_printf("/* index %zu */", z) + ev.str()); + if (ev.action_stream_offset >= sf.event_action_stream_bytes) { + ret.emplace_back(phosg::string_printf( + " // WARNING: Event action stream offset (0x%" PRIX32 ") is outside of this section", + ev.action_stream_offset.load())); + } size_t as_size = as_r.size() - ev.action_stream_offset; ret.emplace_back(this->disassemble_action_stream(as_r.pgetv(ev.action_stream_offset, as_size), as_size)); } } if (sf.random_enemy_locations_data) { - ret.emplace_back(phosg::string_printf(".random_enemy_locations %hhu", floor)); + ret.emplace_back(phosg::string_printf(".random_enemy_locations %hhu /* 0x%zX in file; 0x%zX bytes */", + floor, sf.random_enemy_locations_file_offset, sf.random_enemy_locations_file_size)); ret.emplace_back(phosg::format_data(sf.random_enemy_locations_data, sf.random_enemy_locations_data_size)); } if (sf.random_enemy_definitions_data) { - ret.emplace_back(phosg::string_printf(".random_enemy_definitions %hhu", floor)); + ret.emplace_back(phosg::string_printf(".random_enemy_definitions %hhu /* 0x%zX in file; 0x%zX bytes */", + floor, sf.random_enemy_definitions_file_offset, sf.random_enemy_definitions_file_size)); ret.emplace_back(phosg::format_data(sf.random_enemy_definitions_data, sf.random_enemy_definitions_data_size)); } } @@ -2415,7 +2443,10 @@ void SuperMap::link_event_version( const void* map_file_action_stream, size_t map_file_action_stream_size) { if (entry->action_stream_offset >= map_file_action_stream_size) { - throw runtime_error("event entry action stream offset is beyond end of action stream"); + string s = entry->str(); + throw runtime_error(phosg::string_printf( + "action stream offset 0x%" PRIX32 " is beyond end of action stream (0x%zX) for event %s", + entry->action_stream_offset.load(), map_file_action_stream_size, s.c_str())); } const void* ev_action_stream_start = reinterpret_cast(map_file_action_stream) + entry->action_stream_offset; @@ -3257,6 +3288,40 @@ uint32_t MapState::RareEnemyRates::for_enemy_type(EnemyType type) const { const shared_ptr MapState::NO_RARE_ENEMIES = make_shared(0, 0); const shared_ptr MapState::DEFAULT_RARE_ENEMIES = make_shared(0x0083126E, 0x1999999A); +uint32_t MapState::EnemyState::convert_game_flags(uint32_t game_flags, bool to_v3) { + // The format of game_flags was changed significantly between v2 and v3, and + // not accounting for this results in odd effects like other characters not + // appearing when joining a game. Unfortunately, some bits were deleted on v3 + // and other bits were added, so it doesn't suffice to simply store the most + // complete format of this field - we have to be able to convert between the + // two. + + // Bits on v2: ?IHCBAzy xwvutsrq ponmlkji hgfedcba + // Bits on v3: ?IHGFEDC BAzyxwvu srqponkj hgfedcba + // The bits ilmt were removed in v3 and the bits to their left were shifted + // right. The bits DEFG were added in v3 and do not exist on v2. + // Known meanings for these bits: + // o = is dead + // n = should play hit animation + // y = is near enemy + // H = is enemy? + // I = is object? (some entities have both H and I set though) + // It could be that the flags 0x70000000 are actually a 3-bit integer rather + // than individual flags. TODO: Investigate this. + + if (to_v3) { + return (game_flags & 0xE00000FF) | + ((game_flags & 0x00000600) >> 1) | + ((game_flags & 0x0007E000) >> 3) | + ((game_flags & 0x1FF00000) >> 4); + } else { + return (game_flags & 0xE00000FF) | + ((game_flags << 1) & 0x00000600) | + ((game_flags << 3) & 0x0007E000) | + ((game_flags << 4) & 0x1FF00000); + } +} + MapState::EntityIterator::EntityIterator(MapState* map_state, Version version, bool at_end) : map_state(map_state), version(version), @@ -3755,6 +3820,7 @@ void MapState::import_object_states_from_sync( void MapState::import_enemy_states_from_sync(Version from_version, const SyncEnemyStateEntry* entries, size_t entry_count) { this->log.info("Importing enemy state from sync command"); size_t enemy_index = 0; + bool is_v3 = !is_v1_or_v2(from_version); for (const auto& fc : this->floor_config_entries) { if (!fc.super_map) { continue; @@ -3775,10 +3841,15 @@ void MapState::import_enemy_states_from_sync(Version from_version, const SyncEne if (ene_st->super_ene != ene) { throw logic_error("super enemy link is incorrect"); } - if (ene_st->game_flags != entry.flags) { - this->log.warning("(%04zX => E-%03zX) Flags from client (%08" PRIX32 ") do not match game flags from map (%08" PRIX32 ")", - enemy_index, ene_st->e_id, entry.flags.load(), ene_st->game_flags); - ene_st->game_flags = entry.flags; + if (ene_st->get_game_flags(is_v3) != entry.flags) { + this->log.warning("(%04zX => E-%03zX) Flags from client (%08" PRIX32 "(%s)) do not match game flags from map (%08" PRIX32 "(%s))", + enemy_index, + ene_st->e_id, + entry.flags.load(), + is_v3 ? "v3" : "v2", + ene_st->game_flags, + (ene_st->server_flags & MapState::EnemyState::Flag::GAME_FLAGS_IS_V3) ? "v3" : "v2"); + ene_st->set_game_flags(entry.flags, !is_v1_or_v2(from_version)); } if (ene_st->total_damage != entry.total_damage) { this->log.warning("(%04zX => E-%03zX) Total damage from client (%hu) does not match total damage from map (%hu)", @@ -4107,8 +4178,14 @@ void MapState::print(FILE* stream) const { } } string ene_str = ene_st->super_ene->str(); - fprintf(stream, " %s total_damage=%04hX rare_flags=%04hX game_flags=%08" PRIX32 " set_flags=%04hX server_flags=%02hhX\n", - ene_str.c_str(), ene_st->total_damage, ene_st->rare_flags, ene_st->game_flags, ene_st->set_flags, ene_st->server_flags); + fprintf(stream, " %s total_damage=%04hX rare_flags=%04hX game_flags=%08" PRIX32 "(%s) set_flags=%04hX server_flags=%04hX\n", + ene_str.c_str(), + ene_st->total_damage, + ene_st->rare_flags, + ene_st->game_flags, + (ene_st->server_flags & MapState::EnemyState::Flag::GAME_FLAGS_IS_V3) ? "v3" : "v2", + ene_st->set_flags, + ene_st->server_flags); } if (this->bb_rare_enemy_indexes.empty()) { diff --git a/src/Map.hh b/src/Map.hh index 40323140..637e3086 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -337,14 +337,20 @@ public: }; struct FloorSections { + size_t object_sets_file_offset = 0; + size_t object_sets_file_size = 0; const ObjectSetEntry* object_sets = nullptr; size_t object_set_count = 0; size_t first_object_set_index = 0; + size_t enemy_sets_file_offset = 0; + size_t enemy_sets_file_size = 0; const EnemySetEntry* enemy_sets = nullptr; size_t enemy_set_count = 0; size_t first_enemy_set_index = 0; + size_t events_file_offset = 0; + size_t events_file_size = 0; const void* events_data = nullptr; size_t events_data_size = 0; const Event1Entry* events1 = nullptr; @@ -354,9 +360,13 @@ public: size_t event_action_stream_bytes = 0; size_t first_event_set_index = 0; + size_t random_enemy_locations_file_offset = 0; + size_t random_enemy_locations_file_size = 0; const void* random_enemy_locations_data = nullptr; size_t random_enemy_locations_data_size = 0; + size_t random_enemy_definitions_file_offset = 0; + size_t random_enemy_definitions_file_size = 0; const void* random_enemy_definitions_data = nullptr; size_t random_enemy_definitions_data_size = 0; }; @@ -412,11 +422,11 @@ public: protected: void link_data(std::shared_ptr data); - void set_object_sets_for_floor(uint8_t floor, const void* data, size_t size); - void set_enemy_sets_for_floor(uint8_t floor, const void* data, size_t size); - void set_events_for_floor(uint8_t floor, const void* data, size_t size, bool allow_evt2); - void set_random_enemy_locations_for_floor(uint8_t floor, const void* data, size_t size); - void set_random_enemy_definitions_for_floor(uint8_t floor, const void* data, size_t size); + void set_object_sets_for_floor(uint8_t floor, size_t file_offset, const void* data, size_t size); + void set_enemy_sets_for_floor(uint8_t floor, size_t file_offset, const void* data, size_t size); + void set_events_for_floor(uint8_t floor, size_t file_offset, const void* data, size_t size, bool allow_evt2); + void set_random_enemy_locations_for_floor(uint8_t floor, size_t file_offset, const void* data, size_t size); + void set_random_enemy_definitions_for_floor(uint8_t floor, size_t file_offset, const void* data, size_t size); void compute_floor_start_indexes(); @@ -668,11 +678,12 @@ public: struct EnemyState { std::shared_ptr super_ene; enum Flag { - LAST_HIT_MASK = 0x03, - EXP_GIVEN = 0x04, - ITEM_DROPPED = 0x08, - ALL_HITS_MASK_FIRST = 0x10, - ALL_HITS_MASK = 0xF0, + LAST_HIT_MASK = 0x0003, + EXP_GIVEN = 0x0004, + ITEM_DROPPED = 0x0008, + ALL_HITS_MASK_FIRST = 0x0010, + ALL_HITS_MASK = 0x00F0, + GAME_FLAGS_IS_V3 = 0x0100, }; size_t e_id = 0; size_t set_id = 0; @@ -680,7 +691,9 @@ public: uint16_t rare_flags = 0; uint32_t game_flags = 0; // From 6x0A uint16_t set_flags = 0; // Only used if super_ene->child_index == 0 - uint8_t server_flags = 0; + uint16_t server_flags = 0; + + static uint32_t convert_game_flags(uint32_t game_flags, bool to_v3); inline void reset() { this->total_damage = 0; @@ -690,6 +703,23 @@ public: this->server_flags = 0; } + inline void set_game_flags(uint32_t game_flags, bool is_v3) { + this->game_flags = game_flags; + if (is_v3) { + this->server_flags |= Flag::GAME_FLAGS_IS_V3; + } else { + this->server_flags &= ~Flag::GAME_FLAGS_IS_V3; + } + } + inline uint32_t get_game_flags(bool is_v3) const { + bool flags_is_v3 = (this->server_flags & Flag::GAME_FLAGS_IS_V3); + if (flags_is_v3 == is_v3) { + return this->game_flags; + } else { + return this->convert_game_flags(this->game_flags, is_v3); + } + } + inline bool is_rare(Version version) const { return (((rare_flags >> static_cast(version)) & 1) || ((version == Version::BB_V4) ? this->super_ene->is_default_rare_bb : this->super_ene->is_default_rare_v123)); diff --git a/src/PlayerSubordinates.cc b/src/PlayerSubordinates.cc index ba744086..49790433 100644 --- a/src/PlayerSubordinates.cc +++ b/src/PlayerSubordinates.cc @@ -324,6 +324,50 @@ QuestFlagsV1::operator QuestFlags() const { return ret; } +const QuestFlagsForDifficulty QuestFlagsForDifficulty::BB_QUEST_FLAG_APPLY_MASK{{ + // clang-format off + /* 0000 */ 0x00, 0x3F, 0xFF, 0xE3, 0xE0, 0xFF, 0xFF, 0x00, + /* 0040 */ 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, + /* 0080 */ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, + /* 00C0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 0100 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFC, 0x00, + /* 0140 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + /* 0180 */ 0xFE, 0x00, 0x7F, 0xFE, 0x0F, 0xFF, 0xFF, 0x80, + /* 01C0 */ 0x3F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x0F, 0xFF, + /* 0200 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, + /* 0240 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 0280 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + /* 02C0 */ 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 0300 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 0340 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 0380 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 03C0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // clang-format on + + // The flags in the above mask are: + // 000A 000B 000C 000D 000E 000F 0010 0011 0012 0013 0014 0015 0016 0017 + // 0018 0019 001A 001E 001F 0020 0021 0022 0028 0029 002A 002B 002C 002D + // 002E 002F 0030 0031 0032 0033 0034 0035 0036 0037 0046 0047 0048 0049 + // 004A 004B 004C 004D 004E 004F 0050 0051 0052 0053 0054 0055 0056 0057 + // 0058 0059 005A 005B 005C 005D 005E 005F 0060 0061 0062 0063 0097 0098 + // 0099 009A 012D 012E 012F 0130 0131 0132 0133 0134 0135 0140 0141 0142 + // 0143 0144 0145 0146 0147 0148 0149 014A 014B 014C 014D 014E 014F 0150 + // 0151 0152 0153 0154 0155 0156 0157 0158 0159 015A 015B 015C 015D 015E + // 015F 0160 0161 0162 0163 0164 0165 0166 0167 0168 0169 016A 016B 016C + // 016D 016E 016F 0170 0171 0172 0173 0174 0175 0176 0177 0178 0179 017A + // 017B 017C 017D 017E 017F 0180 0181 0182 0183 0184 0185 0186 0191 0192 + // 0193 0194 0195 0196 0197 0198 0199 019A 019B 019C 019D 019E 01A4 01A5 + // 01A6 01A7 01A8 01A9 01AA 01AB 01AC 01AD 01AE 01AF 01B0 01B1 01B2 01B3 + // 01B4 01B5 01B6 01B7 01B8 01C2 01C3 01C4 01C5 01C6 01C7 01C8 01C9 01CA + // 01CB 01CC 01CD 01CE 01CF 01D0 01D1 01D2 01D3 01D4 01D5 01D6 01F4 01F5 + // 01F6 01F7 01F8 01F9 01FA 01FB 01FC 01FD 01FE 01FF 0200 0201 0202 0203 + // 0204 0205 0206 0207 0208 0209 020A 020B 020C 020D 020E 020F 0210 0211 + // 0212 0213 0214 0215 0216 0217 0218 0219 021A 021B 021C 021D 021E 021F + // 0220 0221 0222 0223 0224 0225 0226 0227 0228 0229 022A 022B 022C 022D + // 022E 022F 0230 0231 0232 0233 0234 0235 02BD 02BE 02BF 02C0 02C1 02C2 + // 02C3 02C4 +}}; + BattleRules::BattleRules(const phosg::JSON& json) { static const phosg::JSON empty_list = phosg::JSON::list(); @@ -699,46 +743,20 @@ const ChallengeTemplateDefinition& get_challenge_template_definition(Version ver } } -const QuestFlagsForDifficulty bb_quest_flag_apply_mask{{ - // clang-format off - /* 0000 */ 0x00, 0x3F, 0xFF, 0xE3, 0xE0, 0xFF, 0xFF, 0x00, - /* 0040 */ 0x03, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, - /* 0080 */ 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x00, 0x00, - /* 00C0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - /* 0100 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFC, 0x00, - /* 0140 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - /* 0180 */ 0xFE, 0x00, 0x7F, 0xFE, 0x0F, 0xFF, 0xFF, 0x80, - /* 01C0 */ 0x3F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x0F, 0xFF, - /* 0200 */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, - /* 0240 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - /* 0280 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, - /* 02C0 */ 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - /* 0300 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - /* 0340 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - /* 0380 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - /* 03C0 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // clang-format on +NPCTalkState::NPCTalkState(const NPCTalkState_DCProtos& proto) + : unknown_a1(proto.unknown_a1), + unknown_a2(proto.unknown_a2), + unknown_a3(0), + unknown_a4(proto.unknown_a4), + unknown_a5(proto.unknown_a5), + unknown_a6(proto.unknown_a6) {} - // The flags in the above mask are: - // 000A 000B 000C 000D 000E 000F 0010 0011 0012 0013 0014 0015 0016 0017 - // 0018 0019 001A 001E 001F 0020 0021 0022 0028 0029 002A 002B 002C 002D - // 002E 002F 0030 0031 0032 0033 0034 0035 0036 0037 0046 0047 0048 0049 - // 004A 004B 004C 004D 004E 004F 0050 0051 0052 0053 0054 0055 0056 0057 - // 0058 0059 005A 005B 005C 005D 005E 005F 0060 0061 0062 0063 0097 0098 - // 0099 009A 012D 012E 012F 0130 0131 0132 0133 0134 0135 0140 0141 0142 - // 0143 0144 0145 0146 0147 0148 0149 014A 014B 014C 014D 014E 014F 0150 - // 0151 0152 0153 0154 0155 0156 0157 0158 0159 015A 015B 015C 015D 015E - // 015F 0160 0161 0162 0163 0164 0165 0166 0167 0168 0169 016A 016B 016C - // 016D 016E 016F 0170 0171 0172 0173 0174 0175 0176 0177 0178 0179 017A - // 017B 017C 017D 017E 017F 0180 0181 0182 0183 0184 0185 0186 0191 0192 - // 0193 0194 0195 0196 0197 0198 0199 019A 019B 019C 019D 019E 01A4 01A5 - // 01A6 01A7 01A8 01A9 01AA 01AB 01AC 01AD 01AE 01AF 01B0 01B1 01B2 01B3 - // 01B4 01B5 01B6 01B7 01B8 01C2 01C3 01C4 01C5 01C6 01C7 01C8 01C9 01CA - // 01CB 01CC 01CD 01CE 01CF 01D0 01D1 01D2 01D3 01D4 01D5 01D6 01F4 01F5 - // 01F6 01F7 01F8 01F9 01FA 01FB 01FC 01FD 01FE 01FF 0200 0201 0202 0203 - // 0204 0205 0206 0207 0208 0209 020A 020B 020C 020D 020E 020F 0210 0211 - // 0212 0213 0214 0215 0216 0217 0218 0219 021A 021B 021C 021D 021E 021F - // 0220 0221 0222 0223 0224 0225 0226 0227 0228 0229 022A 022B 022C 022D - // 022E 022F 0230 0231 0232 0233 0234 0235 02BD 02BE 02BF 02C0 02C1 02C2 - // 02C3 02C4 -}}; +NPCTalkState::operator NPCTalkState_DCProtos() const { + NPCTalkState_DCProtos ret; + ret.unknown_a1 = this->unknown_a1; + ret.unknown_a2 = this->unknown_a2; + ret.unknown_a4 = this->unknown_a4; + ret.unknown_a5 = this->unknown_a5; + ret.unknown_a6 = this->unknown_a6; + return ret; +} diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index f38dca49..a83596d9 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -11,6 +11,7 @@ #include #include "ChoiceSearch.hh" +#include "CommonFileFormats.hh" #include "FileContentsCache.hh" #include "ItemData.hh" #include "LevelTable.hh" @@ -777,6 +778,8 @@ inline PlayerDispDataBB convert_player_disp_data( } struct QuestFlagsForDifficulty { + static const QuestFlagsForDifficulty BB_QUEST_FLAG_APPLY_MASK; + parray data; inline bool get(uint16_t flag_index) const { @@ -995,4 +998,44 @@ using SymbolChatBE = SymbolChatT; check_struct_size(SymbolChat, 0x3C); check_struct_size(SymbolChatBE, 0x3C); -extern const QuestFlagsForDifficulty bb_quest_flag_apply_mask; +struct TelepipeState { + /* 00 */ le_uint16_t owner_client_id = 0xFFFF; + /* 02 */ le_uint16_t floor = 0; + /* 04 */ le_uint32_t room_id = 0; + /* 08 */ VectorXYZF pos; + /* 14 */ le_uint32_t angle_y = 0; + /* 18 */ +} __packed_ws__(TelepipeState, 0x18); + +struct NPCTalkState_DCProtos { + // This is used in all versions of this command except DCNTE and 11/2000. + /* 00 */ le_uint16_t unknown_a1 = 0; + /* 02 */ le_uint16_t unknown_a2 = 0; + // unknown_a3 is missing in this format, unlike the v1+ format below + /* 04 */ le_float unknown_a4 = 0.0f; + /* 08 */ le_float unknown_a5 = 0.0f; + /* 0C */ le_float unknown_a6 = 0.0f; + /* 10 */ +} __packed_ws__(NPCTalkState_DCProtos, 0x10); + +struct NPCTalkState { + // This is used in all versions of this command except DCNTE and 11/2000. + /* 00 */ le_uint16_t unknown_a1 = 0; + /* 02 */ le_uint16_t unknown_a2 = 0; + /* 04 */ le_uint32_t unknown_a3 = 0; + /* 08 */ le_float unknown_a4 = 0.0f; + /* 0C */ le_float unknown_a5 = 0.0f; + /* 10 */ le_float unknown_a6 = 0.0f; + /* 14 */ + + NPCTalkState() = default; + NPCTalkState(const NPCTalkState_DCProtos& proto); + operator NPCTalkState_DCProtos() const; +} __packed_ws__(NPCTalkState, 0x14); + +struct StatusEffectState { + /* 00 */ le_uint32_t effect_type = 0; + /* 04 */ le_float multiplier = 0.0f; + /* 08 */ le_uint32_t remaining_frames = 0; + /* 0C */ +} __packed_ws__(StatusEffectState, 0x0C); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 9fb506b2..90998b74 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -788,11 +788,11 @@ Parsed6x70Data::Parsed6x70Data( unknown_a6(0), battle_team_number(0), telepipe(cmd.telepipe), - unknown_a8(cmd.unknown_a8), - unknown_a9_nte_112000(cmd.unknown_a9), + death_flags(cmd.death_flags), + npc_talk_state(cmd.npc_talk_state), area(cmd.area), - flags2_value(cmd.flags2), - flags2_is_v3(false), + game_flags(cmd.game_flags), + game_flags_is_v3(false), visual(cmd.visual), stats(cmd.stats), num_items(cmd.num_items), @@ -824,11 +824,11 @@ Parsed6x70Data::Parsed6x70Data( unknown_a6(0), battle_team_number(0), telepipe(cmd.telepipe), - unknown_a8(cmd.unknown_a8), - unknown_a9_nte_112000(cmd.unknown_a9), + death_flags(cmd.death_flags), + npc_talk_state(cmd.npc_talk_state), area(cmd.area), - flags2_value(cmd.flags2), - flags2_is_v3(false), + game_flags(cmd.game_flags), + game_flags_is_v3(false), visual(cmd.visual), stats(cmd.stats), num_items(cmd.num_items), @@ -860,7 +860,7 @@ Parsed6x70Data::Parsed6x70Data( Version from_version, bool from_client_customization) : Parsed6x70Data(cmd.base, guild_card_number, from_version, from_client_customization) { - this->flags2_is_v3 = true; + this->game_flags_is_v3 = true; this->stats = cmd.stats; this->num_items = cmd.num_items; this->items = cmd.items; @@ -876,7 +876,7 @@ Parsed6x70Data::Parsed6x70Data( Version from_version, bool from_client_customization) : Parsed6x70Data(cmd.base, guild_card_number, from_version, from_client_customization) { - this->flags2_is_v3 = true; + this->game_flags_is_v3 = true; this->stats = cmd.stats; this->num_items = cmd.num_items; this->items = cmd.items; @@ -892,7 +892,7 @@ Parsed6x70Data::Parsed6x70Data( Version from_version, bool from_client_customization) : Parsed6x70Data(cmd.base, guild_card_number, from_version, from_client_customization) { - this->flags2_is_v3 = true; + this->game_flags_is_v3 = true; this->stats = cmd.stats; this->num_items = cmd.num_items; this->items = cmd.items; @@ -909,10 +909,10 @@ G_SyncPlayerDispAndInventory_DCNTE_6x70 Parsed6x70Data::as_dc_nte(shared_ptrunknown_a5_nte; ret.unknown_a6 = this->unknown_a6; ret.telepipe = this->telepipe; - ret.unknown_a8 = this->unknown_a8; - ret.unknown_a9 = this->unknown_a9_nte_112000; + ret.death_flags = this->death_flags; + ret.npc_talk_state = this->npc_talk_state; ret.area = this->area; - ret.flags2 = this->get_flags2(false); + ret.game_flags = this->get_game_flags(false); ret.visual = this->visual; ret.stats = this->stats; ret.num_items = this->num_items; @@ -937,10 +937,10 @@ G_SyncPlayerDispAndInventory_DC112000_6x70 Parsed6x70Data::as_dc_112000(shared_p ret.bonus_tp_from_materials = this->bonus_tp_from_materials; ret.unknown_a5 = this->unknown_a5_112000; ret.telepipe = this->telepipe; - ret.unknown_a8 = this->unknown_a8; - ret.unknown_a9 = this->unknown_a9_nte_112000; + ret.death_flags = this->death_flags; + ret.npc_talk_state = this->npc_talk_state; ret.area = this->area; - ret.flags2 = this->get_flags2(false); + ret.game_flags = this->get_game_flags(false); ret.visual = this->visual; ret.stats = this->stats; ret.num_items = this->num_items; @@ -1099,11 +1099,11 @@ Parsed6x70Data::Parsed6x70Data( unknown_a6(base.unknown_a6), battle_team_number(base.battle_team_number), telepipe(base.telepipe), - unknown_a8(base.unknown_a8), - unknown_a9_final(base.unknown_a9), + death_flags(base.death_flags), + npc_talk_state(base.npc_talk_state), area(base.area), - flags2_value(base.flags2), - flags2_is_v3(!is_v1_or_v2(from_version)), + game_flags(base.game_flags), + game_flags_is_v3(!is_v1_or_v2(from_version)), technique_levels_v1(base.technique_levels_v1), visual(base.visual) {} @@ -1123,42 +1123,19 @@ G_6x70_Base_V1 Parsed6x70Data::base_v1(bool is_v3) const { ret.unknown_a6 = this->unknown_a6; ret.battle_team_number = this->battle_team_number; ret.telepipe = this->telepipe; - ret.unknown_a8 = this->unknown_a8; - ret.unknown_a9 = this->unknown_a9_final; + ret.death_flags = this->death_flags; + ret.npc_talk_state = this->npc_talk_state; ret.area = this->area; - ret.flags2 = this->get_flags2(is_v3); + ret.game_flags = this->get_game_flags(is_v3); ret.technique_levels_v1 = this->technique_levels_v1; ret.visual = this->visual; return ret; } -uint32_t Parsed6x70Data::get_flags2(bool is_v3) const { - // The format of flags2 was changed significantly between v2 and v3, and not - // accounting for this means that sometimes other characters won't appear - // when joining a game. Unfortunately, some bits were deleted on v3 and other - // bits were added, so it doesn't suffice to simply store the most complete - // format of this field - we have to be able to convert between the two. - - // Bits on v2: ---CBAzy xwvutsrq ponmlkji hgfedcba - // Bits on v3: ---GFEDC BAzyxwvu srqponkj hgfedcba - // The bits ilmt were removed in v3 and the bits to their left were shifted - // right. The bits DEFG were added in v3 and do not exist on v2. - - if (is_v3 == this->flags2_is_v3) { - return this->flags2_value; - } else if (!this->flags2_is_v3) { // Convert v2 -> v3 - return ( - (this->flags2_value & 0x000000FF) | - ((this->flags2_value & 0x00000600) >> 1) | - ((this->flags2_value & 0x0007E000) >> 3) | - ((this->flags2_value & 0x1FF00000) >> 4)); - } else { // Convert v3 -> v2 - return ( - (this->flags2_value & 0x000000FF) | - ((this->flags2_value << 1) & 0x00000600) | - ((this->flags2_value << 3) & 0x0007E000) | - ((this->flags2_value << 4) & 0x1FF00000)); - } +uint32_t Parsed6x70Data::get_game_flags(bool is_v3) const { + return (this->game_flags_is_v3 == is_v3) + ? this->game_flags + : MapState::EnemyState::convert_game_flags(this->game_flags, is_v3); } static void on_sync_joining_player_disp_and_inventory( @@ -3338,16 +3315,42 @@ static void on_trigger_set_event(shared_ptr c, uint8_t command, uint8_t forward_subcommand(c, command, flag, data, size); } -static void on_update_telepipe_state(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { +static inline uint32_t bswap32_high16(uint32_t v) { + return ((v >> 8) & 0x00FF0000) | ((v << 8) & 0xFF000000) | (v & 0x0000FFFF); +} + +static void on_update_telepipe_state(shared_ptr c, uint8_t command, uint8_t, void* data, size_t size) { + if (c->lobby_client_id > 3) { + throw logic_error("client ID is above 3"); + } + if (command_is_private(command)) { + return; + } auto l = c->require_lobby(); if (!l->is_game()) { return; } - c->telepipe_state = check_size_t(data, size); + auto& cmd = check_size_t(data, size); + c->telepipe_state = cmd.state; c->telepipe_lobby_id = l->lobby_id; - forward_subcommand(c, command, flag, data, size); + // See the comments in G_SetTelepipeState_6x68 in CommandsFormats.hh for + // why we have to do this + if (is_big_endian(c->version())) { + c->telepipe_state.room_id = bswap32_high16(phosg::bswap32(c->telepipe_state.room_id)); + } + + for (auto lc : l->clients) { + if (lc && (lc != c)) { + if (is_big_endian(lc->version())) { + cmd.state.room_id = phosg::bswap32(bswap32_high16(c->telepipe_state.room_id)); + } else { + cmd.state.room_id = c->telepipe_state.room_id; + } + send_command_t(lc, 0x60, 0x00, cmd); + } + } } static void on_update_enemy_state(shared_ptr c, uint8_t command, uint8_t, void* data, size_t size) { @@ -3367,36 +3370,64 @@ static void on_update_enemy_state(shared_ptr c, uint8_t command, uint8_t if ((cmd.enemy_index & 0xF000) || (cmd.header.entity_id != (cmd.enemy_index | 0x1000))) { throw runtime_error("mismatched enemy id/index"); } + bool is_v3 = !is_v1_or_v2(c->version()); auto ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, cmd.enemy_index); - ene_st->game_flags = is_big_endian(c->version()) ? bswap32(cmd.flags) : cmd.flags.load(); + uint32_t src_flags = is_big_endian(c->version()) ? bswap32(cmd.game_flags) : cmd.game_flags.load(); + if (l->difficulty == 3) { + src_flags = (src_flags & 0xFFFFFFC0) | (ene_st->get_game_flags(is_v3) & 0x0000003F); + } + ene_st->set_game_flags(src_flags, is_v3); ene_st->total_damage = cmd.total_damage; ene_st->set_last_hit_by_client_id(c->lobby_client_id); - l->log.info("E-%03zX updated to damage=%hu game_flags=%08" PRIX32, - ene_st->e_id, ene_st->total_damage, ene_st->game_flags); + l->log.info("E-%03zX updated to damage=%hu game_flags=%08" PRIX32 " (%s)", + ene_st->e_id, + ene_st->total_damage, + ene_st->game_flags, + (ene_st->server_flags & MapState::EnemyState::Flag::GAME_FLAGS_IS_V3) ? "v3" : "v2"); - G_UpdateEnemyState_GC_6x0A sw_cmd = { - {cmd.header.subcommand, cmd.header.size, cmd.header.entity_id}, - cmd.enemy_index, cmd.total_damage, cmd.flags.load()}; - bool sender_is_be = is_big_endian(c->version()); for (auto lc : l->clients) { if (lc && (lc != c)) { - if (is_big_endian(lc->version()) == sender_is_be) { - cmd.enemy_index = l->map_state->index_for_enemy_state(lc->version(), ene_st); - if (cmd.enemy_index != 0xFFFF) { - cmd.header.entity_id = 0x1000 | cmd.enemy_index; - send_command_t(lc, 0x60, 0x00, cmd); - } - } else { - sw_cmd.enemy_index = l->map_state->index_for_enemy_state(lc->version(), ene_st); - if (sw_cmd.enemy_index != 0xFFFF) { - sw_cmd.header.entity_id = 0x1000 | sw_cmd.enemy_index; - send_command_t(lc, 0x60, 0x00, sw_cmd); - } + cmd.enemy_index = l->map_state->index_for_enemy_state(lc->version(), ene_st); + if (cmd.enemy_index != 0xFFFF) { + cmd.header.entity_id = 0x1000 | cmd.enemy_index; + uint32_t game_flags = ene_st->get_game_flags(!is_v1_or_v2(lc->version())); + cmd.game_flags = is_big_endian(lc->version()) ? phosg::bswap32(game_flags) : game_flags; + send_command_t(lc, 0x60, 0x00, cmd); } } } } +static void on_set_enemy_low_game_flags_ultimate( + shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { + auto& cmd = check_size_t(data, size); + + if (command_is_private(command) || + (cmd.header.entity_id < 0x1000) || + (cmd.header.entity_id >= 0x4000) || + (cmd.low_game_flags & 0xFFFFFFC0) || + (c->lobby_client_id > 3)) { + return; + } + auto l = c->require_lobby(); + if (!l->is_game() || (l->difficulty != 3)) { + return; + } + + bool is_v3 = !is_v1_or_v2(c->version()); + auto ene_st = l->map_state->enemy_state_for_index(c->version(), c->floor, cmd.header.entity_id - 0x1000); + uint32_t game_flags = ene_st->get_game_flags(is_v3); + if (!(game_flags & cmd.low_game_flags)) { + ene_st->set_game_flags(game_flags | cmd.low_game_flags, is_v3); + l->log.info("E-%03zX updated to game_flags=%08" PRIX32 " (%s)", + ene_st->e_id, + ene_st->game_flags, + (ene_st->server_flags & MapState::EnemyState::Flag::GAME_FLAGS_IS_V3) ? "v3" : "v2"); + } + + forward_subcommand_with_entity_id_transcode_t(c, command, flag, data, size); +} + template static void on_update_object_state_t(shared_ptr c, uint8_t command, uint8_t, void* data, size_t size) { auto& cmd = check_size_t(data, size); @@ -3501,7 +3532,7 @@ static void on_battle_scores(shared_ptr c, uint8_t command, uint8_t, voi } } -static void on_dragon_actions(shared_ptr c, uint8_t command, uint8_t, void* data, size_t size) { +static void on_dragon_actions_6x12(shared_ptr c, uint8_t command, uint8_t, void* data, size_t size) { auto& cmd = check_size_t(data, size); if (command_is_private(command)) { @@ -5045,6 +5076,7 @@ static void on_write_quest_counter_bb(shared_ptr c, uint8_t, uint8_t, vo constexpr uint8_t NONE = 0x00; const SubcommandDefinition subcommand_definitions[0x100] = { + // {DC NTE, 11/2000, all other versions, handler} /* 6x00 */ {0x00, 0x00, 0x00, on_invalid}, /* 6x01 */ {0x01, 0x01, 0x01, on_invalid}, /* 6x02 */ {0x02, 0x02, 0x02, forward_subcommand_m}, @@ -5054,18 +5086,18 @@ const SubcommandDefinition subcommand_definitions[0x100] = { /* 6x06 */ {0x06, 0x06, 0x06, on_send_guild_card}, /* 6x07 */ {0x07, 0x07, 0x07, on_symbol_chat, SDF::ALWAYS_FORWARD_TO_WATCHERS}, /* 6x08 */ {0x08, 0x08, 0x08, on_invalid}, - /* 6x09 */ {0x09, 0x09, 0x09, forward_subcommand_with_entity_id_transcode_t}, + /* 6x09 */ {0x09, 0x09, 0x09, on_invalid}, // See notes in CommandFormats.hh /* 6x0A */ {0x0A, 0x0A, 0x0A, on_update_enemy_state}, /* 6x0B */ {0x0B, 0x0B, 0x0B, on_update_object_state_t}, /* 6x0C */ {0x0C, 0x0C, 0x0C, on_received_condition}, /* 6x0D */ {NONE, NONE, 0x0D, on_forward_check_game}, - /* 6x0E */ {NONE, NONE, 0x0E, on_forward_check_game}, + /* 6x0E */ {NONE, NONE, 0x0E, forward_subcommand_with_entity_id_transcode_t}, /* 6x0F */ {NONE, NONE, 0x0F, on_invalid}, - /* 6x10 */ {0x0E, 0x0E, 0x10, forward_subcommand_with_entity_id_transcode_t}, - /* 6x11 */ {0x0F, 0x0F, 0x11, forward_subcommand_with_entity_id_transcode_t}, - /* 6x12 */ {0x10, 0x10, 0x12, on_dragon_actions}, + /* 6x10 */ {0x0E, 0x0E, 0x10, forward_subcommand_with_entity_id_transcode_t}, + /* 6x11 */ {0x0F, 0x0F, 0x11, forward_subcommand_with_entity_id_transcode_t}, + /* 6x12 */ {0x10, 0x10, 0x12, on_dragon_actions_6x12}, /* 6x13 */ {0x11, 0x11, 0x13, forward_subcommand_with_entity_id_transcode_t}, - /* 6x14 */ {0x12, 0x12, 0x14, forward_subcommand_with_entity_id_transcode_t}, + /* 6x14 */ {0x12, 0x12, 0x14, forward_subcommand_with_entity_id_transcode_t}, /* 6x15 */ {0x13, 0x13, 0x15, forward_subcommand_with_entity_id_transcode_t}, /* 6x16 */ {0x14, 0x14, 0x16, forward_subcommand_with_entity_id_transcode_t}, /* 6x17 */ {0x15, 0x15, 0x17, forward_subcommand_with_entity_id_transcode_t}, @@ -5179,7 +5211,7 @@ const SubcommandDefinition subcommand_definitions[0x100] = { /* 6x83 */ {NONE, NONE, 0x83, on_forward_check_game}, /* 6x84 */ {NONE, NONE, 0x84, on_forward_check_game}, /* 6x85 */ {NONE, NONE, 0x85, on_forward_check_game}, - /* 6x86 */ {NONE, NONE, 0x86, forward_subcommand_with_entity_id_transcode_t}, + /* 6x86 */ {NONE, NONE, 0x86, on_update_object_state_t}, /* 6x87 */ {NONE, NONE, 0x87, on_forward_check_game}, /* 6x88 */ {NONE, NONE, 0x88, on_forward_check_game}, /* 6x89 */ {NONE, NONE, 0x89, forward_subcommand_with_entity_id_transcode_t}, @@ -5201,7 +5233,7 @@ const SubcommandDefinition subcommand_definitions[0x100] = { /* 6x99 */ {NONE, NONE, 0x99, on_forward_check_game}, /* 6x9A */ {NONE, NONE, 0x9A, on_forward_check_game_client}, /* 6x9B */ {NONE, NONE, 0x9B, on_forward_check_game}, - /* 6x9C */ {NONE, NONE, 0x9C, forward_subcommand_with_entity_id_transcode_t}, + /* 6x9C */ {NONE, NONE, 0x9C, on_set_enemy_low_game_flags_ultimate}, /* 6x9D */ {NONE, NONE, 0x9D, on_forward_check_game}, /* 6x9E */ {NONE, NONE, 0x9E, forward_subcommand_m}, /* 6x9F */ {NONE, NONE, 0x9F, forward_subcommand_with_entity_id_transcode_t}, diff --git a/src/ReceiveSubcommands.hh b/src/ReceiveSubcommands.hh index d427777f..d89649e8 100644 --- a/src/ReceiveSubcommands.hh +++ b/src/ReceiveSubcommands.hh @@ -50,23 +50,22 @@ public: uint16_t bonus_hp_from_materials = 0; uint16_t bonus_tp_from_materials = 0; parray unknown_a5_112000; - G_6x70_StatusEffectState permanent_status_effect; - G_6x70_StatusEffectState temporary_status_effect; - G_6x70_StatusEffectState attack_status_effect; - G_6x70_StatusEffectState defense_status_effect; - G_6x70_StatusEffectState unused_status_effect; + StatusEffectState permanent_status_effect; + StatusEffectState temporary_status_effect; + StatusEffectState attack_status_effect; + StatusEffectState defense_status_effect; + StatusEffectState unused_status_effect; uint32_t language = 0; uint32_t player_tag = 0; uint32_t guild_card_number = 0; uint32_t unknown_a6 = 0; uint32_t battle_team_number = 0; G_6x70_Sub_Telepipe telepipe; - uint32_t unknown_a8 = 0; - parray unknown_a9_nte_112000; - G_6x70_Sub_UnknownA1 unknown_a9_final; + uint32_t death_flags = 0; + NPCTalkState npc_talk_state; uint32_t area = 0; - uint32_t flags2_value = 0; - bool flags2_is_v3 = false; + uint32_t game_flags = 0; + bool game_flags_is_v3 = false; parray technique_levels_v1 = 0xFF; PlayerVisualConfig visual; std::string name; @@ -127,7 +126,7 @@ protected: Version from_version, bool from_client_customization); G_6x70_Base_V1 base_v1(bool is_v3) const; - uint32_t get_flags2(bool is_v3) const; + uint32_t get_game_flags(bool is_v3) const; }; bool validate_6xBB(G_SyncCardTradeServerState_Ep3_6xBB& cmd); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 4422905e..4e0569c3 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -649,7 +649,7 @@ void send_guild_card_chunk_bb(shared_ptr c, size_t chunk_index) { S_GuildCardFileChunk_02DC cmd; size_t data_size = min(sizeof(PSOBBGuildCardFile) - chunk_offset, sizeof(cmd.data)); - cmd.unknown = 0; + cmd.unknown_a1 = 0; cmd.chunk_index = chunk_index; cmd.data.assign_range( reinterpret_cast(c->guild_card_file().get()) + chunk_offset, @@ -2672,7 +2672,7 @@ void send_game_item_state(shared_ptr c) { fi.source_type = 0; fi.entity_index = 0xFFFF; fi.pos = item->pos; - fi.unknown_a2 = 0; + fi.room_id = 0; fi.drop_number = (floor == 0) ? 0xFFFF : (decompressed_header.next_drop_number_per_floor.at(floor - 1)++); fi.item = item->data; fi.item.encode_for_version(c->version(), s->item_parameter_table_for_encode(c->version())); @@ -2736,9 +2736,10 @@ void send_game_enemy_state(shared_ptr c) { auto s = c->require_server_state(); vector entries; + bool is_v3 = !is_v1_or_v2(c->version()); for (auto ene_st : l->map_state->iter_enemy_states(c->version())) { auto& entry = entries.emplace_back(); - entry.flags = ene_st->game_flags; + entry.flags = ene_st->get_game_flags(is_v3); entry.item_drop_id = (ene_st->server_flags & MapState::EnemyState::Flag::ITEM_DROPPED) ? 0xFFFF : (0xCA0 + l->map_state->index_for_enemy_state(c->version(), ene_st)); @@ -2921,11 +2922,7 @@ void send_game_player_state(shared_ptr to_c, shared_ptr from_c, auto to_l = to_c->lobby.lock(); if (to_l && (from_c->telepipe_lobby_id == to_l->lobby_id)) { - to_send.telepipe.owner_client_id = from_c->telepipe_state.client_id2; - to_send.telepipe.floor = from_c->telepipe_state.floor; - to_send.telepipe.unknown_a1 = from_c->telepipe_state.unknown_b3; - to_send.telepipe.pos = from_c->telepipe_state.pos; - to_send.telepipe.unknown_a3 = from_c->telepipe_state.unknown_a3; + to_send.telepipe.state = from_c->telepipe_state; } if (apply_overrides) { @@ -3040,7 +3037,7 @@ void send_create_inventory_item_to_client(shared_ptr c, uint8_t client_i cmd.header.client_id = client_id; cmd.item_data = item; cmd.unused1 = 0; - cmd.unknown_a2 = 0; + cmd.equip_item = 0; cmd.unused2.clear(0); send_command_t(c, 0x60, 0x00, cmd); }