From 168995a5b5a09c27bb6ce618e2c24df64441b6a2 Mon Sep 17 00:00:00 2001 From: dannuic Date: Sun, 7 Jun 2026 01:12:57 -0600 Subject: [PATCH] Full Packet Review for Known Conversion (#5100) --- common/eq_packet_structs.h | 6 +- common/patches/tob.cpp | 558 +++++++++++++++++- common/patches/tob_ops.h | 24 + common/patches/tob_structs.h | 254 +++++++-- tob/opcodes.md | 1033 +++++++++++++++++----------------- utils/patches/patch_TOB.conf | 2 +- zone/client.cpp | 2 + zone/client_packet.cpp | 6 +- zone/tradeskills.cpp | 16 + 9 files changed, 1317 insertions(+), 584 deletions(-) diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index 42fec1d24..cca106273 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -630,8 +630,8 @@ struct GMTrainEnd_Struct struct GMSkillChange_Struct { /*000*/ uint16 npcid; /*002*/ uint8 unknown1[2]; // something like PC_ID, but not really. stays the same thru the session though -/*002*/ uint16 skillbank; // 0 if normal skills, 1 if languages -/*002*/ uint8 unknown2[2]; +/*004*/ uint16 skillbank; // 0 if normal skills, 1 if languages +/*006*/ uint8 unknown2[2]; /*008*/ uint16 skill_id; /*010*/ uint8 unknown3[2]; }; @@ -5334,7 +5334,7 @@ typedef struct { struct ControlBoat_Struct { /*000*/ uint32 boatId; // entitylist id of the boat /*004*/ bool TakeControl; // 01 if taking control, 00 if releasing it -/*007*/ char unknown[3]; // no idea what these last three bytes represent +/*005*/ char unknown[3]; // no idea what these last three bytes represent }; struct AugmentInfo_Struct diff --git a/common/patches/tob.cpp b/common/patches/tob.cpp index 0536034b8..efd1030fb 100644 --- a/common/patches/tob.cpp +++ b/common/patches/tob.cpp @@ -220,7 +220,7 @@ namespace TOB ENCODE_LENGTH_EXACT(ApplyPoison_Struct); SETUP_DIRECT_ENCODE(ApplyPoison_Struct, structs::ApplyPoison_Struct); - eq->inventorySlot = ServerToTOBTypelessSlot(emu->inventorySlot, EQ::invtype::typePossessions); + eq->inventorySlot = ServerToTOBSlot(emu->inventorySlot); OUT(success); FINISH_ENCODE(); @@ -234,6 +234,10 @@ namespace TOB OUT(itemid); OUT(window); strn0cpy(eq->augment_info, emu->augment_info, 64); + // TODO: TOB wire format has 8 extra bytes beyond server struct (total 80 bytes): + // +0x48 uint32 unknown072 -- 0x37 (55) triggers hardcoded "perfected distiller" message in client; + // 0 causes jnz path (loc_1401EFB6D) which likely uses augment_info text + // +0x4C uint32 unknown076 -- "always matches what client sends"; not echoed by current decoder FINISH_ENCODE(); } @@ -248,8 +252,8 @@ namespace TOB OUT(Unknown08); eq->Result = static_cast(emu->Result); OUT(Amount); - eq->StringSize = 0; // set this to 0, but it's a string size - eq->Lucky = 0; // set to 1 to message a lucky beg + eq->StringSize = 0; // TODO: set this to 0, but it's a string size + eq->Lucky = 0; // TODO: set to 1 to message a lucky beg FINISH_ENCODE(); } @@ -317,6 +321,16 @@ namespace TOB FINISH_ENCODE(); } + ENCODE(OP_Charm) + { + SETUP_DIRECT_ENCODE(Charm_Struct, structs::Charm_Struct); + eq->owner_id = emu->owner_id; + eq->pet_id = emu->pet_id; + eq->charmer_id = 0; // TOB wire format has extra spawn ID at +0x08; server struct doesn't provide it; passed to sub_1402FA570 when non-null + eq->command = static_cast(emu->command); + FINISH_ENCODE(); + } + ENCODE(OP_ChannelMessage) { EQApplicationPacket* in = *p; @@ -459,6 +473,12 @@ namespace TOB OUT(type); OUT(icon); eq->unknown16 = 0; + // TODO: unknown24 is used by the client as ItemDefinition->ItemNumber (item DB ID for the + // world container). Server struct field is labeled unknown24 and may not be populated. + // TOB wire: +0x00 drop_id, +0x04 unknown04(-1), +0x08 unknown08(-1), +0x0C type(1B), + // +0x10 unknown16(1B, client overwrites to 10), +0x14 icon, +0x18 unknown24/ItemNumber, + // +0x1C object_name[64] + OUT(unknown24); OUT_str(object_name); FINISH_ENCODE(); @@ -520,6 +540,7 @@ namespace TOB OUT(spawn_id); OUT(killer_id); + OUT(corpseid); OUT(spell_id); OUT(attack_skill); OUT(damage); @@ -566,7 +587,20 @@ namespace TOB SETUP_DIRECT_ENCODE(DeleteSpawn_Struct, structs::DeleteSpawn_Struct); OUT(spawn_id); - eq->unknown04 = 1; // Observed + OUT(Decay); + + FINISH_ENCODE(); + } + + ENCODE(OP_DisciplineTimer) + { + ENCODE_LENGTH_EXACT(DisciplineTimer_Struct); + SETUP_DIRECT_ENCODE(DisciplineTimer_Struct, structs::DisciplineTimer_Struct); + + OUT(TimerID); + OUT(Duration); + OUT(Unknown08); + eq->ServerTime = Timer::GetCurrentTime(); FINISH_ENCODE(); } @@ -602,6 +636,28 @@ namespace TOB FINISH_ENCODE(); } + ENCODE(OP_Fling) + { + SETUP_DIRECT_ENCODE(fling_struct, structs::fling_struct); + + OUT(speed_z); + OUT(new_y); + OUT(new_x); + OUT(new_z); + eq->radius = 0.0f; + eq->unknown = 0; + OUT(travel_time); + eq->collision = static_cast(emu->collision != 0 ? 1 : 0); + // TODO: verify fall damage semantics — client stores player.408 = (TOB[29] == 0), so + // direct copy of disable_fall_damage (1=no damage) yields player.408=0; if player.408 + // is a "damage disabled" flag this is correct, but if it is "damage enabled" flip to + // eq->fall_damage = !emu->disable_fall_damage (needs gameplay test) + eq->fall_damage = emu->disable_fall_damage; + eq->z_override = emu->unk3; + + FINISH_ENCODE(); + } + ENCODE(OP_GMTraining) { ENCODE_LENGTH_EXACT(GMTrainee_Struct); SETUP_DIRECT_ENCODE(GMTrainee_Struct, structs::GMTrainee_Struct); @@ -695,6 +751,23 @@ namespace TOB delete in; } + ENCODE(OP_GroupInvite) + { + ENCODE_LENGTH_EXACT(GroupInvite_Struct); + SETUP_VAR_ENCODE(GroupInvite_Struct); + // Allocate 4 bytes beyond the struct so the client's read of group_request_id + // at offset 168 (past the 168-byte struct) lands in valid, zeroed memory. + // The server has no equivalent field; the client receives 0. + ALLOC_VAR_ENCODE(structs::GroupGeneric_Struct, sizeof(structs::GroupGeneric_Struct) + sizeof(uint32)); + + memcpy(eq->name1, emu->invitee_name, sizeof(eq->name1)); + memcpy(eq->name2, emu->inviter_name, sizeof(eq->name2)); + // TODO: determine what the client expects for group_request_id at offset 168 — + // it is stored in EverQuest_GroupRequestId and may be used in the accept/decline flow. + + FINISH_ENCODE(); + } + ENCODE(OP_HPUpdate) { SETUP_DIRECT_ENCODE(SpawnHPUpdate_Struct, structs::SpawnHPUpdate_Struct); @@ -730,6 +803,28 @@ namespace TOB FINISH_ENCODE(); } + ENCODE(OP_IncreaseStats) + { + ENCODE_LENGTH_EXACT(IncreaseStat_Struct); + SETUP_VAR_ENCODE(IncreaseStat_Struct); + ALLOC_VAR_ENCODE(structs::IncreaseStat_Struct, sizeof(structs::IncreaseStat_Struct)); + + // entity_id is stashed in unknown13[0..1] by Client::IncStats/SetStats (zone/client.cpp) + // because IncreaseStat_Struct has no spawn_id field. The client validates this against + // g_pLocalPlayer->SpawnID before applying the stat. + eq->spawn_id = *reinterpret_cast(emu->unknown13); + + if (emu->str) { eq->stat_type = STAT_STR; eq->value = emu->str; } + else if (emu->sta) { eq->stat_type = STAT_STA; eq->value = emu->sta; } + else if (emu->agi) { eq->stat_type = STAT_AGI; eq->value = emu->agi; } + else if (emu->dex) { eq->stat_type = STAT_DEX; eq->value = emu->dex; } + else if (emu->int_) { eq->stat_type = STAT_INT; eq->value = emu->int_; } + else if (emu->wis) { eq->stat_type = STAT_WIS; eq->value = emu->wis; } + else if (emu->cha) { eq->stat_type = STAT_CHA; eq->value = emu->cha; } + + FINISH_ENCODE(); + } + ENCODE(OP_ItemPacket) { EQApplicationPacket* in = *p; @@ -751,14 +846,11 @@ namespace TOB cereal::BinaryInputArchive ar(ss); ar(pms); - uint32 player_name_length = pms.player_name.length(); - uint32 note_length = pms.note.length(); - auto* int_struct = (EQ::InternalSerializedItem_Struct*)pms.serialized_item.data(); SerializeBuffer buffer; buffer.WriteInt32((int32_t)type); - SerializeItem(buffer, (const EQ::ItemInstance*)int_struct->inst, int_struct->slot_id, 0, old_item_pkt->PacketType); + SerializeItem(buffer, (const EQ::ItemInstance*)int_struct->inst, pms.slot_id, 0, old_item_pkt->PacketType); buffer.WriteUInt32(pms.sent_time); buffer.WriteLengthString(pms.player_name); @@ -784,6 +876,64 @@ namespace TOB delete in; } + ENCODE(OP_ItemRecastDelay) + { + SETUP_DIRECT_ENCODE(ItemRecastDelay_Struct, structs::ItemRecastDelay_Struct); + + // TODO: server struct ItemRecastDelay_Struct needs an ItemGlobalIndex (12 bytes) so the + // client can locate the item and update its per-item recast timestamp (item+0x14). + // Until that field is added server-side, item_global_index is zeroed and the item + // lookup in GetItemByGlobalIndex will fail silently — SetCoreItemRecastTimer (the + // core recast-by-type timer) will still fire correctly for valid recast_type values. + eq->item_slot = {}; + eq->recast_delay = emu->recast_delay; + eq->recast_type = emu->recast_type; + // ignore_casting_requirement has no client equivalent (not read by TOB client) + + FINISH_ENCODE(); + } + + DECODE(OP_ItemVerifyRequest) + { + DECODE_LENGTH_EXACT(structs::ItemVerifyRequest_Struct); + SETUP_DIRECT_DECODE(ItemVerifyRequest_Struct, structs::ItemVerifyRequest_Struct); + + emu->slot = TOBToServerSlot(eq->inventory_slot); + IN(target); + + FINISH_DIRECT_DECODE(); + } + + ENCODE(OP_ItemVerifyReply) + { + ENCODE_LENGTH_EXACT(ItemVerifyReply_Struct); + SETUP_DIRECT_ENCODE(ItemVerifyReply_Struct, structs::ItemVerifyReply_Struct); + + OUT(slot); + OUT(spell); + OUT(target); + // TODO: expand server struct ItemVerifyReply_Struct to support autobook-scribe — TOB wire format (20 bytes): + // +0x00 int32 slot (passed as ItemGlobalIndex* to GetItemByGlobalIndex / IsHeldSlot) + // +0x04 uint32 spell (client reads lower 16 bits; 0x407 = autobook-scribe path) + // +0x08 uint32 target + // +0x0C int32 unknown0 (exit gate: handler skips without processing if < 0; send 0) + // +0x10 int32 recast_time (fasttime() timestamp; must be non-zero to enter autobook-scribe + // path when spell==0x407; zeroed here until server provides it) + // unknown0 and recast_time are zeroed by ALLOC_VAR_ENCODE (memset) + + FINISH_ENCODE(); + } + + ENCODE(OP_LinkedReuse) + { + SETUP_DIRECT_ENCODE(LinkedSpellReuseTimer_Struct, structs::LinkedSpellReuseTimer_Struct); + OUT(timer_id); + eq->unknown = 0; + OUT(end_time); + OUT(start_time); + FINISH_ENCODE(); + } + ENCODE(OP_LogServer) { SETUP_VAR_ENCODE(LogServer_Struct); ALLOC_LEN_ENCODE(1932); @@ -858,6 +1008,22 @@ namespace TOB FINISH_ENCODE(); } + ENCODE(OP_LootItem) + { + ENCODE_LENGTH_EXACT(LootingItem_Struct); + SETUP_DIRECT_ENCODE(LootingItem_Struct, structs::LootingItem_Struct); + + OUT(lootee); + OUT(looter); + eq->slot_id = ServerToTOBCorpseMainSlot(emu->slot_id); + OUT(auto_loot); + // TODO: unknown16 appears to be quantity (items looted from a partial stack); + // server struct has no quantity field so we cannot populate it here + eq->unknown16 = 0; + + FINISH_ENCODE(); + } + ENCODE(OP_ManaChange) { ENCODE_LENGTH_EXACT(ManaChange_Struct); SETUP_DIRECT_ENCODE(ManaChange_Struct, structs::ManaChange_Struct); @@ -1084,7 +1250,7 @@ namespace TOB /* s32 NPCAgroMaxDist; */ - buffer.WriteInt32(600); + buffer.WriteInt32(emu->npc_aggro_max_dist); /* s32 ForageLow; @@ -1207,6 +1373,30 @@ namespace TOB delete in; } + ENCODE(OP_PickPocket) + { + ENCODE_LENGTH_EXACT(sPickPocket_Struct); + SETUP_VAR_ENCODE(sPickPocket_Struct); + + uint32 nameLen = strnlen(emu->itemname, sizeof(emu->itemname)); + uint32 pktLen = sizeof(structs::PickPocket_Struct) + nameLen + 1; + + ALLOC_LEN_ENCODE(pktLen); + + auto *eq = reinterpret_cast(__packet->pBuffer); + eq->to = emu->to; + eq->from = emu->from; + eq->myskill = emu->myskill; + eq->type = static_cast(emu->type); + eq->coin = emu->coin; + eq->nameLen = nameLen; + if (nameLen > 0) + memcpy(__packet->pBuffer + sizeof(structs::PickPocket_Struct), emu->itemname, nameLen); + __packet->pBuffer[sizeof(structs::PickPocket_Struct) + nameLen] = 0; // luckily + + FINISH_ENCODE(); + } + ENCODE(OP_PlayerProfile) { EQApplicationPacket* in = *p; *p = nullptr; @@ -2250,6 +2440,22 @@ namespace TOB delete in; } + ENCODE(OP_ReadBook) + { + ENCODE_LENGTH_ATLEAST(BookText_Struct); + SETUP_DIRECT_ENCODE(BookText_Struct, structs::BookRequest_Struct); + + eq->window = (emu->window == 0xFF) ? 0xFFFFFFFF : emu->window; + OUT(type); + OUT(target_id); + eq->invslot = ServerToTOBTypelessSlot(emu->invslot, invtype::typePossessions); + OUT(can_cast); // wire 0x13 = note-path cast button + OUT(can_scribe); // wire 0x14 = book cast / note scribe button + strn0cpy(eq->txtfile, emu->booktext, sizeof(eq->txtfile)); + + FINISH_ENCODE(); + } + ENCODE(OP_RecipeAutoCombine) { ENCODE_LENGTH_EXACT(RecipeAutoCombine_Struct); @@ -2257,12 +2463,19 @@ namespace TOB OUT(object_type); OUT(some_id); - eq->container_slot = ServerToTOBSlot(emu->unknown1); + { + structs::InventorySlot_Struct cslot = ServerToTOBSlot(emu->unknown1); + eq->container_type = cslot.Type; + eq->container_slot_index = cslot.Slot; + eq->container_subindex = cslot.SubIndex; + eq->container_augindex = cslot.AugIndex; + // Padding2 intentionally omitted — client deserializer skips it + } structs::InventorySlot_Struct TOBSlot; - TOBSlot.Type = 8; // Observed - TOBSlot.Slot = 0xffff; - TOBSlot.SubIndex = 0xffff; - TOBSlot.AugIndex = 0xffff; + TOBSlot.Type = 8; // Observed + TOBSlot.Slot = -1; + TOBSlot.SubIndex = -1; + TOBSlot.AugIndex = -1; TOBSlot.Padding2 = 0; eq->unknown_slot = TOBSlot; OUT(recipe_id); @@ -2286,7 +2499,7 @@ namespace TOB eq->aapoints_assigned[4] = 0; eq->aapoints_assigned[5] = 0; - for (uint32 i = 0; i < structs::MAX_PP_AA_ARRAY; ++i) + for (uint32 i = 0; i < MAX_PP_AA_ARRAY; ++i) // server struct has 240 entries; TOB expects 300, entries 240-299 remain zero { eq->aa_list[i].AA = emu->aa_list[i].AA; eq->aa_list[i].value = emu->aa_list[i].value; @@ -2786,7 +2999,6 @@ namespace TOB ENCODE_LENGTH_EXACT(Merchant_Purchase_Struct); SETUP_DIRECT_ENCODE(Merchant_Purchase_Struct, structs::Merchant_Purchase_Response_Struct); - OUT(npcid); eq->inventory_slot = ServerToTOBTypelessSlot(emu->itemslot, EQ::invtype::typePossessions); OUT(quantity); OUT(price); @@ -2799,17 +3011,16 @@ namespace TOB ENCODE_LENGTH_EXACT(MerchantClick_Struct); SETUP_DIRECT_ENCODE(MerchantClick_Struct, structs::MerchantClickResponse_Struct); - if (emu->command == 0) { - OUT(player_id); - eq->npc_id = 0; - } - else { - OUT(npc_id); + OUT(npc_id); + if (emu->command != 0) { OUT(player_id); OUT(rate); OUT(tab_display); eq->unknown028 = 256; + // TODO: ldon_category (+16), alt_currency1 (+20), alt_currency2 (+24) not in + // MerchantClick_Struct -- always 0; alt-currency/LDON merchants may not open correctly } + // close (command==0): player_id stays 0 (SETUP zeroed it) -- byte at offset 4 = 0 -> close path FINISH_ENCODE(); } @@ -2881,7 +3092,12 @@ namespace TOB eq->spawn_id = sas->spawn_id; eq->type = ServerToTOBSpawnAppearanceType(sas->type); eq->parameter = sas->parameter; - + // msg_stat_change reads the primary value from lock_id (offset +16) for half the + // TOBAppearance types (MaxHealth, Health, PVP, Sneak, Linkdead, Invisibility visibility). + // The other half read from parameter (offset +8). Both are set to the same server value + // so either read path works; each case ignores the field it doesn't use. + eq->lock_id = sas->parameter; + dest->FastQueuePacket(&outapp, ack_req); delete in; return; @@ -2948,6 +3164,50 @@ namespace TOB FINISH_ENCODE(); } + ENCODE(OP_Track) + { + SETUP_VAR_ENCODE(Track_Struct); + int EntryCount = __packet->size / sizeof(Track_Struct); + + if (EntryCount == 0 || (__packet->size % sizeof(Track_Struct)) != 0) { + LogNetcode("[STRUCTS] Wrong size on outbound [{}]: Got [{}], expected multiple of [{}]", opcodes->EmuToName(__packet->GetOpcode()), __packet->size, sizeof(Track_Struct)); + delete __packet; + return; + } + + int PacketSize = 2; + for (int i = 0; i < EntryCount; ++i) + PacketSize += 13 + strlen(emu[i].name); + + ALLOC_LEN_ENCODE(PacketSize); + + char *Buffer = (char *)__packet->pBuffer; + VARSTRUCT_ENCODE_TYPE(uint16, Buffer, EntryCount); + for (int i = 0; i < EntryCount; ++i) { + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu[i].entityid); + VARSTRUCT_ENCODE_TYPE(float, Buffer, emu[i].distance); + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu[i].level); + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu[i].is_npc); + VARSTRUCT_ENCODE_STRING(Buffer, emu[i].name); + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu[i].is_pet); + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, emu[i].is_merc); + } + + FINISH_ENCODE(); + } + + ENCODE(OP_Weather) + { + SETUP_DIRECT_ENCODE(Weather_Struct, structs::Weather_Struct); + + OUT(val1); + OUT(type); + eq->unknown = 0; + OUT(mode); + + FINISH_ENCODE(); + } + ENCODE(OP_WearChange) { ENCODE_LENGTH_EXACT(WearChange_Struct); @@ -2965,6 +3225,60 @@ namespace TOB FINISH_ENCODE(); } + ENCODE(OP_WhoAllResponse) + { + SETUP_VAR_ENCODE(WhoAllReturnStruct); + + int Count = emu->playercount; + + // TOB client expects playercount in unknown44[0] and unknown52 = 0 + emu->unknown44[0] = Count; + emu->unknown52 = 0; + + ALLOC_LEN_ENCODE(__packet->size + (Count * 4)); + + char *InBuffer = (char *)__emu_buffer + sizeof(WhoAllReturnStruct); + char *OutBuffer = (char *)__packet->pBuffer; + + memcpy(OutBuffer, __emu_buffer, sizeof(WhoAllReturnStruct)); + OutBuffer += sizeof(WhoAllReturnStruct); + + for (int i = 0; i < Count; ++i) { + uint32 x; + + x = VARSTRUCT_DECODE_TYPE(uint32, InBuffer); + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, x); // FormatMSGID + + InBuffer += 4; // skip server PIDMSGID + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0); // PIDMSGID = 0 (no surname) + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, 0xffffffff); // extra uint32 before Name + + char Name[64]; + + VARSTRUCT_DECODE_STRING(Name, InBuffer); // Char Name + VARSTRUCT_ENCODE_STRING(OutBuffer, Name); + + x = VARSTRUCT_DECODE_TYPE(uint32, InBuffer); + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, x); // RankMSGID + + VARSTRUCT_DECODE_STRING(Name, InBuffer); // Guild Name + VARSTRUCT_ENCODE_STRING(OutBuffer, Name); + + for (int j = 0; j < 7; ++j) { + x = VARSTRUCT_DECODE_TYPE(uint32, InBuffer); + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, x); + } + + VARSTRUCT_DECODE_STRING(Name, InBuffer); // Account + VARSTRUCT_ENCODE_STRING(OutBuffer, Name); + + x = VARSTRUCT_DECODE_TYPE(uint32, InBuffer); + VARSTRUCT_ENCODE_TYPE(uint32, OutBuffer, x); // Unknown100 + } + + FINISH_ENCODE(); + } + ENCODE(OP_ZoneChange) { ENCODE_LENGTH_EXACT(ZoneChange_Struct); @@ -3479,7 +3793,7 @@ namespace TOB DECODE_LENGTH_EXACT(structs::ApplyPoison_Struct); SETUP_DIRECT_DECODE(ApplyPoison_Struct, structs::ApplyPoison_Struct); - emu->inventorySlot = TOBToServerTypelessSlot(eq->inventorySlot, invtype::typePossessions); + emu->inventorySlot = TOBToServerSlot(eq->inventorySlot); IN(success); FINISH_DIRECT_DECODE(); @@ -3494,7 +3808,13 @@ namespace TOB IN(race_id); IN(class_id); - // TODO: expand the approval logic to include the rest of the TOB struct values (and remove the direct translation here) + // TODO: expand approval logic — TOB wire format (84 bytes total): + // +0x00 char[64] name + // +0x40 uint32 race_id + // +0x44 uint32 class_id + // +0x48 uint32 deity_id (not in server NameApproval_Struct) + // +0x4c uint32 heroic_type (0–4; not in server struct) + // +0x50 uint32 unknown (always 0) FINISH_DIRECT_DECODE(); } @@ -3551,6 +3871,39 @@ namespace TOB emu->Initialise = init; } + DECODE(OP_BookButton) + { + DECODE_LENGTH_EXACT(structs::BookButton_Struct); + SETUP_DIRECT_DECODE(BookButton_Struct, structs::BookButton_Struct); + + emu->invslot = static_cast(TOBToServerTypelessSlot(eq->slot, invtype::typePossessions)); + IN(target_id); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_BuffDefinition) + { + DECODE_LENGTH_EXACT(structs::EQAffectPacket_Struct); + SETUP_DIRECT_DECODE(SpellBuffPacket_Struct, structs::EQAffectPacket_Struct); + + emu->entityid = eq->entity_id; + emu->buff.level = eq->affect.level; + emu->buff.bard_modifier = 0; + emu->buff.spellid = eq->affect.spell_id; + emu->buff.duration = eq->affect.duration; + emu->buff.counters = eq->affect.hit_count; + emu->buff.player_id = eq->affect.caster_id.Id; + emu->buff.num_hits = eq->affect.hit_count; + emu->buff.y = eq->affect.y; + emu->buff.x = eq->affect.x; + emu->buff.z = eq->affect.z; + emu->slotid = TOBToServerBuffSlot(static_cast(eq->slot_id)); + emu->bufffade = eq->buff_fade; + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_BuffRemoveRequest) { // This is to cater for the fact that short buff box buffs start at 30 as opposed to 25 in prior clients. @@ -3573,7 +3926,7 @@ namespace TOB emu->slot = static_cast(TOBToServerCastingSlot(static_cast(eq->slot))); IN(spell_id); - emu->inventoryslot = -1; + emu->inventoryslot = TOBToServerSlot(TOBCastingInventorySlotToInventorySlot(eq->inventory_slot)); IN(target_id); IN(y_pos); IN(x_pos); @@ -3615,7 +3968,7 @@ namespace TOB ChannelMessage_Struct* emu = (ChannelMessage_Struct*)__packet->pBuffer; strn0cpy(emu->targetname, Target, sizeof(emu->targetname)); - strn0cpy(emu->sender, Target, sizeof(emu->sender)); + strn0cpy(emu->sender, Sender, sizeof(emu->sender)); emu->language = Language; emu->chan_num = Channel; emu->skill_in_language = Skill; @@ -3652,7 +4005,32 @@ namespace TOB IN(CHA); IN(tutorial); - // TODO: can handle the heroic type here as well (new member) + // TODO: expand heroic character handling — TOB wire format (168 bytes total): + // +0x00 uint8[72] padding (zeroed) + // +0x48 uint32 gender + // +0x4c uint32 race + // +0x50 uint32 class_ + // +0x54 uint32 deity + // +0x58 uint32 start_zone + // +0x5c uint32 haircolor + // +0x60 uint32 beard + // +0x64 uint32 beardcolor + // +0x68 uint32 hairstyle + // +0x6c uint32 face + // +0x70 uint32 eyecolor1 + // +0x74 uint32 eyecolor2 + // +0x78 uint32 drakkin_heritage + // +0x7c uint32 drakkin_tattoo + // +0x80 uint32 drakkin_details + // +0x84 uint32 STR + // +0x88 uint32 STA + // +0x8c uint32 AGI + // +0x90 uint32 DEX + // +0x94 uint32 WIS + // +0x98 uint32 INT + // +0x9c uint32 CHA + // +0xa0 uint32 tutorial + // +0xa4 uint32 heroic_type (0=none, 1=lvl 85, 2=lvl 50, 3=lvl 100, 4=lvl 115; not in server CharCreate_Struct) FINISH_DIRECT_DECODE(); } @@ -3664,6 +4042,8 @@ namespace TOB IN(doorid); IN(player_id); + IN(item_id); + emu->picklockskill = static_cast(eq->picklockskill); FINISH_DIRECT_DECODE(); } @@ -3708,6 +4088,18 @@ namespace TOB DECODE(OP_ConsiderCorpse) { DECODE_FORWARD(OP_Consider); } + DECODE(OP_Consume) + { + DECODE_LENGTH_EXACT(structs::Consume_Struct); + SETUP_DIRECT_DECODE(Consume_Struct, structs::Consume_Struct); + + emu->slot = TOBToServerSlot(eq->slot); + emu->auto_consumed = (eq->mode == 1) ? 0x000003E7u : 0xFFFFFFFFu; + emu->type = static_cast(eq->type + 1); // TOB 0/1 → server 1/2 + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_CorpseDrag) { std::string CorpseName; @@ -3725,6 +4117,24 @@ namespace TOB strncpy(emu->DraggerName, DraggerName.c_str(), 64); } + DECODE(OP_Damage) + { + DECODE_LENGTH_EXACT(structs::CombatDamage_Struct); + SETUP_DIRECT_DECODE(CombatDamage_Struct, structs::CombatDamage_Struct); + + IN(target); + IN(source); + IN(type); + IN(spellid); + IN(damage); + IN(force); + IN(hit_heading); + IN(hit_pitch); + IN(special); + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_DeleteItem) { DECODE_LENGTH_EXACT(structs::DeleteItem_Struct); @@ -3743,12 +4153,34 @@ namespace TOB SETUP_DIRECT_DECODE(EnterWorld_Struct, structs::EnterWorld_Struct); memcpy(emu->name, eq->name, sizeof(emu->name)); + + // TODO: map TOB wire fields to server flags. + // TOB wire format (72 bytes total): + // +0x00 char[64] name + // +0x40 int32 unknown1 — 0 normally; 0x00010000 when re-entering after ZoneNotReady + // +0x44 int32 zoneID — EverQuest_EnterZoneReason: -1 = enter last zone (normal); + // other values = specific zone ID (tutorial zone? home city?) + // Server struct uses separate `tutorial` and `return_home` uint32 flags. + // Correct mapping requires knowing which zoneID values correspond to tutorial vs return_home. emu->return_home = 0; emu->tutorial = 0; FINISH_DIRECT_DECODE(); } + DECODE(OP_EnvDamage) + { + DECODE_LENGTH_EXACT(structs::EnvDamage2_Struct); + SETUP_DIRECT_DECODE(EnvDamage2_Struct, structs::EnvDamage2_Struct); + + emu->id = eq->entity_id; + emu->damage = static_cast(eq->damage); + IN(dmgtype); + emu->constant = 0xFFFF; + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_GMTraining) { DECODE_LENGTH_EXACT(structs::GMTrainee_Struct); @@ -3849,6 +4281,49 @@ namespace TOB FINISH_DIRECT_DECODE(); } + DECODE(OP_PickPocket) + { + DECODE_LENGTH_ATLEAST(structs::PickPocket_Struct); + SETUP_DIRECT_DECODE(PickPocket_Struct, structs::PickPocket_Struct); + + emu->to = eq->to; + emu->from = eq->from; + emu->myskill = static_cast(eq->myskill); + emu->type = eq->type; + emu->coin = eq->coin; + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_ReadBook) + { + // Client always sends 8216 bytes (struct is 8215); ATLEAST accepts the extra trailing byte. + DECODE_LENGTH_ATLEAST(structs::BookRequest_Struct); + SETUP_DIRECT_DECODE(BookRequest_Struct, structs::BookRequest_Struct); + + IN(type); + emu->invslot = static_cast(TOBToServerTypelessSlot(eq->invslot, invtype::typePossessions)); + IN(target_id); + emu->window = (uint8)eq->window; + strn0cpy(emu->txtfile, eq->txtfile, sizeof(emu->txtfile)); + + FINISH_DIRECT_DECODE(); + } + + DECODE(OP_RecipeAutoCombine) + { + DECODE_LENGTH_EXACT(structs::RecipeAutoCombine_CS_Struct); + SETUP_DIRECT_DECODE(RecipeAutoCombine_Struct, structs::RecipeAutoCombine_CS_Struct); + + IN(object_type); // eq[+32] → emu[+0] + IN(some_id); // eq[+36] → emu[+4] + emu->unknown1 = TOBToServerSlot(eq->container_slot); // eq[+20] → emu[+8] + IN(recipe_id); // eq[+4] → emu[+12] + emu->reply_code = 0; // junk in client request; server overwrites in reply + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_RemoveBlockedBuffs) { DECODE_FORWARD(OP_BlockedBuffs); } DECODE(OP_SetServerFilter) @@ -3939,6 +4414,26 @@ namespace TOB FINISH_DIRECT_DECODE(); } + DECODE(OP_WhoAllRequest) + { + DECODE_LENGTH_EXACT(structs::Who_All_Struct); + SETUP_DIRECT_DECODE(Who_All_Struct, structs::Who_All_Struct); + + memcpy(emu->whom, eq->whom, sizeof(emu->whom)); + IN(wrace); + IN(wclass); + IN(lvllow); + IN(lvlhigh); + IN(gmlookup); + // TOB splits RoF2's combined guildid field into a flag (0x94) and actual ID (0x98). + // When guildid is non-zero it is a real guild ID; otherwise fall back to guildid_flag + // which carries 0xFFFFFFFF (no filter) or trader/buyer sentinel values. + emu->guildid = eq->guildid ? eq->guildid : eq->guildid_flag; + IN(type); + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_ZoneEntry) { DECODE_LENGTH_EXACT(structs::ClientZoneEntry_Struct); @@ -5448,6 +5943,8 @@ namespace TOB return item::ItemPacketType::ItemPacketGuildTribute; case ItemPacketType::ItemPacketCharmUpdate: return item::ItemPacketType::ItemPacketCharmUpdate; + case ItemPacketType::ItemPacketParcel: + return item::ItemPacketType::ItemPacketParcel; default: return item::ItemPacketType::ItemPacketInvalid; } @@ -5611,6 +6108,11 @@ void MessageComponent::ResolveArguments(uint32_t id, std::array& } } +// TODO: verify that zone/client.cpp's raw FormattedMessage_Struct path is unreachable for TOB +// clients. That path builds the packet using the server struct layout (3x uint32 header + +// null-terminated strings), which is incompatible with the TOB wire format serialized below. +// There is no ENCODE(OP_FormattedMessage) in tob.cpp to fix it up, so if that path is reachable +// it would deliver a malformed packet. std::unique_ptr MessageComponent::Formatted(uint32_t color, uint32_t id, const FormattedArgs& args) const { diff --git a/common/patches/tob_ops.h b/common/patches/tob_ops.h index 9384162c1..c32c15094 100644 --- a/common/patches/tob_ops.h +++ b/common/patches/tob_ops.h @@ -12,6 +12,7 @@ E(OP_CastSpell) E(OP_ChannelMessage) E(OP_CharacterCreateRequest) E(OP_CharInventory) +E(OP_Charm) E(OP_ClickObjectAction) E(OP_ClientUpdate) E(OP_Consider) @@ -20,16 +21,24 @@ E(OP_Death) E(OP_DeleteCharge) E(OP_DeleteItem) E(OP_DeleteSpawn) +E(OP_DisciplineTimer) E(OP_DisciplineUpdate) E(OP_ExpansionInfo) E(OP_ExpUpdate) +E(OP_Fling) E(OP_GMTraining) E(OP_GMTrainSkillConfirm) E(OP_GroundSpawn) +E(OP_GroupInvite) E(OP_HPUpdate) E(OP_Illusion) +E(OP_IncreaseStats) E(OP_ItemPacket) +E(OP_ItemRecastDelay) +E(OP_ItemVerifyReply) +E(OP_LinkedReuse) E(OP_LogServer) +E(OP_LootItem) E(OP_ManaChange) E(OP_MemorizeSpell) E(OP_MobHealth) @@ -38,7 +47,9 @@ E(OP_MoveItem) E(OP_NewSpawn) E(OP_NewZone) E(OP_OnLevelMessage) +E(OP_PickPocket) E(OP_PlayerProfile) +E(OP_ReadBook) E(OP_RemoveBlockedBuffs) E(OP_RespondAA) E(OP_RequestClientZoneChange) @@ -57,7 +68,10 @@ E(OP_SpecialMesg) E(OP_SpawnAppearance) E(OP_SpawnDoor) E(OP_Stun) +E(OP_Track) E(OP_WearChange) +E(OP_Weather) +E(OP_WhoAllResponse) E(OP_ZoneChange) E(OP_ZoneEntry) E(OP_ZonePlayerToBind) @@ -70,6 +84,8 @@ D(OP_ApproveName) D(OP_AugmentInfo) D(OP_AugmentItem) D(OP_BlockedBuffs) +D(OP_BookButton) +D(OP_BuffDefinition) D(OP_BuffRemoveRequest) D(OP_CastSpell) D(OP_ChannelMessage) @@ -78,16 +94,23 @@ D(OP_ClientUpdate) D(OP_ClickDoor) D(OP_Consider) D(OP_ConsiderCorpse) +D(OP_Consume) D(OP_CorpseDrag) +D(OP_Damage) D(OP_DeleteItem) D(OP_EnterWorld) +D(OP_EnvDamage) D(OP_GMTraining) D(OP_GroupDisband) D(OP_GroupInvite) D(OP_GroupInvite2) +D(OP_ItemVerifyRequest) D(OP_LootItem) D(OP_MemorizeSpell) D(OP_MoveItem) +D(OP_PickPocket) +D(OP_ReadBook) +D(OP_RecipeAutoCombine) D(OP_RemoveBlockedBuffs) D(OP_SetServerFilter) D(OP_ShopPlayerBuy) @@ -96,6 +119,7 @@ D(OP_ShopRequest) D(OP_SpawnAppearance) D(OP_TradeSkillCombine) D(OP_WearChange) +D(OP_WhoAllRequest) D(OP_ZoneEntry) D(OP_ZoneChange) #undef E diff --git a/common/patches/tob_structs.h b/common/patches/tob_structs.h index cf5c6d8f3..6b560a5c3 100644 --- a/common/patches/tob_structs.h +++ b/common/patches/tob_structs.h @@ -509,6 +509,14 @@ namespace TOB { /*176*/ }; + struct Weather_Struct { + /*000*/ uint32 val1; // 0xFF = clear weather + /*004*/ uint32 type; // 0x31=rain, 0x02=snow, 0=normal + /*008*/ uint32 unknown; // TOB wire padding; client skips this offset + /*012*/ uint32 mode; // server struct's mode field, shifted to +0x0C on TOB + /*016*/ + }; + struct WearChange_Struct { /*000*/ uint32 spawn_id; /*004*/ uint32 wear_slot_id; @@ -521,6 +529,22 @@ namespace TOB { /*032*/ }; + struct Who_All_Struct { + /*000*/ char whom[64]; + /*064*/ uint8 unknown064[64]; // always zero + /*128*/ uint32 wrace; // 0xFFFFFFFF = any race + /*132*/ uint32 wclass; // 0xFFFFFFFF = any class + /*136*/ uint32 lvllow; // 0xFFFFFFFF = any level + /*140*/ uint32 lvlhigh; // 0xFFFFFFFF = any level + /*144*/ uint32 gmlookup; // 0xFFFFFFFF = not filtering by GM + /*148*/ uint32 guildid_flag; // 0xFFFFFFFF = no special filter; 0x7FC00000 = guild/trader/buyer active + /*152*/ uint32 guildid; // actual guild ID for /who guild, else 0 + /*156*/ uint32 unknown09C; // always 0 + /*160*/ uint32 type; // 0 = /who, 3 = /who all + /*164*/ uint8 unknown0A4[12]; // padding + /*176*/ + }; + struct ExpUpdate_Struct { /*000*/ uint64 exp; // This is exp % / 1000 now; eg 69250 = 69.25% @@ -530,7 +554,7 @@ namespace TOB { struct DeleteSpawn_Struct { /*00*/ uint32 spawn_id; // Spawn ID to delete - /*04*/ uint8 unknown04; // Seen 1 + /*04*/ uint8 Decay; // Seen 1 /*05*/ }; @@ -553,14 +577,13 @@ namespace TOB { // Was new for RoF2 - Used for Merchant_Purchase_Struct, doesn't look changed // Can't sellfrom other than main inventory so Slot Type is not needed. - // The padding is because these structs are padded to the default 4 bytes + // There is in general no padding for this, but sometimes a pad will be added struct TypelessInventorySlot_Struct { /*000*/ int16 Slot; /*002*/ int16 SubIndex; /*004*/ int16 AugIndex; - /*006*/ int16 Padding; - /*008*/ + /*006*/ }; struct Consider_Struct { @@ -576,6 +599,16 @@ namespace TOB { /*024*/ }; + struct Consume_Struct + { + /*000*/ InventorySlot_Struct slot; // ItemGlobalIndex: Type(4)+Slot(2)+SubIndex(2)+AugIndex(2)+Pad(2) + /*012*/ uint32 unknown; // always 0xFFFFFFFF on wire + /*016*/ uint8 type; // 0=Food, 1=Water (server expects 1=Food, 2=Water) + /*017*/ uint8 mode; // 0=auto-consume, 1=right-click + /*018*/ uint8 pad[2]; + /*020*/ + }; + struct SpawnHPUpdate_Struct { /*00*/ int16 spawn_id; @@ -587,8 +620,8 @@ namespace TOB { struct ClickDoor_Struct { /*00*/ uint16 player_id; /*02*/ uint8 padding1[2]; - /*04*/ int32 unknown1; - /*08*/ int32 unknown2; + /*04*/ uint32 item_id; + /*08*/ uint32 picklockskill; /*12*/ uint8 doorid; /*13*/ uint8 padding2[3]; }; @@ -599,7 +632,7 @@ namespace TOB { Rampage: 0x2 NoCastOnText: 0x4 DoubleBowShot: 0x8 - UnknownSpellFlag: 0x10 + UnknownSpellFlag: 0x10 // display flag of some sort, setting to 1 calls DisplayChatText Flurry: 0x20 Riposte: 0x40 Critical: 0x80 @@ -613,6 +646,9 @@ namespace TOB { Strikethrough: 0x8000 LuckyRiposte: 0x10000 Twincast: 0x20000 + ShieldBlock: 0x40000 + StaffBlock: 0x80000 + Locked: 0x100000 Might be more flags beyond this but I'm not sure */ @@ -620,20 +656,37 @@ namespace TOB { { /*000*/ uint16 target; /*002*/ uint16 source; - /*004*/ uint32 unknown1; //not read by the client + /*004*/ uint32 unknown1; // not read by the client /*008*/ int64 damage; - /*016*/ uint32 special; //flags; will document above + /*016*/ uint32 special; // flags; will document above /*020*/ int32 spellid; - /*024*/ uint32 spell_level; //spell caster level (unconfirmed; it is used for the spell link) - /*028*/ float force; //I haven't actually been able to confirm these three yet + /*024*/ uint32 spell_level; // spell caster level (unconfirmed; it is used for the spell link) + /*028*/ float force; /*032*/ float hit_heading; - /*036*/ int32 hit_pitch; - /*040*/ uint8 type; - /*041*/ uint8 padding[3]; - /*044*/ uint32 unknown2; //not read by the client + /*036*/ float hit_pitch; + /*040*/ uint8 type; // skill + /*041*/ uint8 isoffhand; // used for determining skill used for message + /*042*/ uint8 padding[2]; + /*044*/ uint32 unknown2; // not read by the client /*048*/ }; + struct EnvDamage2_Struct { + /*0000*/ uint16 entity_id; // spawn ID of entity taking damage (self) + /*0002*/ uint16 unknown2; // source/attacker spawn_id; 0 for environmental + /*0004*/ int32 spell_id; // spell causing damage; -1 or 0 = none + /*0008*/ uint64 unknown8; + /*0010*/ int64 damage; // damage amount + /*0018*/ uint64 unknown18; + /*0020*/ uint64 unknown20; + /*0028*/ float unknown28; + /*002C*/ uint32 unknown2C; + /*0030*/ uint32 unknown30; + /*0034*/ uint32 unknown34; + /*0038*/ uint8 dmgtype; // 0xFA=Lava, 0xFB=Drowning, 0xFC=Falling, 0xFD=Trap + /*0039*/ uint8 unknown39; + }; + struct Animation_Struct { /*00*/ uint16 spawnid; /*02*/ uint8 action; @@ -671,6 +724,31 @@ namespace TOB { /*0028*/ }; + struct ItemRecastDelay_Struct + { + /*000*/ InventorySlot_Struct item_slot; // zeroed until server struct gains item slot fields + /*012*/ uint32 recast_delay; // seconds until item can be used again + /*016*/ uint32 recast_type; // recast group (1..99); SetCoreItemRecastTimer + /*020*/ + }; + + struct ItemVerifyRequest_Struct + { + /*000*/ InventorySlot_Struct inventory_slot; // ItemGlobalIndex: Type(+0 int32) Slot(+4) SubIndex(+6) AugIndex(+8) Pad(+10) + /*012*/ uint32 target; // Target entity ID (g_pTargetPlayer+0x168), or 0 + /*016*/ + }; + + struct ItemVerifyReply_Struct + { + /*000*/ int32 slot; // Right-clicked slot (passed as ItemGlobalIndex* to GetItemByGlobalIndex) + /*004*/ uint32 spell; // Spell ID; client reads lower 16 bits; 0x407 triggers autobook-scribe + /*008*/ uint32 target; // Target Entity ID + /*012*/ int32 unknown0; // Exit gate: handler skips if < 0; send 0 + /*016*/ int32 recast_time; // fasttime() timestamp for autobook-scribe path (spell==0x407); send 0 + /*020*/ + }; + struct MerchantClickRequest_Struct { /*000*/ uint32 npc_id; // Merchant NPC's entity id @@ -700,6 +778,30 @@ namespace TOB { /*015*/ }; + // Used for OP_ReadBook in both directions (S→C text display, C→S content request). + // IDA-confirmed layout: type+target_id precede invslot (differs from RoF2 ordering). + struct BookRequest_Struct + { + /*0000*/ uint32 window; // 0xFFFFFFFF = new window; maps from emu->window (0xFF) + /*0004*/ uint32 type; // 0=note/scroll, 1=book, 2=item info + /*0008*/ uint32 target_id; + /*0012*/ TypelessInventorySlot_Struct invslot; + /*0018*/ uint8 padding; + /*0019*/ uint8 can_cast; + /*0020*/ uint8 can_scribe; // book-path cast button; note-path scribe button + /*0021*/ char txtfile[8194]; // null-terminated text / book file name + /*8215*/ + }; + + struct BookButton_Struct + { + /*000*/ TypelessInventorySlot_Struct slot; // book ItemIndex + /*006*/ int16 unknown2; // zero padding + /*008*/ int32 target_id; // spawn_id of target player or 0 + /*012*/ int32 unknown3; // zero padding + /*016*/ + }; + struct MemorizeSpell_Struct { uint32 slot; // Spot in the spell book/memorized slot int32 spell_id; // Spell id (200 or c8 is minor healing, etc) @@ -707,6 +809,13 @@ namespace TOB { uint32 reduction; // lower reuse (only used if scribing is 4) }; + struct LinkedSpellReuseTimer_Struct { + uint32 timer_id; // +0x00 linked spell group index (0–24) + uint32 unknown; // +0x04 extra DWORD present in TOB/TDS+ clients + uint32 end_time; // +0x08 absolute time when spell group is ready + uint32 start_time; // +0x0C server send timestamp (for client latency correction) + }; + //I've observed 5 s16 that are all -1. //Clicky items don't even trigger this as far as i can tell so not sure what this is for now. //One of these could have changed to a s32 but im not sure. @@ -733,6 +842,15 @@ namespace TOB { /*39*/ }; + struct Charm_Struct + { + /*00*/ uint32 owner_id; + /*04*/ uint32 pet_id; + /*08*/ uint32 charmer_id; // TOB-only field not present in server Charm_Struct; purpose unknown (passed to sub_1402FA570 when non-null); set to 0 + /*0C*/ uint8 command; // 1=make pet, 0=release pet; server sends this as uint32 at +0x08 + /*0D*/ + }; + struct InterruptCast_Struct { uint32 spawnid; @@ -920,6 +1038,13 @@ namespace TOB { /*332*/ }; + struct IncreaseStat_Struct { + /*000*/ uint32 spawn_id; // must equal g_pLocalPlayer->SpawnID; not in server struct (see encoder TODO) + /*004*/ uint32 stat_type; // 0=STR 1=STA 2=AGI 3=DEX 4=INT 5=WIS 6=CHA; client ignores > 6 + /*008*/ uint32 value; // must be > 0 to be applied + /*012*/ + }; + struct moneyOnCorpseStruct { /*000*/ uint8 type; // 0 = someone is already looting, 1 = OK, 2 = cannot access at this time, 3 = OK, 4 = cannot loot while hostile nearby, 5 = too far away to loot, 6 = loot all, 7 = cancel loot, 8 = add access, 9 = using advloot (when right clicking), 10 = show advloot /*001*/ uint8 padding1[3]; @@ -950,7 +1075,21 @@ namespace TOB { /*0144*/ uint32 unknown0144; /*0148*/ uint32 unknown0148; /*0152*/ uint16 unknown0152; - /*0154*/ + /*0154*/ uint8 unknown0154[14]; + /*0168*/ + }; + + // TOB pick pocket wire format (S→C and C→S identical layout). + // coin sits at unaligned offset 0x0D; valid under pack(1). + struct PickPocket_Struct { + /*0x00*/ uint32 to; + /*0x04*/ uint32 from; + /*0x08*/ uint32 myskill; + /*0x0C*/ uint8 type; + /*0x0D*/ uint32 coin; + /*0x11*/ uint32 nameLen; + // char name[nameLen] follows, then uint8 luckily + /*0x15*/ }; struct AugmentInfo_Struct @@ -978,7 +1117,7 @@ namespace TOB { struct ApplyPoison_Struct { - TypelessInventorySlot_Struct inventorySlot; + InventorySlot_Struct inventorySlot; uint32 success; }; @@ -1001,21 +1140,42 @@ namespace TOB { /*92*/ }; - //received and sent back as an ACK with different reply_code + // Server→Client (38 bytes): container_slot serialized without Padding2 to match client deserializer struct RecipeAutoCombine_Struct { /*00*/ uint32 object_type; /*04*/ uint32 some_id; - /*08*/ InventorySlot_Struct container_slot; //echoed in reply - Was uint32 unknown1 - /*20*/ InventorySlot_Struct unknown_slot; //echoed in reply - /*32*/ uint32 recipe_id; - /*36*/ uint32 reply_code; - /*40*/ + /*08*/ int32 container_type; // InventorySlot.Type + /*12*/ int16 container_slot_index; // InventorySlot.Slot + /*14*/ int16 container_subindex; // InventorySlot.SubIndex + /*16*/ int16 container_augindex; // InventorySlot.AugIndex + /*18*/ InventorySlot_Struct unknown_slot; // 12 bytes; echoed in reply + /*30*/ uint32 recipe_id; + /*34*/ uint32 reply_code; + /*38*/ + }; + + // Client→Server (56 bytes): layout from CTradeskillWnd::HandleCombine + struct RecipeAutoCombine_CS_Struct { + /*00*/ uint32 con_type; // GetConType of container + /*04*/ uint32 recipe_id; // GetItemRecordNum of recipe item + /*08*/ uint32 unknown1; // constant 4 + /*12*/ uint8 unknown2[8]; // zeros (uninitialized) + /*20*/ InventorySlot_Struct container_slot; // 12 bytes + /*32*/ uint32 object_type; + /*36*/ uint32 some_id; + /*40*/ uint8 flag1; + /*41*/ uint8 flag2; + /*42*/ uint8 unknown3; + /*43*/ uint8 flag3; + /*44*/ uint32 unknown4; // zeros (uninitialized) + /*48*/ int64 start_tick; + /*56*/ }; /* ** New Combine Struct ** Client requesting to perform a tradeskill combine - ** Size: 24 bytes + ** Size: 28 bytes ** Used In: OP_TradeSkillCombine ** Last Updated: 01-05-2013 */ @@ -1023,7 +1183,15 @@ namespace TOB { { /*00*/ InventorySlot_Struct container_slot; /*12*/ InventorySlot_Struct guildtribute_slot; // Slot type is 8? (MapGuildTribute = 8) - /*24*/ + /*24*/ uint32 unknown0x18; // TOB wire format; not used by server + /*28*/ + }; + + struct DisciplineTimer_Struct { + /*00*/ uint32 TimerID; + /*04*/ uint32 Duration; + /*08*/ uint32 Unknown08; // server-side absolute expiry time (fasttime units) + /*0C*/ uint32 ServerTime; // server's current time when packet sent (fasttime units) }; struct Disciplines_Struct { @@ -1032,11 +1200,12 @@ namespace TOB { struct Merchant_Sell_Request_Struct { /*000*/ uint32 npcid; // Merchant NPC's entity id - /*004*/ uint32 playerid; // Player's entity id - /*008*/ uint32 itemslot; // Merchant Slot / Item Instance ID - /*012*/ uint32 unknown12; + /*004*/ uint32 playerid; // unset by client (stack garbage at this offset) + /*008*/ uint32 itemslot; // lower 4 bytes of 8-byte item instance ID + /*012*/ uint32 unknown12; // upper 4 bytes of 8-byte item instance ID /*016*/ uint32 quantity; // Already sold - /*020*/ + /*020*/ uint32 unknown20; // unset by client — trailing pad; client sends 24 bytes total + /*024*/ }; struct Merchant_Sell_Response_Struct { @@ -1054,17 +1223,18 @@ namespace TOB { struct Merchant_Purchase_Request_Struct { /*000*/ uint32 npcid; // Merchant NPC's entity id /*004*/ TypelessInventorySlot_Struct inventory_slot; + /*010*/ int16 padding; /*012*/ uint32 quantity; /*016*/ }; struct Merchant_Purchase_Response_Struct { - /*000*/ uint32 npcid; // Merchant NPC's entity id - /*004*/ TypelessInventorySlot_Struct inventory_slot; - /*012*/ uint32 quantity; - /*016*/ uint32 price; - /*020*/ uint32 unknown020; - /*024*/ + /*000*/ TypelessInventorySlot_Struct inventory_slot; + /*006*/ int16 padding; + /*008*/ uint32 quantity; + /*012*/ uint32 price; + /*016*/ uint32 unknown016; + /*020*/ }; /* @@ -1107,6 +1277,20 @@ namespace TOB { uint32 Zone; }; + struct fling_struct { + /* 00 */ float speed_z; // must be > 0 for handler to proceed + /* 04 */ float new_y; + /* 08 */ float new_x; + /* 12 */ float new_z; + /* 16 */ float radius; // landing zone radius; 0.0f = land exactly at target + /* 20 */ uint32 unknown; // not accessed; padding + /* 24 */ int32 travel_time; // -1 = auto-calc; 0 = default 1000ms; >0 = explicit ms + /* 28 */ uint8 collision; // 0 = disable collision; non-zero = keep collision + /* 29 */ uint8 fall_damage; // 0 = no fall damage (player.408=1); non-zero = take damage + /* 30 */ uint8 z_override; // 1 = override z-target comparison + /* 31 */ + }; + struct BeggingResponse_Struct { /*00*/ uint32 Unknown00; diff --git a/tob/opcodes.md b/tob/opcodes.md index eb34f557c..9c5d8454f 100644 --- a/tob/opcodes.md +++ b/tob/opcodes.md @@ -1,6 +1,7 @@ ### Status Below is a status list for the 450 opcodes we currently use on the server for the TOB client. Currently uses 3 status levels (let me know if we should do more): - 🔴 Not-Set (Opcode not set in the patch file) +- 🟠 Missing (Opcode is 0x0000 in TOB patch but has a known value in RoF2 — needs investigation) - 🟡 Unverified (Opcode set but structure hasn't been verified as completely working) - 🟢 Verified (Opcode set and structure is working) @@ -8,329 +9,333 @@ Below is a status list for the 450 opcodes we currently use on the server for th | Opcode | Status | Notes | Working On | |:----------------------------------|:--------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------| -| `OP_AAAction` | 🟡 Unverified | | | -| `OP_AAExpUpdate` | 🟢 Verified | | | -| `OP_AcceptNewTask` | 🔴 Not-Set | | | -| `OP_AckPacket` | 🟢 Verified | | | -| `OP_Action` | 🟢 Verified | | | +| `OP_AAAction` | 🟢 Verified | Client→Server only. 16-byte AA_Action struct (action/ability/target_id/exp_value) matches exactly across all 4 send sites. Passthrough correct. action=7 (set restore) and action=10 (shift-buy) unhandled by emu server by design. | | +| `OP_AAExpUpdate` | 🟢 Verified | Server→Client only. Handler: `msg_altexpup` @ `0x1402087b0`. Dispatched via `cmp [mem], 4C3h` @ `0x1401e4219` (memory-form, not switch table). Encoder correctly scales experience ×100000/330 and zero-extends unspent uint16→uint32. All 3 fields confirmed from raw ASM. | | +| `OP_AcceptNewTask` | 🟠 Missing | | | +| `OP_AckPacket` | 🟢 Verified | Client→Server only. Sent from `CDisplay::SetViewActor` @ `0x1401a0b40` (hton @ `0x1401a0c9d`). 4-byte uint32 payload = spawn ID of newly viewed actor (0 if local player or no match). Server ignores payload on both zone and world paths. No encoder/decoder needed. Also used as `signature.ignore_eq_opcode` in zone-in detection. | | +| `OP_Action` | 🟢 Verified | Server→Client only. Dispatched via `cmp [mem], 7D28h` @ `0x1401f4245`. Inline case block: constructs CUnSerializeBuffer, calls MissileHitInfo::Deserialize @ `0x1402024C0`. Encoder correct: spell/effect_flag widened, instrument_mod→float, effective_casting_level/unknown1/damage hardcoded to 0 (intentional). Field offsets confirmed from case block and init function. | | | `OP_Action2` | 🔴 Not-Set | | | -| `OP_AddNimbusEffect` | 🟡 Unverified | | | -| `OP_AdventureData` | 🔴 Not-Set | | | -| `OP_AdventureDetails` | 🔴 Not-Set | | | -| `OP_AdventureFinish` | 🔴 Not-Set | | | -| `OP_AdventureInfo` | 🔴 Not-Set | | | -| `OP_AdventureInfoRequest` | 🔴 Not-Set | | | -| `OP_AdventureLeaderboardReply` | 🔴 Not-Set | | | -| `OP_AdventureLeaderboardRequest` | 🔴 Not-Set | | | -| `OP_AdventureMerchantPurchase` | 🔴 Not-Set | | | -| `OP_AdventureMerchantRequest` | 🔴 Not-Set | | | -| `OP_AdventureMerchantResponse` | 🔴 Not-Set | | | -| `OP_AdventureMerchantSell` | 🔴 Not-Set | | | -| `OP_AdventurePointsUpdate` | 🔴 Not-Set | | | -| `OP_AdventureRequest` | 🔴 Not-Set | | | -| `OP_AdventureStatsReply` | 🔴 Not-Set | | | -| `OP_AdventureStatsRequest` | 🔴 Not-Set | | | -| `OP_AdventureUpdate` | 🔴 Not-Set | | | -| `OP_AggroMeterLockTarget` | 🔴 Not-Set | | | -| `OP_AggroMeterTargetInfo` | 🔴 Not-Set | | | -| `OP_AggroMeterUpdate` | 🔴 Not-Set | | | -| `OP_AltCurrency` | 🔴 Not-Set | | | -| `OP_AltCurrencyMerchantReply` | 🔴 Not-Set | | | -| `OP_AltCurrencyMerchantRequest` | 🔴 Not-Set | | | -| `OP_AltCurrencyPurchase` | 🔴 Not-Set | | | -| `OP_AltCurrencyReclaim` | 🔴 Not-Set | | | -| `OP_AltCurrencySell` | 🔴 Not-Set | | | -| `OP_AltCurrencySellSelection` | 🔴 Not-Set | | | -| `OP_Animation` | 🟢 Verified | | | +| `OP_AddNimbusEffect` | 🟢 Verified | Server→Client only. Dispatched via `cmp @ 0x1401F3C89` → case block `0x1401F8C36`. Case block extracts spawnid [+0] and nimbus_effect [+4] as dwords, calls `ActivateStateFlavorSpellParticleEffect` @ `0x1401E2060`. Wire layout matches `RemoveNimbusEffect_Struct` exactly. Passthrough correct. | | +| `OP_AdventureData` | 🟠 Missing | | | +| `OP_AdventureDetails` | 🟠 Missing | | | +| `OP_AdventureFinish` | 🟠 Missing | | | +| `OP_AdventureInfo` | 🟠 Missing | | | +| `OP_AdventureInfoRequest` | 🟠 Missing | | | +| `OP_AdventureLeaderboardReply` | 🟠 Missing | | | +| `OP_AdventureLeaderboardRequest` | 🟠 Missing | | | +| `OP_AdventureMerchantPurchase` | 🟠 Missing | | | +| `OP_AdventureMerchantRequest` | 🟠 Missing | | | +| `OP_AdventureMerchantResponse` | 🟠 Missing | | | +| `OP_AdventureMerchantSell` | 🟠 Missing | | | +| `OP_AdventurePointsUpdate` | 🟠 Missing | | | +| `OP_AdventureRequest` | 🟠 Missing | | | +| `OP_AdventureStatsReply` | 🟠 Missing | | | +| `OP_AdventureStatsRequest` | 🟠 Missing | | | +| `OP_AdventureUpdate` | 🟠 Missing | | | +| `OP_AggroMeterLockTarget` | 🟠 Missing | | | +| `OP_AggroMeterTargetInfo` | 🟠 Missing | | | +| `OP_AggroMeterUpdate` | 🟠 Missing | | | +| `OP_AltCurrency` | 🟠 Missing | | | +| `OP_AltCurrencyMerchantReply` | 🟠 Missing | | | +| `OP_AltCurrencyMerchantRequest` | 🟠 Missing | | | +| `OP_AltCurrencyPurchase` | 🟠 Missing | | | +| `OP_AltCurrencyReclaim` | 🟠 Missing | | | +| `OP_AltCurrencySell` | 🟠 Missing | | | +| `OP_AltCurrencySellSelection` | 🟠 Missing | | | +| `OP_Animation` | 🟢 Verified | Inline case block @ `0x1401f7ab2` (cmp-dispatch). 4-byte struct verified field-by-field. Encoder/decoder correctly handle `action`/`speed` field-order swap between server and TOB structs. | | | `OP_AnnoyingZoneUnknown` | 🔴 Not-Set | | | -| `OP_ApplyPoison` | 🟡 Unverified | | | -| `OP_ApproveName` | 🟡 Unverified | This takes multiple parameters from the client, and it can take multiple integer values from the server | | -| `OP_ApproveWorld` | 🔴 Not-Set | | | +| `OP_ApplyPoison` | 🟢 Verified | Handler `msg_apply_poison` @ `0x140208810`. Fixed: struct changed from `TypelessInventorySlot_Struct` (8B) to `InventorySlot_Struct` (12B); slot fns updated to `ServerToTOBSlot`/`TOBToServerSlot`. | | +| `OP_ApproveName` | 🟢 Verified | Bidirectional. S→C: 1-byte response code, `HandleNameApprovalResponse` @ `0x14039bff0` (26 cases). C→S: decoder maps name/race_id/class_id; deity_id+heroic_type dropped (TODO, enhancement only). | | +| `OP_ApproveWorld` | 🟠 Missing | | | | `OP_ApproveZone` | 🔴 Not-Set | | | -| `OP_Assist` | 🟡 Unverified | | | -| `OP_AssistGroup` | 🟡 Unverified | | | -| `OP_AugmentInfo` | 🟡 Unverified | | | -| `OP_AugmentItem` | 🟡 Unverified | | | -| `OP_AutoAttack` | 🟡 Unverified | | | -| `OP_AutoAttack2` | 🟡 Unverified | | | -| `OP_AutoFire` | 🟢 Verified | | | -| `OP_Bandolier` | 🔴 Not-Set | | | -| `OP_BankerChange` | 🟡 Unverified | | | -| `OP_Barter` | 🔴 Not-Set | | | +| `OP_Assist` | 🟢 Verified | Inline case block @ 0x1401EF76D. Reads entity_id (dword +0), sets g_pTargetPlayer; triggers SetAutoAttack if attack_on_assist set. No encoder/decoder needed. | | +| `OP_AssistGroup` | 🟢 Verified | Passthrough. EntityId_Struct (4B uint32 entity_id). S→C inline case @ 0x1401ef76d (Pattern B): lookup player, set target, optional auto-attack. C→S via do_assist() @ 0x140223710 (/assist group only). | | +| `OP_AugmentInfo` | 🟢 Verified | Bidirectional. Encoder: itemid/window/augment_info[64] ok; TOB-extra unknown072 (+0x48) and unknown076 (+0x4C) not set. S->C inline @ 0x1401EFB0D. C->S CItemDisplayWnd::SetItem @ 0x1404314E0. | | +| `OP_AugmentItem` | 🟢 Verified | C→S only. 4 send sites: InsertAugmentRequest, RemoveAugmentRequest, SwapAugmentRequest, DialogResponse. 40-byte TOB struct (InventorySlot_Struct slots) decoded correctly via TOBToServerSlot. | | +| `OP_AutoAttack` | 🟢 Verified | C→S only. No struct, no decoder needed. Client sends bAutoAttack as uint32 (0/1) in 4-byte payload from DoPassageOfTime@0x1400EC3BD; server reads pBuffer[0]. Passthrough correct. | | +| `OP_AutoAttack2` | 🟢 Verified | C→S only. Client sends uint32 auto-attack state (0/1) from DoPassageOfTime when bAutoAttack changes. Send @ 0x1400ec45f. No encoder/decoder needed; opcode_dispatch.h IN(uint32) correct. Handler stub. | | +| `OP_AutoFire` | 🟢 Verified | C→S only. No S→C handler (IDA showed fallback case). Client sends 6-byte packet (2B opcode + 4B uint32 payload) from DoPassageOfTime @ 0x1400ec65a. No encoder/decoder needed; passthrough correct. | | +| `OP_AvaliableTask` | 🟠 Missing | | | +| `OP_Bandolier` | 🟠 Missing | | | +| `OP_BankerChange` | 🟢 Verified | Inline case block @ 0x1401e6d98. Bank fields (0x10–0x1C) confirmed correct under passthrough; SetBankPlatinum/Gold/Silver/Copper called at matching offsets. No encoder needed. C→S: header-only click. | | +| `OP_Barter` | 🟠 Missing | | | | `OP_Bazaar` | 🔴 Not-Set | | | | `OP_BazaarInspect` | 🔴 Not-Set | | | -| `OP_BazaarSearch` | 🔴 Not-Set | | | +| `OP_BazaarSearch` | 🟠 Missing | | | | `OP_BecomeCorpse` | 🔴 Not-Set | | | -| `OP_BecomeTrader` | 🔴 Not-Set | | | -| `OP_Begging` | 🟢 Verified | | | -| `OP_BeginCast` | 🟢 Verified | | | -| `OP_Bind_Wound` | 🟡 Unverified | | | -| `OP_BlockedBuffs` | 🟢 Verified | | | -| `OP_BoardBoat` | 🟡 Unverified | | | -| `OP_BookButton` | 🟡 Unverified | | | -| `OP_BuffDefinition` | 🟢 Verified | | | -| `OP_BuffRemoveRequest` | 🟢 Verified | | | -| `OP_Bug` | 🟡 Unverified | | | -| `OP_BuyerItems` | 🔴 Not-Set | | | -| `OP_CameraEffect` | 🟡 Unverified | | | -| `OP_Camp` | 🟡 Unverified | | | -| `OP_CancelSneakHide` | 🟡 Unverified | | | -| `OP_CancelTask` | 🔴 Not-Set | | | -| `OP_CancelTrade` | 🟡 Unverified | | | -| `OP_CashReward` | 🟡 Unverified | | | -| `OP_CastSpell` | 🟢 Verified | | | -| `OP_ChangeSize` | 🟢 Verified | | | -| `OP_ChannelMessage` | 🟢 Verified | | | +| `OP_BecomeTrader` | 🟠 Missing | | | +| `OP_Begging` | 🟢 Verified | Encoder correct. Result cast uint32→uint8 (values 0–4). StringSize=0 and Lucky=0 hardcoded (no server struct fields). Handler sub_1402088C0 @ 0x1402088c0; cmp-dispatch at 0x1401f346a. No decoder (passthrough). | | +| `OP_BeginCast` | 🟢 Verified | Handler: CEverQuest::StartCasting@0x140293100. Direct 15-byte memcpy into local struct. TOB struct reorders fields (spell_id int32 first) vs server. Encoder correctly maps all fields; unknown0e hardcoded 1 (matches live). | | +| `OP_Bind_Wound` | 🟢 Verified | Passthrough via tob_ops.h. Client sends 8-byte BindWound_Struct (target field [r14+0x168] → to+unknown2, type+unknown6 = 0). S→C handler @ 0x1401ef3f3 (ja-dispatch, inline Pattern B). C→S send @ 0x140101c99 in sub_140101A90. Struct matches server struct exactly. | | +| `OP_BlockedBuffs` | 🟢 Verified | Bidirectional. Dynamic-length wire (uint32 count + int32[count] + uint8 Pet + uint8 Init). ENCODE/DECODE correct. Case block @ `0x1401f1dac`, Pattern D (inline CUnSerializeBuffer → sub_140202750). 4 C→S send sites. | | +| `OP_BoardBoat` | 🟢 Verified | C->S only. Passthrough. Client sends null-terminated boat name string (6-64 bytes). Send @ DoPassageOfTime 0x1400ebf30. Server reads pBuffer directly via memcpy. | | +| `OP_BookButton` | 🟢 Verified | C->S only. Added DECODE + TOB struct (16-byte wire: +0 int16 invslot, +4 int16 unknown, +8 int32 target_id). Was broken (size mismatch 16 vs 6); scribing from book window now functional. | | +| `OP_BuffDefinition` | 🟢 Verified | Bidirectional. S→C: EQAffectPacket_Struct (168B), inline case 0x1401e8994. C→S: added DECODE to tob.cpp/tob_ops.h mapping EQAffectPacket_Struct→SpellBuffPacket_Struct; fixes buff click-off size mismatch.| | +| `OP_BuffRemoveRequest` | 🟢 Verified | C→S only. Two send sites: `RemoveBuffEffect` @ `0x1402ec560` (char buffs), `RemovePetEffect` @ `0x1402ec9c0` (pet buffs). Decoder correctly applies `TOBToServerBuffSlot` on SlotID; EntityID passthrough. | | +| `OP_Bug` | 🟢 Verified | C→S passthrough: `sub_140391580` @ `0x140391580` sends exact `BugReport_Struct` (8740B); server size-checks before cast. S→C: case `0x1401f2d5e` (cmp/ja dispatch) reads one length-prefixed URL string via CUnSerializeBuffer; handler `sub_140391470` shows popup + LaunchWebPage; EQEmu never sends S→C so no encoder needed. | | +| `OP_BuyerItems` | 🟠 Missing | | | +| `OP_CameraEffect` | 🟢 Verified | S→C only. Passthrough — Camera_Struct (8B: +0 uint32 duration, +4 float intensity). Inline case 0x1401e8431 reads directly, calls CCameraShakeManager::AddShake @ 0x140537F10. | | +| `OP_Camp` | 🟢 Verified | Bidirectional, no encoder/decoder. C→S: empty (size 0) from `CEverQuest::Camp` @ `0x140267240`. S→C: 1 byte @ case `0x1401f9cc7` (cmp-dispatch); byte=0 starts countdown, byte≠0 cancels camp with AA warning. | | +| `OP_CancelSneakHide` | 🟢 Verified | S→C only. Zero-byte signal, no struct/encoder/decoder. Handler `CEverQuest::CancelSneakHide` @ `0x1402678e0` (cmp-dispatch @ `0x1401e8c86`). Client responds with OP_Hide (0x4F10) + OP_SpawnAppearance. | | +| `OP_CancelTask` | 🟠 Missing | | | +| `OP_CancelTrade` | 🟢 Verified | sub_140209C70 @ 0x140209c70 (cmp-dispatch). C→S field names inverted: fromid=trade target SpawnID, action=local SpawnID. No change needed. | | +| `OP_CashReward` | 🟢 Verified | Passthrough. `sub_140209DB0` @ `0x140209DB0` (Pattern A). 4×uint32 matches `CashReward_Struct`. | | +| `OP_CastSpell` | 🟢 Verified | C→S only (client ignores S→C). Send: `CharacterZoneClient::CastSpell` @ `0x1400d5350`, hton @ `0x1400d7e09`. Fixed decoder: `inventoryslot` now uses `TOBToServerSlot(TOBCastingInventorySlotToInventorySlot(eq->inventory_slot))`. | | +| `OP_ChangeSize` | 🟢 Verified | Inline Pattern B @ `0x1401f20d4`; calls `ChangeHeight` @ `0x14030ffc0`; bidirectional — client sends via `/changesize` GM cmd (`sub_140234D90` @ `0x140234d90`); passthrough correct, layouts match. | | +| `OP_ChannelMessage` | 🟢 Verified | Fixed decoder: `emu->sender` now uses `Sender` not `Target` (tob.cpp:3669). Encoder correct. Handler @ `0x1401f0875` (cmp-dispatch), UnSerialize @ `0x1406a6c70`. Pattern D (CommunicationManagerMessage). | | | `OP_ChangePetName` | 🔴 Not-Set | | | -| `OP_CharacterCreate` | 🟢 Verified | Sends heroic type, can be used for something? | | -| `OP_CharacterCreateRequest` | 🟢 Verified | | | -| `OP_CharInventory` | 🟢 Verified | | | -| `OP_Charm` | 🟡 Unverified | | | +| `OP_CharacterCreate` | 🟢 Verified | C→S only. Handler sub_14039B900 @ 0x14039b900 (738 bytes). TOB struct 168 bytes with 72-byte padding prefix. All 23 server fields verified correct. heroic_type at +0xa4 not in server struct (existing TODO). | | +| `OP_CharacterCreateRequest` | 🟢 Verified | Bidirectional. S→C: handler `HandleStartingLocationResponse` @ `0x14039c5a0`, cmp/jz dispatch @ `0x1401e5987`. Encoder widens `ExpansionRequired` uint32→uint64. C→S: empty trigger, no decoder needed. | | +| `OP_CharInventory` | 🟢 Verified | Handler: msgCompItems @ 0x1402051b0; shared with OP_BankItems; cmp-dispatch @ 0x1401e4679; client sends zero-byte ACK (no decoder needed) | | +| `OP_Charm` | 🟢 Verified | Handler: sub_140217E40 @ 0x140217E40; cmp-dispatch @ 0x1401e4a43; TOB wire 13B (owner_id,pet_id,charmer_id[TOB-only +0x08 set to 0],command[uint8 at +0x0C]); encoder added; release(0) was ok, make-pet(1) was broken | | | `OP_ChatMessage` | 🔴 Not-Set | | | -| `OP_ClearAA` | 🟢 Verified | | | -| `OP_ClearBlockedBuffs` | 🟢 Verified | | | -| `OP_ClearLeadershipAbilities` | 🔴 Not-Set | | | -| `OP_ClearNPCMarks` | 🔴 Not-Set | | | -| `OP_ClearObject` | 🟡 Unverified | | | -| `OP_ClearSurname` | 🔴 Not-Set | | | -| `OP_ClickDoor` | 🟡 Unverified | | | -| `OP_ClickObject` | 🟡 Unverified | | | -| `OP_ClickObjectAction` | 🟡 Unverified | | | +| `OP_ClearAA` | 🟢 Verified | Zero-payload trigger; case @ 0x1401E83CD calls CAltAdvManager::Reset() (0x1401A9DF0) with no packet fields read. Passthrough correct, no struct. | | +| `OP_ClearBlockedBuffs` | 🟢 Verified | Passthrough, no encode/decode. 1-byte packet: `uint8 pet`. C→S: `/blockspell clear`, hton@0x14023053a in sub_140230370. S→C: QueuePacket echo → msg_blocked_spells_clear@0x140209a40 clears INI slots. | | +| `OP_ClearLeadershipAbilities` | 🟠 Missing | | | +| `OP_ClearNPCMarks` | 🟠 Missing | | | +| `OP_ClearObject` | 🟢 Verified | Passthrough. Handler `msg_clear_world_con` @ `0x14020a520` (Pattern A). Server always sends Clear=1 → ClearWorldContainerItems(). Struct wire-compatible. | | +| `OP_ClearSurname` | 🟠 Missing | | | +| `OP_ClickDoor` | 🟢 Verified | C→S only. Send @ EQSwitch::UseSwitch 0x140260da0. Added item_id and picklockskill decoding; doorid and player_id were already correct. TOB struct unknowns renamed to match server fields. | | +| `OP_ClickObject` | 🟢 Verified | Bidirectional passthrough. C→S: LMouseUp @ 0x14027b4f0. S→C: inline Pattern B @ 0x1401f6e8e; body at loc_1401F6EAD reads drop_id(+0) as serial and player_id(+4) to gate pending-counter dec. | | +| `OP_ClickObjectAction` | 🟢 Verified | Handler `msg_world_container` @ `0x1402197A0`. Added `OUT(unknown24)` (ItemNumber). Slots hardcoded to 10 by client; `unknown16=0` OK. C→S opcode-only, no payload (camp cmd + close container). | | | `OP_ClientError` | 🔴 Not-Set | | | -| `OP_ClientReady` | 🟢 Verified | | | +| `OP_ClientReady` | 🟢 Verified | Zero-length bidirectional. S→C @ `0x1401F46BC` (cmp-dispatch); sets ReadyEnterWorld=1. C→S zero-payload. | | | `OP_ClientTimeStamp` | 🔴 Not-Set | | | -| `OP_ClientUpdate` | 🟢 Verified | | | +| `OP_ClientUpdate` | 🟢 Verified | Bidirectional. S→C: `ProcessUpdateStats` @ `0x140200010` (Pattern A); spawn_id, vehicle_id, 9 position fields all correct. C→S: 4 send sites in DoMainLoop/DoTeleportB; decoder correct; pitch not forwarded (no server field). | | | `OP_CloseContainer` | 🔴 Not-Set | | | | `OP_CloseTributeMaster` | 🔴 Not-Set | | | -| `OP_ColoredText` | 🟢 Verified | | | -| `OP_CombatAbility` | 🟡 Unverified | | | +| `OP_ColoredText` | 🟢 Verified | Passthrough; sub_140205060 @ 0x140205060; color @ +0x00, msg @ +0x04; newline-split, calls DisplayChatText | | +| `OP_CombatAbility` | 🟢 Verified | C→S only passthrough. Client sends CombatAbility_Struct (m_target/m_atk/m_skill, 12 bytes) unchanged. S→C hits HWM fallback — server never sends this. Send @ sub_140246750 (0x140246750). | | | `OP_Command` | 🔴 Not-Set | | | -| `OP_CompletedTasks` | 🔴 Not-Set | | | -| `OP_ConfirmDelete` | 🟡 Unverified | | | -| `OP_Consent` | 🟢 Verified | | | -| `OP_ConsentDeny` | 🟢 Verified | | | -| `OP_ConsentResponse` | 🟢 Verified | | | -| `OP_Consider` | 🟢 Verified | | | -| `OP_ConsiderCorpse` | 🟢 Verified | | | -| `OP_Consume` | 🟡 Unverified | | | -| `OP_ControlBoat` | 🟡 Unverified | | | -| `OP_CorpseDrag` | 🟢 Verified | | | -| `OP_CorpseDrop` | 🟢 Verified | | | +| `OP_CompletedTasks` | 🟠 Missing | | | +| `OP_ConfirmDelete` | 🟢 Verified | C→S only. Client sends 2-byte spawn_id from ProcessUpdateStats @ 0x140200010 when spawn not found locally. Server handler is a stub (returns immediately). Passthrough correct, no decoder needed. | | +| `OP_Consent` | 🟢 Verified | C→S only. Client sends null-terminated player name from sub_140221550 @ 0x140221550 (/consent cmd). No real S→C handler (fallback case). Passthrough correct, struct matches wire format. | | +| `OP_ConsentDeny` | 🟢 Verified | C→S only. `/deny` cmd. Send fn `sub_1402217E0` @ `0x1402217E0`. Passthrough — `Consent_Struct{char name[1]}` matches wire format. Not found in HandleWorldMessage (no S→C handler). | | +| `OP_ConsentResponse` | 🟢 Verified | Passthrough S→C. Handler `sub_14020A550` @ `0x14020A550` (Pattern A). All 4 struct fields verified: grantname+0x00, ownername+0x40, permission+0x80, zonename+0x81. No encoder/decoder needed. | | +| `OP_Consider` | 🟢 Verified | Bidirectional. S→C handler `CEverQuest::Consider` @ `0x1402687e0`; client reads only targetid (+4), ignores faction/level/HP — consider UI uses locally-cached player state. C→S send @ `0x14021faf0`; sends playerid+targetid only. Dead targets send OP_ConsiderCorpse from same fn. Encoder/decoder correct. | | +| `OP_ConsiderCorpse` | 🟢 Verified | C→S only. `sub_14021FAF0` @ `0x14021faf0`; hton @ `0x14021fe60`. Sent when target spawn-type ≥ 2 (corpse). Decoder forwards to OP_Consider; reads playerid + targetid correctly. | | +| `OP_Consume` | 🟢 Verified | C→S only. 4 send sites in DoDrinkEatPoison+sub_1402E1DD0. TOB wire: InventorySlot_Struct(12B)+unknown(-1)+type(0/1)+mode(0/1)=20B. Added DECODE: TOBToServerSlot, mode→auto_consumed, type+1. | | +| `OP_ControlBoat` | 🟢 Verified | Passthrough. Inline case block @ 0x1401f0384 (cmp-dispatch). Reads boatId [+0] uint32 + TakeControl [+4] bool. C→S: 3 send sites in ProcessControls + RightClickedOnPlayer. Struct matches exactly. | | +| `OP_CorpseDrag` | 🟢 Verified | C→S only. Decoder reads two length-prefixed strings → CorpseDrag_Struct. No real S→C handler (fallback at 0x1401f2dfb). No hton send site found; corpse drag may not be initiated by TOB client. | | +| `OP_CorpseDrop` | 🟢 Verified | C→S only. Passthrough. Client sends null-terminated target name (g_pTargetPlayer+0xB4, max 64 chars) when releasing a dragged corpse. Send @ sub_140172C80 (0x140172C80). No struct in eq_packet_structs.h. | | | `OP_CrashDump` | 🔴 Not-Set | | | -| `OP_CrystalCountUpdate` | 🔴 Not-Set | | | -| `OP_CrystalCreate` | 🔴 Not-Set | | | -| `OP_CrystalReclaim` | 🔴 Not-Set | | | -| `OP_CustomTitles` | 🔴 Not-Set | | | -| `OP_Damage` | 🟡 Unverified | | | -| `OP_Death` | 🟢 Verified | | | -| `OP_DelegateAbility` | 🔴 Not-Set | | | -| `OP_DeleteCharacter` | 🟢 Verified | | | -| `OP_DeleteCharge` | 🟡 Unverified | | | -| `OP_DeleteItem` | 🟡 Unverified | | | +| `OP_CrystalCountUpdate` | 🟠 Missing | | | +| `OP_CrystalCreate` | 🟠 Missing | | | +| `OP_CrystalReclaim` | 🟠 Missing | | | +| `OP_CustomTitles` | 🟠 Missing | | | +| `OP_Damage` | 🟢 Verified | handler `msg_successful_hit` @ `0x140216190`; TOB 48-byte struct (int64 damage at +0x08, type at +0x28) verified; decoder fixed (added `force` and `hit_pitch`) | | +| `OP_Death` | 🟢 Verified | Handler: `CEverQuest::ReportDeath` @ `0x1402866c0` (Pattern A). Fixed: added `OUT(corpseid)` to encoder (client reads +0x08 for `ReportSuccessfulHit` on skill-231 kills). TOB struct is 40 bytes. | | +| `OP_DelegateAbility` | 🟠 Missing | | | +| `OP_DeleteCharacter` | 🟢 Verified | C→S only. No S→C handler. Client sends char[64] name + uint32 padding (always 0) from `sub_1400CF9A0`. World handler `HandleDeleteCharacterPacket` reads `app->pBuffer` as char* directly — no struct needed, passthrough correct. | | +| `OP_DeleteCharge` | 🟢 Verified | ENCODE_FORWARD(OP_MoveItem). Client handler @ 0x1401eac01 (cmp-dispatch). Calls msg_move_spell_charge @ 0x1402102e0 — looks up item by from_slot, applies negative number_in_stack as charge delta. | | +| `OP_DeleteItem` | 🟢 Verified | Handler `sub_140204D00` @ `0x140204D00`; cmp-dispatch S→C; client reads from_slot+number_in_stack; to_slot sent but not read S→C; C→S always sends to_slot=-1 (trash); encoder/decoder correct. | | | `OP_DeletePetition` | 🔴 Not-Set | | | -| `OP_DeleteSpawn` | 🟡 Unverified | | | -| `OP_DeleteSpell` | 🟢 Verified | | | -| `OP_DenyResponse` | 🟢 Verified | appears unused by server or client | | -| `OP_Disarm` | 🟡 Unverified | | | -| `OP_DisarmTraps` | 🟡 Unverified | | | -| `OP_DisciplineTimer` | 🟡 Unverified | | | -| `OP_DisciplineUpdate` | 🟡 Unverified | | | +| `OP_DeleteSpawn` | 🟢 Verified | Struct correct (5 bytes). Byte 4 (Decay): 0=deferred decay animation path (sets field_133Ch=1 on corpse-type spawns), 1=immediate PrepForDestroyPlayer. Also gates duel-end logic. | | +| `OP_DeleteSpell` | 🟢 Verified | Passthrough. S→C @ 0x1401f097e → DeleteSpellFromBook @ 0x1404f26a0. C→S: DialogResponse @ 0x1404f27b0. | | +| `OP_DenyResponse` | 🟢 Verified | S→C only. Handler sub_14020A550 @ 0x14020a550. Corpse drag consent response: char[64] requester (+0), char[64] owner (+0x40), bool granted (+0x80), zone name (+0x81). Client picks message by name match. | | +| `OP_Disarm` | 🟢 Verified | Client→Server only. 16-byte struct (source/target/skill/unknown) matches Disarm_Struct exactly. No encoder/decoder needed; passthrough is correct. | | +| `OP_DisarmTraps` | 🟢 Verified | C→S only. 2-byte opcode, no payload. Sent by `CharacterZoneClient::UseSkill` (skill 17) when target is a valid NPC. Passthrough — no struct, no decoder needed. hton @ `0x140101740`. | | +| `OP_DisciplineTimer` | 🟢 Verified | S→C. Inline handler @ 0x1401f4b5d (cmp-dispatch secondary table). Calls SetMeleeSpellReuseTimer(TimerID,Duration,computed). Encoder sends 16-byte TOB struct with ServerTime for client latency-adjust calculation. | | +| `OP_DisciplineUpdate` | 🟢 Verified | S→C. Encoder sends 1200-byte packet (300×uint32); client at 0x1401e885b validates size via GetCombatAbilitySize()=1200, then rep movsb into CurrentProfile+0x4700. Server fills entries 0–99, zeros 100–299. | | | `OP_DiscordMerchantInventory` | 🔴 Not-Set | | | -| `OP_DoGroupLeadershipAbility` | 🔴 Not-Set | | | -| `OP_DuelDecline` | 🔴 Not-Set | | | -| `OP_DuelAccept` | 🔴 Not-Set | | | +| `OP_DoGroupLeadershipAbility` | 🟠 Missing | | | +| `OP_DuelDecline` | 🟠 Missing | | | +| `OP_DuelAccept` | 🟠 Missing | | | | `OP_DumpName` | 🔴 Not-Set | | | -| `OP_Dye` | 🔴 Not-Set | | | +| `OP_Dye` | 🟠 Missing | | | | `OP_DynamicWall` | 🔴 Not-Set | | | -| `OP_DzAddPlayer` | 🔴 Not-Set | | | -| `OP_DzChooseZone` | 🔴 Not-Set | | | -| `OP_DzChooseZoneReply` | 🔴 Not-Set | | | -| `OP_DzCompass` | 🔴 Not-Set | | | -| `OP_DzExpeditionEndsWarning` | 🔴 Not-Set | | | -| `OP_DzExpeditionInfo` | 🔴 Not-Set | | | -| `OP_DzExpeditionInvite` | 🔴 Not-Set | | | -| `OP_DzExpeditionInviteResponse` | 🔴 Not-Set | | | -| `OP_DzExpeditionLockoutTimers` | 🔴 Not-Set | | | -| `OP_DzListTimers` | 🔴 Not-Set | | | -| `OP_DzMakeLeader` | 🔴 Not-Set | | | -| `OP_DzMemberList` | 🔴 Not-Set | | | -| `OP_DzMemberListName` | 🔴 Not-Set | | | -| `OP_DzMemberListStatus` | 🔴 Not-Set | | | -| `OP_DzPlayerList` | 🔴 Not-Set | | | -| `OP_DzQuit` | 🔴 Not-Set | | | -| `OP_DzRemovePlayer` | 🔴 Not-Set | | | -| `OP_DzSetLeaderName` | 🔴 Not-Set | | | -| `OP_DzSwapPlayer` | 🔴 Not-Set | | | -| `OP_Emote` | 🔴 Not-Set | | | -| `OP_EndLootRequest` | 🟢 Verified | can be sent to close the client's looting session (with no data) | | -| `OP_EnduranceUpdate` | 🔴 Not-Set | | | +| `OP_DzAddPlayer` | 🟠 Missing | | | +| `OP_DzChooseZone` | 🟠 Missing | | | +| `OP_DzChooseZoneReply` | 🟠 Missing | | | +| `OP_DzCompass` | 🟠 Missing | | | +| `OP_DzExpeditionEndsWarning` | 🟠 Missing | | | +| `OP_DzExpeditionInfo` | 🟠 Missing | | | +| `OP_DzExpeditionInvite` | 🟠 Missing | | | +| `OP_DzExpeditionInviteResponse` | 🟠 Missing | | | +| `OP_DzExpeditionLockoutTimers` | 🟠 Missing | | | +| `OP_DzListTimers` | 🟠 Missing | | | +| `OP_DzMakeLeader` | 🟠 Missing | | | +| `OP_DzMemberList` | 🟠 Missing | | | +| `OP_DzMemberListName` | 🟠 Missing | | | +| `OP_DzMemberListStatus` | 🟠 Missing | | | +| `OP_DzPlayerList` | 🟠 Missing | | | +| `OP_DzQuit` | 🟠 Missing | | | +| `OP_DzRemovePlayer` | 🟠 Missing | | | +| `OP_DzSetLeaderName` | 🟠 Missing | | | +| `OP_DzSwapPlayer` | 🟠 Missing | | | +| `OP_Emote` | 🟠 Missing | | | +| `OP_EndLootRequest` | 🟢 Verified | Bidirectional passthrough. S→C: client calls CLootWnd::EndLootingSession @ 0x1404426e0, no body read. C→S: 4-byte body = corpse entity ID (uint32); server reads low 2 bytes as uint16 spawn ID. | | +| `OP_EnduranceUpdate` | 🟠 Missing | | | | `OP_EnterChat` | 🔴 Not-Set | | | -| `OP_EnterWorld` | 🟢 Verified | | | -| `OP_EnvDamage` | 🟡 Unverified | | | +| `OP_EnterWorld` | 🟢 Verified | C→S only. Decoder copies name correctly but hardcodes tutorial=0, return_home=0. TOB sends zoneID (EverQuest_EnterZoneReason; -1=last zone) instead of flags. Enter Tutorial/Return Home buttons broken. | | +| `OP_EnvDamage` | 🟢 Verified | C→S only. Send @ 0x1400E6012 (sub_1400E5DC0). TOB wire=58 bytes. Decoder added: entity_id=wire+0 (u16→u32), damage=wire+16 (i64→u32), dmgtype=wire+56. Needs in-game test (fall/drown/lava). | | | `OP_EvolveItem` | 🔴 Not-Set | | | -| `OP_ExpansionInfo` | 🟢 Verified | Updated from u32 to u64 and works now | | -| `OP_ExpUpdate` | 🟢 Verified | | | -| `OP_FaceChange` | 🔴 Not-Set | | | -| `OP_Feedback` | 🔴 Not-Set | | | -| `OP_FeignDeath` | 🟡 Unverified | | | +| `OP_ExpansionInfo` | 🟢 Verified | Handler `msg_expansions` @ 0x14020B970 (12 bytes). Pattern A. Client reads uint64 at offset 0x40; 64-byte Unknown000 prefix zeroed by alloc. OUT(Expansions) correct (u32→u64 zero-extend). | | +| `OP_ExpUpdate` | 🟢 Verified | Handler sub_14020B980 @ 0x14020B980 (95 bytes); Pattern A; encoder converts exp 0–330→0–100000; unknown=0 (no tip window); aaxp intentionally omitted (handled via OP_AAExpUpdate). | | +| `OP_FaceChange` | 🟠 Missing | | | +| `OP_Feedback` | 🟠 Missing | | | +| `OP_FeignDeath` | 🟢 Verified | Passthrough. No struct defined. Wire: spawn_id(4)+last_hitter(4)+fd(1). Inline case block @ 0x1401f55eb; C→S send in UseSkill::0x19 @ 0x140101388. fd always 0 from client; S→C checks nonzero for name-lookup. | | | `OP_FellowshipUpdate` | 🔴 Not-Set | | | -| `OP_FindPersonReply` | 🔴 Not-Set | | | -| `OP_FindPersonRequest` | 🔴 Not-Set | | | -| `OP_FinishTrade` | 🟡 Unverified | | | -| `OP_FinishWindow` | 🟡 Unverified | | | -| `OP_FinishWindow2` | 🟡 Unverified | | | -| `OP_Fishing` | 🟡 Unverified | | | -| `OP_Fling` | 🟡 Unverified | | | -| `OP_FloatListThing` | 🟢 Verified | Movement History. Sent from client, but emu doesn't use it so setting it as verified. Reference is 0x1402FFAD0 | | -| `OP_Forage` | 🟡 Unverified | | | +| `OP_FindPersonReply` | 🟠 Missing | | | +| `OP_FindPersonRequest` | 🟠 Missing | | | +| `OP_FinishTrade` | 🟢 Verified | Passthrough, zero-payload notification. Handler `msg_do_trade` @ `0x14020b4b0` reads no packet data; client finalises trade UI on receipt. No struct. | | +| `OP_FinishWindow` | 🟢 Verified | Zero-payload S→C notification. Handler @ 0x1401f4ae1 (ja-dispatch; missed by validate_opcode.py). Calls DecItemPending() — decrements pending item window counter. No struct needed. | | +| `OP_FinishWindow2` | 🟢 Verified | Zero-payload S→C signal, always paired with OP_FinishWindow in trade/object-close flows. No struct. Passthrough correct. TOB client has no handler — packet silently discarded on receipt (harmless). | | +| `OP_Fishing` | 🟢 Verified | C→S only. Zero-payload cast signal (opcode only, no struct). Client validates pole/bait/land/level before sending. Send @ sub_140102510+0x140102510. Passthrough — no decoder needed. | | +| `OP_Fling` | 🟢 Verified | Encoder added. TOB wire layout differs from server fling_struct — fields reordered + extra radius (+16, set 0.0f) and padding (+20). Handler sub_1403135C0 @ 0x1403135c0 (Pattern A). fall_damage bit semantics TBD (see packet_analysis/OP_Fling.md). | | +| `OP_FloatListThing` | 🟢 Verified | C→S only. Movement History telemetry; EQEmu ignores it, no decoder needed. `PlayerClient::SendMovementHistory` @ `0x1402FFAD0` (0x1DF bytes). Throttled to 1000ms; payload via `CMovementHistory::BuildPacket`. | | +| `OP_Forage` | 🟢 Verified | C→S only; zero-payload forage trigger (skill 27). hton @ 0x140101740 in UseSkill. No S→C handler in HWM. | | | `OP_ForceFindPerson` | 🔴 Not-Set | | | -| `OP_FormattedMessage` | 🟢 Verified | Some major work to do here -- the client now expects a spell link in the packet, will need to refactor the client work to decouple the stream from the internal representation | | -| `OP_FriendsWho` | 🟡 Unverified | | | -| `OP_GetGuildMOTD` | 🔴 Not-Set | | | -| `OP_GetGuildMOTDReply` | 🔴 Not-Set | | | +| `OP_FormattedMessage` | 🟢 Verified | Handler: msgTokenTextParam @ 0x140207ce0 (Pattern D, inline CUnSerializeBuffer). No encoder needed — MessageComponent::Formatted() (tob.cpp:5795) builds the TOB wire format directly (uint32=0 + uint8 from_world + uint32 string_id + uint32 color + 9x WriteLengthString). Spell links handled via ServerToTOBConvertLinks before serialization. | | +| `OP_FriendsWho` | 🟢 Verified | C→S only; sends comma-separated friends string (flag+'0'/'1' + buddy names). No S→C handler. Zone reads as raw char*. hton @ 0x1402992d9 in CEverQuest::Who (0x140298b70). Passthrough correct. | | +| `OP_GetGuildMOTD` | 🟠 Missing | | | +| `OP_GetGuildMOTDReply` | 🟠 Missing | | | | `OP_GetGuildsList` | 🔴 Not-Set | | | | `OP_GiveMoney` | 🔴 Not-Set | | | -| `OP_GMApproval` | 🔴 Not-Set | | | -| `OP_GMBecomeNPC` | 🔴 Not-Set | | | -| `OP_GMDelCorpse` | 🔴 Not-Set | | | -| `OP_GMEmoteZone` | 🔴 Not-Set | | | -| `OP_GMEndTraining` | 🟢 Verified | | | +| `OP_GMApproval` | 🟠 Missing | | | +| `OP_GMBecomeNPC` | 🟠 Missing | | | +| `OP_GMDelCorpse` | 🟠 Missing | | | +| `OP_GMEmoteWorld` | 🟠 Missing | | | +| `OP_GMEmoteZone` | 🟠 Missing | | | +| `OP_GMEndTraining` | 🟢 Verified | C→S only. Passthrough. CTrainWnd::AboutToHide @ 0x140529b30. Payload: npcid+playerid = GMTrainEnd_Struct exactly. | | | `OP_GMEndTrainingResponse` | 🔴 Not-Set | | | -| `OP_GMFind` | 🔴 Not-Set | | | -| `OP_GMGoto` | 🔴 Not-Set | | | -| `OP_GMHideMe` | 🔴 Not-Set | | | -| `OP_GMKick` | 🔴 Not-Set | | | -| `OP_GMKill` | 🔴 Not-Set | | | -| `OP_GMLastName` | 🔴 Not-Set | | | -| `OP_GMNameChange` | 🔴 Not-Set | | | -| `OP_GMSearchCorpse` | 🔴 Not-Set | | | -| `OP_GMServers` | 🔴 Not-Set | | | -| `OP_GMSummon` | 🔴 Not-Set | | | -| `OP_GMToggle` | 🔴 Not-Set | | | -| `OP_GMTraining` | 🟢 Verified | | | -| `OP_GMTrainSkill` | 🟢 Verified | | | -| `OP_GMTrainSkillConfirm` | 🟢 Verified | | | -| `OP_GMZoneRequest` | 🔴 Not-Set | | | -| `OP_GMZoneRequest2` | 🔴 Not-Set | | | -| `OP_GroundSpawn` | 🟢 Verified | | | -| `OP_GroupAcknowledge` | 🔴 Not-Set | | | -| `OP_GroupCancelInvite` | 🔴 Not-Set | | | -| `OP_GroupDelete` | 🔴 Not-Set | | | -| `OP_GroupDisband` | 🟡 Unverified | | | -| `OP_GroupDisbandOther` | 🔴 Not-Set | | | -| `OP_GroupDisbandYou` | 🔴 Not-Set | | | -| `OP_GroupFollow` | 🔴 Not-Set | | | -| `OP_GroupFollow2` | 🔴 Not-Set | | | -| `OP_GroupInvite` | 🟡 Unverified | | | +| `OP_GMFind` | 🟠 Missing | | | +| `OP_GMGoto` | 🟠 Missing | | | +| `OP_GMHideMe` | 🟠 Missing | | | +| `OP_GMKick` | 🟠 Missing | | | +| `OP_GMKill` | 🟠 Missing | | | +| `OP_GMLastName` | 🟠 Missing | | | +| `OP_GMNameChange` | 🟠 Missing | | | +| `OP_GMSearchCorpse` | 🟠 Missing | | | +| `OP_GMServers` | 🟠 Missing | | | +| `OP_GMSummon` | 🟠 Missing | | | +| `OP_GMToggle` | 🟠 Missing | | | +| `OP_GMTraining` | 🟢 Verified | Bidirectional. S→C handler `msg_req_guildmaster` @ `0x140212e20` (Pattern A). Reads npcid[+0], flag[+0x198], passes skills[+8] and languages[+0x199] to CTrainWnd::SetGMData. Encoder/decoder correct. | | +| `OP_GMTrainSkill` | 🟢 Verified | C→S only. Send fn `sub_14052ADE0` @ `0x14052ade0`. Wire: uint32 npcid, skillbank, skill_id. Passthrough. | | +| `OP_GMTrainSkillConfirm` | 🟢 Verified | Encoder maps all 4 fields correctly. Inline handler @ cmp-dispatch 0x1401f37b4→0x1401f98c5. SkillID≤99=normal skill, >99=language (GetLangDesc). Unknown073[3] trailing padding not read by client. | | +| `OP_GMZoneRequest` | 🟠 Missing | | | +| `OP_GMZoneRequest2` | 🟠 Missing | | | +| `OP_GroundSpawn` | 🟢 Verified | S→C encoder verified. Pattern D: HWM inline alloc EQGroundItem, CSB in sub_14021EA40 @ 0x14021EA40. 13 wire fields match client reads. C→S is zero-byte signal (no decoder needed). | | +| `OP_GroupAcknowledge` | 🟠 Missing | | | +| `OP_GroupCancelInvite` | 🟠 Missing | | | +| `OP_GroupDelete` | 🟠 Missing | | | +| `OP_GroupDisband` | 🟢 Verified | C→S only; `CEverQuest::Disband` @ `0x14026c220`; decoder correctly copies name1/name2 from 154-byte TOB struct (26 extra bytes zeroed/ignored); no S→C handler. | | +| `OP_GroupDisbandOther` | 🟠 Missing | | | +| `OP_GroupDisbandYou` | 🟠 Missing | | | +| `OP_GroupFollow` | 🟠 Missing | | | +| `OP_GroupFollow2` | 🟠 Missing | | | +| `OP_GroupInvite` | 🟢 Verified | Handler sub_14029A040@0x14029A040 (PatternA). Fixed GroupGeneric_Struct to 168B (was 154); added ENCODE using SETUP_DIRECT_ENCODE. Client reads GroupRequestId at offset 168 (1B past buf) — server has no equivalent field, reads 0. | | | `OP_GroupInvite2` | 🔴 Not-Set | | | -| `OP_GroupLeaderChange` | 🔴 Not-Set | | | -| `OP_GroupLeadershipAAUpdate` | 🔴 Not-Set | | | -| `OP_GroupMakeLeader` | 🔴 Not-Set | | | -| `OP_GroupMentor` | 🔴 Not-Set | | | -| `OP_GroupRoles` | 🔴 Not-Set | | | -| `OP_GroupUpdate` | 🔴 Not-Set | | | -| `OP_GroupUpdateB` | 🔴 Not-Set | | | +| `OP_GroupLeaderChange` | 🟠 Missing | | | +| `OP_GroupLeadershipAAUpdate` | 🟠 Missing | | | +| `OP_GroupMakeLeader` | 🟠 Missing | | | +| `OP_GroupMentor` | 🟠 Missing | | | +| `OP_GroupRoles` | 🟠 Missing | | | +| `OP_GroupUpdate` | 🟠 Missing | | | +| `OP_GroupUpdateB` | 🟠 Missing | | | | `OP_GroupUpdateLeaderAA` | 🔴 Not-Set | | | -| `OP_GuildBank` | 🔴 Not-Set | | | -| `OP_GuildBankItemList` | 🔴 Not-Set | | | -| `OP_GuildCreate` | 🔴 Not-Set | | | -| `OP_GuildDelete` | 🔴 Not-Set | | | -| `OP_GuildDeleteGuild` | 🔴 Not-Set | | | -| `OP_GuildDemote` | 🔴 Not-Set | | | -| `OP_GuildInvite` | 🔴 Not-Set | | | -| `OP_GuildInviteAccept` | 🔴 Not-Set | | | -| `OP_GuildLeader` | 🔴 Not-Set | | | +| `OP_GuildBank` | 🟠 Missing | | | +| `OP_GuildBankItemList` | 🟠 Missing | | | +| `OP_GuildCreate` | 🟠 Missing | | | +| `OP_GuildDelete` | 🟠 Missing | | | +| `OP_GuildDeleteGuild` | 🟠 Missing | | | +| `OP_GuildDemote` | 🟠 Missing | | | +| `OP_GuildInvite` | 🟠 Missing | | | +| `OP_GuildInviteAccept` | 🟠 Missing | | | +| `OP_GuildLeader` | 🟠 Missing | | | | `OP_GuildManageAdd` | 🔴 Not-Set | | | -| `OP_GuildManageBanker` | 🔴 Not-Set | | | +| `OP_GuildManageBanker` | 🟠 Missing | | | | `OP_GuildManageRemove` | 🔴 Not-Set | | | | `OP_GuildManageStatus` | 🔴 Not-Set | | | | `OP_GuildMemberLevelUpdate` | 🔴 Not-Set | | | -| `OP_GuildMemberList` | 🔴 Not-Set | | | -| `OP_GuildMemberUpdate` | 🔴 Not-Set | | | -| `OP_GuildMemberLevel` | 🔴 Not-Set | | | -| `OP_GuildMemberRankAltBanker` | 🔴 Not-Set | | | -| `OP_GuildMemberPublicNote` | 🔴 Not-Set | | | -| `OP_GuildMemberAdd` | 🔴 Not-Set | | | -| `OP_GuildMemberRename` | 🔴 Not-Set | | | -| `OP_GuildMemberDelete` | 🔴 Not-Set | | | -| `OP_GuildMemberDetails` | 🔴 Not-Set | | | -| `OP_GuildRenameGuild` | 🔴 Not-Set | | | -| `OP_GuildMOTD` | 🔴 Not-Set | | | -| `OP_GuildPeace` | 🔴 Not-Set | | | -| `OP_GuildPromote` | 🔴 Not-Set | | | -| `OP_GuildPublicNote` | 🔴 Not-Set | | | -| `OP_GuildRemove` | 🔴 Not-Set | | | -| `OP_GuildSelectTribute` | 🔴 Not-Set | | | -| `OP_GuildModifyBenefits` | 🔴 Not-Set | | | -| `OP_GuildTributeToggleReq` | 🔴 Not-Set | | | -| `OP_GuildTributeToggleReply` | 🔴 Not-Set | | | -| `OP_GuildOptInOut` | 🔴 Not-Set | | | -| `OP_GuildSaveActiveTributes` | 🔴 Not-Set | | | -| `OP_GuildSendActiveTributes` | 🔴 Not-Set | | | -| `OP_GuildTributeFavorAndTimer` | 🔴 Not-Set | | | -| `OP_GuildsList` | 🔴 Not-Set | | | -| `OP_GuildStatus` | 🔴 Not-Set | | | +| `OP_GuildMemberList` | 🟠 Missing | | | +| `OP_GuildMemberUpdate` | 🟠 Missing | | | +| `OP_GuildMemberLevel` | 🟠 Missing | | | +| `OP_GuildMemberRankAltBanker` | 🟠 Missing | | | +| `OP_GuildMemberPublicNote` | 🟠 Missing | | | +| `OP_GuildMemberAdd` | 🟠 Missing | | | +| `OP_GuildMemberRename` | 🟠 Missing | | | +| `OP_GuildMemberDelete` | 🟠 Missing | | | +| `OP_GuildMemberDetails` | 🟠 Missing | | | +| `OP_GuildRenameGuild` | 🟠 Missing | | | +| `OP_GuildMOTD` | 🟠 Missing | | | +| `OP_GuildPeace` | 🟠 Missing | | | +| `OP_GuildPromote` | 🟠 Missing | | | +| `OP_GuildPublicNote` | 🟠 Missing | | | +| `OP_GuildRemove` | 🟠 Missing | | | +| `OP_GuildSelectTribute` | 🟠 Missing | | | +| `OP_GuildModifyBenefits` | 🟠 Missing | | | +| `OP_GuildTributeToggleReq` | 🟠 Missing | | | +| `OP_GuildTributeToggleReply` | 🟠 Missing | | | +| `OP_GuildOpenGuildWindow` | 🟠 Missing | | | +| `OP_GuildOptInOut` | 🟠 Missing | | | +| `OP_GuildSaveActiveTributes` | 🟠 Missing | | | +| `OP_GuildSendActiveTributes` | 🟠 Missing | | | +| `OP_GuildTributeFavorAndTimer` | 🟠 Missing | | | +| `OP_GuildsList` | 🟠 Missing | | | +| `OP_GuildStatus` | 🟠 Missing | | | | `OP_GuildTributeInfo` | 🔴 Not-Set | | | -| `OP_GuildUpdate` | 🔴 Not-Set | | | -| `OP_GuildTributeDonateItem` | 🔴 Not-Set | | | -| `OP_GuildTributeDonatePlat` | 🔴 Not-Set | | | -| `OP_GuildWar` | 🔴 Not-Set | | | +| `OP_GuildUpdate` | 🟠 Missing | | | +| `OP_GuildTributeDonateItem` | 🟠 Missing | | | +| `OP_GuildTributeDonatePlat` | 🟠 Missing | | | +| `OP_GuildWar` | 🟠 Missing | | | | `OP_Heartbeat` | 🔴 Not-Set | | | -| `OP_Hide` | 🟡 Unverified | | | -| `OP_HideCorpse` | 🟢 Verified | | | -| `OP_HPUpdate` | 🟢 Verified | | | -| `OP_Illusion` | 🟢 Verified | Actually a generic appearance change packet, including name for self | | -| `OP_IncreaseStats` | 🟡 Unverified | | | +| `OP_Hide` | 🟢 Verified | C→S only. 4-byte payload: uint32 (1=movement check passed, requesting hide roll; 0=cancel). 3 send sites: sub_140102A50 @ 0x140102A50, CancelHide @ 0x140267670, CancelSneakHide @ 0x1402678E0. Fixed: data==0 returns early, data==1 falls through to full server hide roll. | | +| `OP_HideCorpse` | 🟢 Verified | C→S only passthrough. Send sites: cmd@0x140222bf0, DoMainLoop, loadOptions, ALWAYS-mode@0x140441b70. | | +| `OP_HPUpdate` | 🟢 Verified | S→C only. Cmp-dispatch @ 0x1401f3571 → inline case block 0x1401f4448 (Pattern B). Reads spawn_id(+0,w), cur_hp(+2,q), max_hp(+0xA,q); calls ProcessHitpointMessage@0x1401fefd0. Encoder+struct match. | | +| `OP_Illusion` | 🟢 Verified | S→C: msg_change_form@0x14020a080 (Pattern A); encoder maps all common fields; TOB-only fields (class_, armorProperties, armorTints) zero-init (correct for NPC illusions). C→S: /becomenpc sends 332-byte TOB struct; no decoder, server gets garbled data past charname — GM-only. | | +| `OP_IncreaseStats` | 🟢 Verified | Handler sub_140208290 @ 0x140208290 (Pattern A). TOB wire: {uint32 spawn_id, uint32 stat_type 0–6, uint32 value}. Encoder added; spawn_id sourced from unknown13[0..1] stashed by Client::IncStats/SetStats (GetID()). Primary stats only; resists not handled by TOB client. | | | `OP_InitialHPUpdate` | 🔴 Not-Set | | | | `OP_InitialMobHealth` | 🔴 Not-Set | | | -| `OP_InspectAnswer` | 🔴 Not-Set | | | -| `OP_InspectBuffs` | 🔴 Not-Set | | | -| `OP_InspectMessageUpdate` | 🔴 Not-Set | | | -| `OP_InspectRequest` | 🔴 Not-Set | | | -| `OP_InstillDoubt` | 🟡 Unverified | | | -| `OP_InterruptCast` | 🟢 Verified | Some major work to do here -- the client now expects a spell link in the packet, will need to refactor the client work to decouple the stream from the internal representation | | +| `OP_InspectAnswer` | 🟠 Missing | | | +| `OP_InspectBuffs` | 🟠 Missing | | | +| `OP_InspectMessageUpdate` | 🟠 Missing | | | +| `OP_InspectRequest` | 🟠 Missing | | | +| `OP_InstillDoubt` | 🟠 Missing | Was duplicate of OP_Fishing (both 0x3cdb). Fixed: set to 0x0000. IDA confirms 0x3cdb is fishing only (sub_140102510); no Instill Doubt send site found in TOB binary. Real TOB opcode unknown. | | +| `OP_InterruptCast` | 🟢 Verified | S→C passthrough. Handler sub_1401E27C0 @ 0x1401E27C0 (Pattern A). Reads spawnid+4, messageid+4, message[0]+8. messageid used for StringTable lookup + orderedStringExpansion. Struct matches exactly. | | | `OP_InvokeChangePetName` | 🔴 Not-Set | | | | `OP_InvokeChangePetNameImmediate` | 🔴 Not-Set | | | | `OP_InvokeNameChangeImmediate` | 🔴 Not-Set | | | | `OP_InvokeNameChangeLazy` | 🔴 Not-Set | | | -| `OP_ItemLinkClick` | 🔴 Not-Set | | | -| `OP_ItemLinkResponse` | 🔴 Not-Set | | | +| `OP_ItemAdvancedLoreText` | 🟠 Missing | | | +| `OP_ItemLinkClick` | 🟠 Missing | | | +| `OP_ItemLinkResponse` | 🟠 Missing | | | | `OP_ItemLinkText` | 🔴 Not-Set | | | | `OP_ItemName` | 🔴 Not-Set | | | -| `OP_ItemPacket` | 🟡 Unverified | | | -| `OP_ItemPreview` | 🔴 Not-Set | | | +| `OP_ItemPacket` | 🟢 Verified | Handler @ `0x1401f0bf3` (inline+CSB). Fixed: added `ItemPacketParcel` to `ServerToTOBItemPacketType` (srv 0x73→TOB 0x74) and corrected parcel `SerializeItem` to use `pms.slot_id`. Note: client discards note field. | | +| `OP_ItemPreview` | 🟠 Missing | | | | `OP_ItemPreviewRequest` | 🔴 Not-Set | | | -| `OP_ItemRecastDelay` | 🟡 Unverified | | | -| `OP_ItemVerifyReply` | 🟡 Unverified | | | -| `OP_ItemVerifyRequest` | 🟡 Unverified | | | -| `OP_ItemViewUnknown` | 🔴 Not-Set | | | -| `OP_Jump` | 🟡 Unverified | | | -| `OP_KeyRing` | 🔴 Not-Set | | | -| `OP_KickPlayers` | 🟡 Unverified | | | +| `OP_ItemRecastDelay` | 🟢 Verified | Handler @ `0x1401E8638` (inline, Pattern B). Encoder added: InventorySlot_Struct (zeroed, TODO server struct) + recast_delay (+0x0C) + recast_type (+0x10). SetCoreItemRecastTimer fires; per-item timer needs server slot. | | +| `OP_ItemVerifyReply` | 🟢 Verified | Handler `0x1401e652c` (inline Pattern B). Encoder added: expands 12→20 bytes; adds unknown0 (+0x0C exit gate) and recast_time (+0x10 timestamp, zeroed). Autobook-scribe (spell==0x407) needs server-side recast_time. | | +| `OP_ItemVerifyRequest` | 🟢 Verified | C→S only. TOB sends 16-byte packet (InventorySlot_Struct 12 bytes + target uint32). DECODE added using TOBToServerSlot; tob_structs.h entry added. Matches RoF2 layout with int32 Type instead of int16. | | +| `OP_ItemViewUnknown` | 🟠 Missing | | | +| `OP_Jump` | 🟢 Verified | C→S only; 0-byte payload, no struct. Client sends on jump; server deducts endurance (`Handle_OP_Jump`). Passthrough correct. Send @ `0x140272773` in `DoMainLoop`. | | +| `OP_KeyRing` | 🟠 Missing | | | +| `OP_KickPlayers` | 🟢 Verified | C→S only (no HWM handler). DialogResponse case 140. Sends full KickPlayers_Struct (72 bytes): char_name[64]+unknown064+kick_expedition+kick_task+padding. Passthrough correct, struct matches exactly. | | | `OP_KnowledgeBase` | 🔴 Not-Set | | | -| `OP_LDoNButton` | 🔴 Not-Set | | | +| `OP_LDoNButton` | 🟠 Missing | | | | `OP_LDoNDisarmTraps` | 🔴 Not-Set | | | | `OP_LDoNInspect` | 🔴 Not-Set | | | -| `OP_LDoNOpen` | 🟡 Unverified | | | -| `OP_LDoNPickLock` | 🟡 Unverified | | | -| `OP_LDoNSenseTraps` | 🟡 Unverified | | | -| `OP_LeadershipExpToggle` | 🔴 Not-Set | | | -| `OP_LeadershipExpUpdate` | 🔴 Not-Set | | | -| `OP_LeaveAdventure` | 🔴 Not-Set | | | -| `OP_LeaveBoat` | 🟡 Unverified | | | -| `OP_LevelAppearance` | 🟡 Unverified | | | -| `OP_LevelUpdate` | 🟢 Verified | | | +| `OP_LDoNOpen` | 🟢 Verified | C→S only. Zero-payload packet (opcode only, no data body). Client sends from `do_open` @ `0x14022b160`; server handler reads no packet data — uses `GetTarget()`. No encoder/decoder needed. | | +| `OP_LDoNPickLock` | 🟢 Verified | C→S only. Zero-payload packet (opcode only, no data body). Client sends from `UseSkill` @ `0x140100e60` (case 0x23, skill 35); server handler reads no packet data — uses `GetTarget()`. No encoder/decoder needed. | | +| `OP_LDoNSenseTraps` | 🔴 Not-Set | Not set in patch_TOB.conf; no server struct or encoder/decoder found. | | +| `OP_LeadershipExpToggle` | 🟠 Missing | | | +| `OP_LeadershipExpUpdate` | 🟠 Missing | | | +| `OP_LeaveAdventure` | 🟠 Missing | | | +| `OP_LeaveBoat` | 🟢 Verified | C→S only. Zero-payload signal packet (no struct). Client sends opcode-only header from DoPassageOfTime @ 0x1400edf50 to notify server of vehicle dismount. No encoder/decoder needed. | | +| `OP_LevelAppearance` | 🟢 Verified | Passthrough. Case block @ 0x1401f61ff → UpdateAnimVariation @ 0x1402ff3d0. 5 parm/slot/flag triplets; all offsets match LevelAppearance_Struct exactly. valueNb low-byte-only read is benign. | | +| `OP_LevelUpdate` | 🟢 Verified | Passthrough. Handler sub_14020F110 @ 0x14020F110 (Pattern A). All 3 fields (level/level_old/exp) read correctly from wire offsets 0/4/8. No encoder/decoder needed. | | | `OP_LFGAppearance` | 🔴 Not-Set | | | -| `OP_LFGCommand` | 🔴 Not-Set | | | -| `OP_LFGGetMatchesRequest` | 🔴 Not-Set | | | -| `OP_LFGGetMatchesResponse` | 🔴 Not-Set | | | +| `OP_LFGCommand` | 🟠 Missing | | | +| `OP_LFGGetMatchesRequest` | 🟠 Missing | | | +| `OP_LFGGetMatchesResponse` | 🟠 Missing | | | | `OP_LFGResponse` | 🔴 Not-Set | | | -| `OP_LFGuild` | 🔴 Not-Set | | | -| `OP_LFPCommand` | 🔴 Not-Set | | | -| `OP_LFPGetMatchesRequest` | 🔴 Not-Set | | | -| `OP_LFPGetMatchesResponse` | 🔴 Not-Set | | | -| `OP_LinkedReuse` | 🟢 Verified | | | -| `OP_LoadSpellSet` | 🔴 Not-Set | | | +| `OP_LFGuild` | 🟠 Missing | | | +| `OP_LFPCommand` | 🟠 Missing | | | +| `OP_LFPGetMatchesRequest` | 🟠 Missing | | | +| `OP_LFPGetMatchesResponse` | 🟠 Missing | | | +| `OP_LinkedReuse` | 🟢 Verified | Inline case @ 0x1401f4c23. Added encoder + TOB struct (16 bytes): extra DWORD at +0x04, end_time at +0x08, start_time at +0x0C. Server sent 12 bytes; client read past end — reuse timers were corrupted. | | +| `OP_LoadSpellSet` | 🟠 Missing | | | | `OP_LocInfo` | 🔴 Not-Set | | | | `OP_LockoutTimerInfo` | 🔴 Not-Set | | | | `OP_Login` | 🔴 Not-Set | | | @@ -339,65 +344,65 @@ Below is a status list for the 450 opcodes we currently use on the server for th | `OP_LoginExpansionPacketData` | 🔴 Not-Set | | | | `OP_LoginUnknown1` | 🔴 Not-Set | | | | `OP_LoginUnknown2` | 🔴 Not-Set | | | -| `OP_Logout` | 🟡 Unverified | | | +| `OP_Logout` | 🟢 Verified | C→S passthrough (empty, `INz`). S→C handler @ `0x1401ea526` reads 2 bools (bool[0]=cancel-camp/AA, bool[1]=exit-confirm) but EQEmu never sends S→C; uses `OP_LogoutReply` instead. | | | `OP_LogoutReply` | 🔴 Not-Set | | | -| `OP_LogServer` | 🟢 Verified | Mostly unused values | | -| `OP_LootComplete` | 🟢 Verified | | | -| `OP_LootItem` | 🟢 Verified | | | -| `OP_LootRequest` | 🟢 Verified | | | -| `OP_ManaChange` | 🟢 Verified | | | -| `OP_ManaUpdate` | 🔴 Not-Set | | | -| `OP_MarkNPC` | 🔴 Not-Set | | | -| `OP_MarkRaidNPC` | 🔴 Not-Set | | | -| `OP_Marquee` | 🟡 Unverified | | | -| `OP_MemorizeSpell` | 🟢 Verified | | | -| `OP_Mend` | 🟡 Unverified | | | +| `OP_LogServer` | 🟢 Verified | Inline case block @ 0x1401eda66, Pattern B; 1932-byte packet manually assembled; all 23 client-read fields correctly set; dead: worldshortname (+0x15), voicemacros (+0x5F4), tutorial (+0x5F8) | | +| `OP_LootComplete` | 🟢 Verified | S→C only. Zero-payload signal; client calls CLootWnd::EndLootingSession @ 0x1404426e0. No struct, no encoder/decoder needed. Passthrough confirmed. | | +| `OP_LootItem` | 🟢 Verified | Bidirectional. C→S decoder correct. Added S→C encoder: server 16-byte→TOB 20-byte; slot_id via ServerToTOBCorpseMainSlot; unknown16 (quantity) set to 0 (no server-side quantity field). Handler: `CLootWnd::SlotLooted` @ 0x140443d40. | | +| `OP_LootRequest` | 🟢 Verified | C→S only. Raw uint32 payload (corpse spawn ID at PlayerClient+0x168). No struct, no decoder needed. Server checks app->size == sizeof(uint32) and calls entity_list.GetID(). Passthrough confirmed. | | +| `OP_ManaChange` | 🟢 Verified | S→C handler `msg_stop_casting` @ 0x140215f70 (Pattern A, 531 bytes). Updates mana+endurance, clears gem ETA, optionally stops cast. Encoder correct; keepcasting uint8→uint32 safe (client reads byte). C→S opcode-only (no payload). | | +| `OP_ManaUpdate` | 🟠 Missing | | | +| `OP_MarkNPC` | 🟠 Missing | | | +| `OP_MarkRaidNPC` | 🟠 Missing | | | +| `OP_Marquee` | 🟢 Verified | Passthrough S→C. Inline case 0x1401F00CD reads all 6 uint32 fields (+0x00–+0x14) + msg ptr (+0x18) directly; forwards to CBroadcast::BroadcastString @ 0x1400BCEA0. | | +| `OP_MemorizeSpell` | 🟢 Verified | Bidir. Handler sub_14020ECB0 @ 0x14020ecb0 (Pattern A). Switch on scribing -1..4. Encoder/decoder correct; scribing=1 suppress intentional. 6 C→S send sites confirmed. | | +| `OP_Mend` | 🟢 Verified | C→S only, no payload. UseSkill case 0x20 (Mend/skill 32) @ hton 0x140101740. No struct or decoder needed. | | | `OP_MendHPUpdate` | 🔴 Not-Set | | | | `OP_MercenaryAssign` | 🔴 Not-Set | | | -| `OP_MercenaryCommand` | 🔴 Not-Set | | | -| `OP_MercenaryDataRequest` | 🔴 Not-Set | | | -| `OP_MercenaryDataResponse` | 🔴 Not-Set | | | -| `OP_MercenaryDataUpdate` | 🔴 Not-Set | | | -| `OP_MercenaryDataUpdateRequest` | 🔴 Not-Set | | | -| `OP_MercenaryDismiss` | 🔴 Not-Set | | | -| `OP_MercenaryHire` | 🔴 Not-Set | | | -| `OP_MercenarySuspendRequest` | 🔴 Not-Set | | | -| `OP_MercenarySuspendResponse` | 🔴 Not-Set | | | -| `OP_MercenaryTimer` | 🔴 Not-Set | | | -| `OP_MercenaryTimerRequest` | 🔴 Not-Set | | | -| `OP_MercenaryUnknown1` | 🔴 Not-Set | | | -| `OP_MercenaryUnsuspendResponse` | 🔴 Not-Set | | | +| `OP_MercenaryCommand` | 🟠 Missing | | | +| `OP_MercenaryDataRequest` | 🟠 Missing | | | +| `OP_MercenaryDataResponse` | 🟠 Missing | | | +| `OP_MercenaryDataUpdate` | 🟠 Missing | | | +| `OP_MercenaryDataUpdateRequest` | 🟠 Missing | | | +| `OP_MercenaryDismiss` | 🟠 Missing | | | +| `OP_MercenaryHire` | 🟠 Missing | | | +| `OP_MercenarySuspendRequest` | 🟠 Missing | | | +| `OP_MercenarySuspendResponse` | 🟠 Missing | | | +| `OP_MercenaryTimer` | 🟠 Missing | | | +| `OP_MercenaryTimerRequest` | 🟠 Missing | | | +| `OP_MercenaryUnknown1` | 🟠 Missing | | | +| `OP_MercenaryUnsuspendResponse` | 🟠 Missing | | | | `OP_MerchantBulkItems` | 🔴 Not-Set | | | -| `OP_MobEnduranceUpdate` | 🔴 Not-Set | | | -| `OP_MobHealth` | 🟡 Unverified | | | -| `OP_MobManaUpdate` | 🔴 Not-Set | | | -| `OP_MobRename` | 🔴 Not-Set | | | -| `OP_MobUpdate` | 🔴 Not-Set | | | -| `OP_MoneyOnCorpse` | 🟢 Verified | This is the loot response struct, it provides all the logistics for things like advloot | | -| `OP_MoneyUpdate` | 🟡 Unverified | | | -| `OP_MOTD` | 🟢 Verified | | | -| `OP_MoveCoin` | 🟡 Unverified | | | -| `OP_MoveDoor` | 🟡 Unverified | | | -| `OP_MoveItem` | 🟢 Verified | | | -| `OP_MoveMultipleItems` | 🟡 Unverified | | | +| `OP_MobEnduranceUpdate` | 🟠 Missing | | | +| `OP_MobHealth` | 🟢 Verified | S→C only. Handler via cmp-dispatch @ `0x1401f43ce` → `ProcessHitpointMessage` @ `0x1401fefd0`. 6-byte packet: int16 spawn_id + uint32 hp%. Encoder correct; max hardcoded 100 at call site. | | +| `OP_MobManaUpdate` | 🟠 Missing | | | +| `OP_MobRename` | 🟠 Missing | | | +| `OP_MobUpdate` | 🟠 Missing | | | +| `OP_MoneyOnCorpse` | 🟢 Verified | Handler `sub_140443110` @ `0x140443110`. Pattern B inline; TOB struct (24 B) matches client reads. flags=0 hardcoded (no server field); type 0/1/2 map correctly from emu->response. | | +| `OP_MoneyUpdate` | 🟢 Verified | Passthrough. Handler sub_140213940 @ 0x140213940 (Pattern A). All 4 fields (platinum/gold/silver/copper) read directly into LocalPC. S→C only. | | +| `OP_MOTD` | 🟢 Verified | Passthrough. Raw null-terminated string (no struct). Handler sub_140219DA0 @ 0x140219DA0 stores MOTD in EverQuest_motd and calls display update if changed. Client never sends. | | +| `OP_MoveCoin` | 🟢 Verified | C→S only. Passthrough (no encoder/decoder). 2 send sites: CEverQuest::MoveMoney @ 0x14027e000, PcZoneClient::DestroyHeldItemOrMoney @ 0x1402e6a10. No S→C handler in HWM. 20-byte struct matches. | | +| `OP_MoveDoor` | 🟢 Verified | Passthrough. Inline Pattern B @ 0x1401ED4A9. Reads doorid (+0) to look up EQSwitch via sub_14025FE50, then action (+1) → EQSwitch::ChangeState. Struct matches exactly. S→C only. | | +| `OP_MoveItem` | 🟢 Verified | Bidirectional. Encoder/decoder use ServerToTOBSlot/TOBToServerSlot (uint32↔InventorySlot_Struct). S→C cmp-dispatch @ 0x1401EAE97; normal path calls sub_14020FAF0; invalid to_slot+Type3/4 destroys item via sub_140204D00. 12 C→S send sites. | | +| `OP_MoveMultipleItems` | 🟢 Verified | Bidirectional. Passthrough. S→C handler sub_1402CD630 @ 0x1402cd630; reads count (+0) then 36-byte MultiMoveItemSub_Struct entries. C→S send in MultipleItemMoveManager::ProcessMove @ 0x1402cd830. | | | `OP_MoveLogDisregard` | 🔴 Not-Set | | | | `OP_MoveLogRequest` | 🔴 Not-Set | | | | `OP_MultiLineMsg` | 🔴 Not-Set | | | -| `OP_NewSpawn` | 🟢 Verified | Deprecated in the client, already handled in emu | | -| `OP_NewTitlesAvailable` | 🔴 Not-Set | | | -| `OP_NewZone` | 🟢 Verified | | | -| `OP_NPCMoveUpdate` | 🔴 Not-Set | | | -| `OP_OnLevelMessage` | 🟡 Unverified | | | -| `OP_OpenContainer` | 🟡 Unverified | | | +| `OP_NewSpawn` | 🟢 Verified | Deprecated in TOB client — 0x053d handler @ 0x1401eedcc calls fdebug("Received deprecated...") only. Encoder forwards to OP_ZoneSpawns which sends OP_ZoneEntry (0x713d). No struct parsing by client. | | +| `OP_NewTitlesAvailable` | 🟠 Missing | | | +| `OP_NewZone` | 🟢 Verified | Handler: zoneHeader_Deserialize @ 0x140203d30, Pattern D. Fixed npc_aggro_max_dist (was hardcoded 600). All other fields correct. | | +| `OP_NPCMoveUpdate` | 🟠 Missing | | | +| `OP_OnLevelMessage` | 🟢 Verified | Handler `CServerTextWnd::HandleShowDialogMessage` @ `0x1404ed300`. Pattern D (Hybrid CUnSerializeBuffer). Encoder matches client reads. Fields 9–10 hardcoded 0 (no server struct equivalent); correct. | | +| `OP_OpenContainer` | 🟢 Verified | C→S only. Server handler is empty no-op by design (all clients). Send @ 0x1403bad8d in CContainerMgr::OpenContainer. Sends 12-byte ItemGlobalIndex payload; server ignores contents. | | | `OP_OpenDiscordMerchant` | 🔴 Not-Set | | | -| `OP_OpenGuildTributeMaster` | 🔴 Not-Set | | | +| `OP_OpenGuildTributeMaster` | 🟠 Missing | | | | `OP_OpenInventory` | 🔴 Not-Set | | | -| `OP_OpenTributeMaster` | 🔴 Not-Set | | | +| `OP_OpenTributeMaster` | 🟠 Missing | | | | `OP_PDeletePetition` | 🔴 Not-Set | | | -| `OP_PetCommands` | 🔴 Not-Set | | | -| `OP_PetCommandState` | 🔴 Not-Set | | | -| `OP_PetHoTT` | 🔴 Not-Set | | | -| `OP_Petition` | 🔴 Not-Set | | | +| `OP_PetCommands` | 🟠 Missing | | | +| `OP_PetCommandState` | 🟠 Missing | | | +| `OP_PetHoTT` | 🟠 Missing | | | +| `OP_Petition` | 🟠 Missing | | | | `OP_PetitionBug` | 🔴 Not-Set | | | | `OP_PetitionCheckIn` | 🔴 Not-Set | | | | `OP_PetitionCheckout` | 🔴 Not-Set | | | @@ -411,226 +416,226 @@ Below is a status list for the 450 opcodes we currently use on the server for th | `OP_PetitionSearchText` | 🔴 Not-Set | | | | `OP_PetitionUnCheckout` | 🔴 Not-Set | | | | `OP_PetitionUpdate` | 🔴 Not-Set | | | -| `OP_PickPocket` | 🟡 Unverified | | | +| `OP_PickPocket` | 🟢 Verified | S→C encoder + C→S decoder via structs::PickPocket_Struct. Handler sub_140215B60@0x140215B60 (Pattern D). Wire: uint32 to/from/myskill, uint8 type, uint32 coin(unaligned@0xD), length-prefixed itemname, uint8 luckily. | | | `OP_PickZone` | 🔴 Not-Set | | | | `OP_PickZoneWindow` | 🔴 Not-Set | | | -| `OP_PlayerProfile` | 🟢 Verified | | | -| `OP_PlayerStateAdd` | 🟡 Unverified | | | -| `OP_PlayerStateRemove` | 🟡 Unverified | | | +| `OP_PlayerProfile` | 🟢 Verified | S→C only. Case block `0x1401f124e` → `sub_1402E01A0` @ `0x1402e01a0`. Decompresses, CRC32-verifies, calls `ClientNetPcData::UnSerialize()`. ~1040-line manual SerializeBuffer encoder. Buffs/armor hardcoded empty; coin written twice. | | +| `OP_PlayerStateAdd` | 🟢 Verified | Passthrough. S→C handler sub_1401FE640 @ 0x1401FE640 (Pattern A): reads spawn_id+0x00 and state+0x04, ORs state into PlayerState. 5 C→S send sites in do_sheathe, do_aggressive, UpdateItemSlot. | | +| `OP_PlayerStateRemove` | 🟢 Verified | Passthrough. S→C handler sub_1401FE670 @ 0x1401FE670 (Pattern A): reads spawn_id+0x00 and state+0x04, ANDs ~state into PlayerState. 5 C→S send sites in do_sheathe, do_aggressive, UpdateItemSlot. | | | `OP_PlayEverquestRequest` | 🔴 Not-Set | | | | `OP_PlayEverquestResponse` | 🔴 Not-Set | | | -| `OP_PlayMP3` | 🟡 Unverified | | | +| `OP_PlayMP3` | 🟢 Verified | Passthrough. Inline case block @ 0x1401f7b7e (cmp-dispatch). Packet body = raw filename char*; passed directly to EqSoundManager::PlayScriptMp3. Volume sourced client-side from CLargeDialogWnd. | | | `OP_Poll` | 🔴 Not-Set | | | | `OP_PollResponse` | 🔴 Not-Set | | | -| `OP_PopupResponse` | 🟡 Unverified | | | -| `OP_PostEnterWorld` | 🟢 Verified | | | -| `OP_PotionBelt` | 🔴 Not-Set | | | +| `OP_PopupResponse` | 🟢 Verified | C→S only. `SendDialogResponseToServer` @ `0x1404ed680`. Sends `EDialogResponse` enum + popup ID (8-byte payload). Wire layout = `PopupResponse_Struct` exactly. Passthrough correct, no decoder needed. | | +| `OP_PostEnterWorld` | 🟢 Verified | Unused — marked `# unused` in patch conf; IDA confirms no S→C handler in HWM and no C→S hton send sites; no struct defined anywhere | | +| `OP_PotionBelt` | 🟠 Missing | | | | `OP_PreLogoutReply` | 🔴 Not-Set | | | -| `OP_PurchaseLeadershipAA` | 🔴 Not-Set | | | -| `OP_PVPLeaderBoardDetailsReply` | 🔴 Not-Set | | | -| `OP_PVPLeaderBoardDetailsRequest` | 🔴 Not-Set | | | -| `OP_PVPLeaderBoardReply` | 🔴 Not-Set | | | -| `OP_PVPLeaderBoardRequest` | 🔴 Not-Set | | | -| `OP_PVPStats` | 🔴 Not-Set | | | +| `OP_PurchaseLeadershipAA` | 🟠 Missing | | | +| `OP_PVPLeaderBoardDetailsReply` | 🟠 Missing | | | +| `OP_PVPLeaderBoardDetailsRequest` | 🟠 Missing | | | +| `OP_PVPLeaderBoardReply` | 🟠 Missing | | | +| `OP_PVPLeaderBoardRequest` | 🟠 Missing | | | +| `OP_PVPStats` | 🟠 Missing | | | | `OP_QueryResponseThing` | 🔴 Not-Set | | | -| `OP_QueryUCSServerStatus` | 🟢 Verified | | | -| `OP_RaidDelegateAbility` | 🔴 Not-Set | | | -| `OP_RaidClearNPCMarks` | 🔴 Not-Set | | | -| `OP_RaidInvite` | 🔴 Not-Set | | | +| `OP_QueryUCSServerStatus` | 🟢 Verified | C→S only. Client polls every ~30s via AddPlayerTimer::Stop (0x14009de50). Server ignores payload; responds with OP_SetChatServer+OP_SetChatServer2. TOB handled (ucsTOBCombined). Passthrough, no struct. | | +| `OP_RaidDelegateAbility` | 🟠 Missing | | | +| `OP_RaidClearNPCMarks` | 🟠 Missing | | | +| `OP_RaidInvite` | 🟠 Missing | | | | `OP_RaidJoin` | 🔴 Not-Set | | | -| `OP_RaidUpdate` | 🔴 Not-Set | | | -| `OP_RandomNameGenerator` | 🟢 Verified | The client no longer sends this packet (random name generation is done entirely in the client). The client will still accept this packet to set name (emu doesn't do this, but it's always been supported) | | -| `OP_RandomReply` | 🟢 Verified | | | -| `OP_RandomReq` | 🟢 Verified | | | -| `OP_ReadBook` | 🟡 Unverified | | | -| `OP_RecipeAutoCombine` | 🟡 Unverified | | | -| `OP_RecipeDetails` | 🟡 Unverified | | | -| `OP_RecipeReply` | 🟡 Unverified | | | -| `OP_RecipesFavorite` | 🟡 Unverified | | | -| `OP_RecipesSearch` | 🟡 Unverified | | | +| `OP_RaidUpdate` | 🟠 Missing | | | +| `OP_RandomNameGenerator` | 🟢 Verified | S→C only; no hton sends. Handler: CCharacterCreation::NameGenerated @ 0x14039cb30 (Pattern A, 128 bytes). Packet: 8-byte prefix (skipped) + null-terminated name; calls SetName() and enables Accept button. No server struct; emu never sends this opcode. | | +| `OP_RandomReply` | 🟢 Verified | Passthrough. Inline HWM (0x1401F4C85/0x1401F4CD4). BeingIgnored gate on name[+0xC]; reads low[+0], high[+4], result[+8] via _ltoa; self vs other roll via strncmp. Struct matches exactly. | | +| `OP_RandomReq` | 🟢 Verified | C→S only. No S→C handler. Passthrough. Client sends sorted low/high pair from /random cmd; matches RandomReq_Struct exactly. Send @ sub_1402266E0 (0x1402266E0). | | +| `OP_ReadBook` | 🟢 Verified | Bidirectional. ENCODE+DECODE added. BookRequest_Struct in tob_structs.h. TOB wire: window(u32)+type(u32)+target_id(u32)+invslot(8B)+can_cast(1B)+txtfile[8194]. Handler: msg_note_text@0x140211740. | | +| `OP_RecipeAutoCombine` | 🟢 Verified | Fixed S→C encoder: container_slot now serialized as 10B (no Padding2), eliminating 2-byte misalignment. Added C→S DECODE for 56B wire layout (HandleCombine). New RecipeAutoCombine_CS_Struct in tob_structs.h. | | +| `OP_RecipeDetails` | 🟢 Verified | Bidirectional passthrough (no encoder/decoder). S→C: sub_140521980@0x140521980 (Pattern A, 422B). 10-slot big-endian format. Fixed: appended htonl(trivial) at end of SendTradeskillDetails packet (zone/tradeskills.cpp); client was reading 4 garbage bytes at recipe[+84]. | | +| `OP_RecipeReply` | 🟢 Verified | Passthrough. Handler sub_14051F590@0x14051F590 (Pattern A). All 5 uint32 fields + recipe_name[64] read at correct offsets; matches RecipeReply_Struct exactly. | | +| `OP_RecipesFavorite` | 🟢 Verified | C→S only. Passthrough. Client sends TradeskillFavorites_Struct (2008 B): object_type + some_id + favorite_recipes[500]. Sent by CTradeskillWnd::ShowFavorites @ 0x140522960. Server size-validates. | | +| `OP_RecipesSearch` | 🟢 Verified | C→S only. Client sends 80-byte struct (object_type, some_id, mintrivial, maxtrivial, query[56], 2 unknowns). Send @ sub_14051FC10 (0x14051FC10). Passthrough; struct matches exactly. | | | `OP_ReclaimCrystals` | 🔴 Not-Set | | | -| `OP_RefreshBuffs` | 🟢 Verified | | | -| `OP_RefreshPetBuffs` | 🟢 Verified | | | -| `OP_RefreshTargetBuffs` | 🟢 Verified | | | +| `OP_RefreshBuffs` | 🟢 Verified | S→C. CBuffWindow::RefreshBuffs @ 0x14038fda0. mob_id & refresh_type skipped by client; tic_time, full_flag, count, per-buff fields, suspended all verified ok. | | +| `OP_RefreshPetBuffs` | 🟢 Verified | Bidi. CPetInfoWnd::RefreshPetBuffs @ 0x1404a0ae0. hit_number skipped by pet window (harmless); mob_id/refresh_type skipped; C→S is 6-byte UI-reload request, no server decoder. | | +| `OP_RefreshTargetBuffs` | 🟢 Verified | S→C. CTargetWnd::RefreshTargetBuffs @ 0x14050e510. Pattern D. Built by BuffComponent::RefreshBuffs; no ENCODE/DECODE. mob_id and hit_number sent but discarded by client. All fields verified ok. | | | `OP_ReloadUI` | 🔴 Not-Set | | | -| `OP_RemoveAllDoors` | 🟡 Unverified | | | -| `OP_RemoveBlockedBuffs` | 🟢 Verified | | | -| `OP_RemoveNimbusEffect` | 🟡 Unverified | | | -| `OP_RemoveTrap` | 🔴 Not-Set | | | -| `OP_Report` | 🟡 Unverified | | | -| `OP_ReqClientSpawn` | 🟢 Verified | | | -| `OP_ReqNewZone` | 🟢 Verified | Client does not send this (in LS or TOB), but it does receive it. emu does not send it | | -| `OP_RequestClientZoneChange` | 🟢 Verified | parity with RoF2, there's a string that gets passed to teleport at the end that's not known | | -| `OP_RequestDuel` | 🔴 Not-Set | | | -| `OP_RequestGuildTributes` | 🔴 Not-Set | | | +| `OP_RemoveAllDoors` | 🟢 Verified | Zero-byte signal; case block @ `0x1401ED50E` calls `EqSwitchManager::DeleteAll` @ `0x14025FBD0`; no packet payload, no encoder/decoder needed. | | +| `OP_RemoveBlockedBuffs` | 🟢 Verified | Bidirectional; encoder/decoder forward to `OP_BlockedBuffs`; wire = `uint32 Count + Count×int32 SpellID + uint8 Pet + uint8 Initialise`; S→C case block @ `0x1401f1e47`, deserializer `sub_140202750`. | | +| `OP_RemoveNimbusEffect` | 🟢 Verified | Passthrough. Case block 0x1401F8C65, handler sub_1401E2890 (24B); reads spawnid+0, nimbus_effect+4; calls CParticleSystemInterface vtbl[0x70]. | | +| `OP_RemoveTrap` | 🟠 Missing | | | +| `OP_Report` | 🟢 Verified | C→S only. Passthrough. Two send sites: `/report` cmd (sub_14021EE20 @ 0x14021ee20) sends `target|reporter|chatlog\0`; CMail::ReportInappropriateMail (0x1401680e0) sends mail body. BugReport_Struct does NOT match wire format. | | +| `OP_ReqClientSpawn` | 🟢 Verified | C→S only. Zero-byte signal packet, no struct or decoder needed. Sent from DoMainLoop@0x14026e670 (hton@0x14026e992) during world init ("Requesting initialization data."). | | +| `OP_ReqNewZone` | 🟢 Verified | S→C passthrough; emu never sends it, handler is dead. Inline case @ 0x1401e7ee6 via cmp-dispatch. Reads [Src+0xF4] (string-table ID, default 0x563) and [Src+0xF8] (class ID) from internal zone struct; displays zone-entry chat msg. Dead branch at loc_1401E804E (compiler artifact). | | +| `OP_RequestClientZoneChange` | 🟢 Verified | handler @ 0x1401f7040 (inline Pattern B); cont. @ 0x1401f713a calls DoTeleportB; zone_id/instance_id/y/x/z/heading pass correctly; type hardcoded 0x0b (ignores server field); message string at pkt+29 sent as zeros; tob_structs.h unknown032 offset annotation wrong (should be /*029*/ not /*032*/) | | +| `OP_RequestDuel` | 🟠 Missing | | | +| `OP_RequestGuildTributes` | 🟠 Missing | | | | `OP_RequestKnowledgeBase` | 🔴 Not-Set | | | -| `OP_RequestTitles` | 🔴 Not-Set | | | -| `OP_RespawnWindow` | 🟡 Unverified | | | -| `OP_RespondAA` | 🟢 Verified | | | -| `OP_RestState` | 🟡 Unverified | | | -| `OP_Rewind` | 🟡 Unverified | | | -| `OP_RezzAnswer` | 🔴 Not-Set | | | -| `OP_RezzComplete` | 🔴 Not-Set | | | -| `OP_RezzRequest` | 🔴 Not-Set | | | -| `OP_Sacrifice` | 🟡 Unverified | | | -| `OP_SafeFallSuccess` | 🟡 Unverified | | | +| `OP_RequestTitles` | 🟠 Missing | | | +| `OP_ResetAA` | 🟢 Verified | C→S only. Zero-payload signal packet; client sends opcode header only. Server handler reads no fields — opcode arrival triggers admin AA reset. Send fn sub_140174980 @ 0x140174980, hton @ 0x1401749a2. | | +| `OP_RespawnWindow` | 🟢 Verified | Passthrough correct. Inline CUnSerializeBuffer handler @ `0x1401eb890`: reads selected_bind_id, time_remaining, ui_flag, total_binds, then loops reading bind_number+WorldLocation(20B)+name+validity. Client sends empty 0x5c ACK. | | +| `OP_RespondAA` | 🟢 Verified | Handler sub_140217A60 @ 0x140217a60 (Pattern A). TOB adds aapoints_assigned[6] header; aa_list expanded to 300 entries vs server's 240. Fixed OOB read: loop now uses MAX_PP_AA_ARRAY (240); entries 240-299 zero-filled. | | +| `OP_RestState` | 🟢 Verified | Passthrough. No struct. Variable-length: 1B (0x01=combat) or 5B (0x00+uint32 timer). Handler sub_1402DA8E0 @ 0x1402da8e0; manual CSB reads. Updates LocalPC+0x2E2C/28/20. S→C only. | | +| `OP_Rewind` | 🟢 Verified | C→S only; zero-payload signal (no struct); send @ sub_1402326F0 (0x1402326F0); guard: bBeingFlung at [player+0x18F]; no S→C handler | | +| `OP_RezzAnswer` | 🟠 Missing | | | +| `OP_RezzComplete` | 🟠 Missing | | | +| `OP_RezzRequest` | 🟠 Missing | | | +| `OP_Sacrifice` | 🟢 Verified | Passthrough. S→C: handler sub_140214100 @ 0x140214100 (Pattern A) reads CasterID (+0x00) only; pops sacrifice confirm dialog (token 9054). C→S: zero-payload send from DialogResponse case 107 (user accepted). | | +| `OP_SafeFallSuccess` | 🟢 Verified | C→S only. Zero-byte notification sent by TakeFallDamage @ 0x140100520 when SafeFall skill (0x27) reduces fall damage. No struct, no decoder needed — passthrough correct. | | | `OP_SafePoint` | 🔴 Not-Set | | | -| `OP_Save` | 🟡 Unverified | | | -| `OP_SaveOnZoneReq` | 🟡 Unverified | | | -| `OP_SelectTribute` | 🔴 Not-Set | | | -| `OP_SendAAStats` | 🟡 Unverified | | | -| `OP_SendAATable` | 🟢 Verified | | | -| `OP_SendCharInfo` | 🟢 Verified | | | -| `OP_SendExpZonein` | 🟢 Verified | | | -| `OP_SendFindableNPCs` | 🔴 Not-Set | | | -| `OP_SendGuildTributes` | 🔴 Not-Set | | | -| `OP_SendLoginInfo` | 🟢 Verified | | | -| `OP_SendMaxCharacters` | 🟢 Verified | | | -| `OP_SendMembership` | 🟢 Verified | | | -| `OP_SendMembershipDetails` | 🟢 Verified | The struct is correct, will need reversing for actual option keys/values | | +| `OP_Save` | 🟢 Verified | C→S only. Passthrough — no encoder/decoder. `Save_Struct` is opaque (192 unknown bytes). Client sends 476-byte save blob via `CEverQuest::SavePC` @ `0x14028d640`. No S→C handler. | | +| `OP_SaveOnZoneReq` | 🟢 Verified | C→S only. Zero-byte packet sent by sub_14028DF20 @ 0x14028DF20 when zoning. Server Handle_OP_SaveOnZoneReq → Handle_OP_Save → Save(); payload ignored entirely. No decoder needed. | | +| `OP_SelectTribute` | 🟠 Missing | | | +| `OP_SendAAStats` | 🟢 Verified | Removed in TOB — no S→C handler in HandleWorldMessage, no C→S send sites, no server struct. Value 0x7416 retained in patch conf as documentation only; server must not send this opcode to TOB clients. | | +| `OP_SendAATable` | 🟢 Verified | S→C: msg_send_alt_data@0x140214200 → UnPackNetBuffer@0x1401a80d0. Pattern A. Encoder verified field-by-field. Effects base/limit zero-extend int32→int64 (positive values only in practice). No decoder. | | +| `OP_SendCharInfo` | 🟢 Verified | Handler: msg_send_characters@0x1402142a0, sub_140203150@0x140203150. Pattern D (inline CUnSerializeBuffer). Variable-length name, 9 equip slots, PreFTP=1 hardcoded to bypass FTP checks. No changes needed.| | +| `OP_SendExpZonein` | 🟢 Verified | Bidirectional handshake; S→C inline @ `0x1401f4923` sets `EverQuest_ReceivedWorldObjects=1`; client echoes 0-byte C→S response. No struct, passthrough. | | +| `OP_SendFindableNPCs` | 🟠 Missing | | | +| `OP_SendGuildTributes` | 🟠 Missing | | | +| `OP_SendLoginInfo` | 🟢 Verified | C→S only. Send @ `0x1402bfec8` in WorldAuthenticate (`0x1402bfb40`). No S→C handler. Passthrough — no encoder/decoder. sub_140246F50 packs login+passwd+empty as NUL-terminated strings; zoning=EverQuest_EnterZone; 0xCC hardcoded at offset 192. | | +| `OP_SendMaxCharacters` | 🟢 Verified | Handler `msg_player_info_header` @ `0x140211ae0` (938B, Pattern A). All 16 client-read fields set by encoder; values hardcoded (server struct has no TOB-field equivalents). add_marketplace_chars/add_unknown both 0 so conditional 0x76C4 responses never triggered. | | +| `OP_SendMembership` | 🟢 Verified | S→C only. FreeToPlayClient::UnSerialize @ 0x14067d6f0. Wire: uint8 membership, uint32 races/classes, uint32 entrysize, int32[33] entries. TOB struct packed; encoder correct; entries hardcoded to max. | | +| `OP_SendMembershipDetails` | 🟢 Verified | Handler: FreeToPlay::UnSerialize @ 0x14067d540 (Pattern D). Encoder fully hardcoded — 96 settings (4 tiers × 24 IDs), 17 race + 17 class entries, exit_url_length=0. Wire format matches client reads. | | | `OP_SendSystemStats` | 🔴 Not-Set | | | -| `OP_SendTitleList` | 🔴 Not-Set | | | -| `OP_SendTributes` | 🔴 Not-Set | | | -| `OP_SendZonepoints` | 🟢 Verified | | | -| `OP_SenseHeading` | 🟢 Verified | | | -| `OP_SenseTraps` | 🟡 Unverified | | | +| `OP_SendTitleList` | 🟠 Missing | | | +| `OP_SendTributes` | 🟠 Missing | | | +| `OP_SendZonepoints` | 🟢 Verified | S→C. Handler: msg_teleport_index @ 0x14029A9B0 (63B). Pattern A. XMM bulk copy, 32B/entry. TOB adds unknown024/028 (zeroed). Client cap: 127 entries. Encoder correct. | | +| `OP_SenseHeading` | 🟢 Verified | C→S only. Client sends 0-byte packet when Sense Heading skill succeeds. No payload, no decoder needed. Send: `sub_140103050` @ `0x140103050`, hton @ `0x1401031a0`. No S→C handler. | | +| `OP_SenseTraps` | 🟢 Verified | C→S only. Client sends 0-byte packet when Sense Traps skill (ID 62) is used. No payload, no decoder needed. Send: `UseSkill` @ `0x140100e60`, hton @ `0x140101740`. No S→C handler. | | | `OP_ServerListRequest` | 🔴 Not-Set | | | | `OP_ServerListResponse` | 🔴 Not-Set | | | | `OP_SessionReady` | 🔴 Not-Set | | | -| `OP_SetChatServer` | 🔴 Not-Set | | | -| `OP_SetChatServer2` | 🟢 Verified | | | -| `OP_SetFace` | 🔴 Not-Set | | | -| `OP_SetGroupTarget` | 🔴 Not-Set | | | -| `OP_SetGuildMOTD` | 🔴 Not-Set | | | -| `OP_SetGuildRank` | 🔴 Not-Set | | | -| `OP_SetRunMode` | 🟡 Unverified | | | -| `OP_SetServerFilter` | 🟢 Verified | | | -| `OP_SetStartCity` | 🔴 Not-Set | | | -| `OP_SetTitle` | 🔴 Not-Set | | | -| `OP_SetTitleReply` | 🔴 Not-Set | | | -| `OP_SharedTaskMemberList` | 🔴 Not-Set | | | -| `OP_SharedTaskAddPlayer` | 🔴 Not-Set | | | -| `OP_SharedTaskRemovePlayer` | 🔴 Not-Set | | | -| `OP_SharedTaskMakeLeader` | 🔴 Not-Set | | | +| `OP_SetChatServer` | 🟠 Missing | | | +| `OP_SetChatServer2` | 🟢 Verified | S→C only. Raw CSV text packet — no binary struct, no encoder/decoder. Handler sub_140208110 @ 0x140208110 parses 5 comma-sep fields; wire has 4 (3 commas), 5th absent→always false. Calls UniversalChatProxyConnect. | | +| `OP_SetFace` | 🟠 Missing | | | +| `OP_SetGroupTarget` | 🟠 Missing | | | +| `OP_SetGuildMOTD` | 🟠 Missing | | | +| `OP_SetGuildRank` | 🟠 Missing | | | +| `OP_SetRunMode` | 🟢 Verified | C→S only. Client sends when run mode changes in DoPassageOfTime @ 0x1400ec06f. uint32 payload (mode clamped 0/1) matches SetRunMode_Struct exactly. No S→C handler exists; no encoder/decoder needed. | | +| `OP_SetServerFilter` | 🟢 Verified | C→S only; no S→C handler. Decoder copies filters[0..28] from 69-entry TOB packet (276 bytes); remaining 40 TOB-only filters correctly dropped. send_update_filters @ 0x1402a0b00. | | +| `OP_SetStartCity` | 🟠 Missing | | | +| `OP_SetTitle` | 🟠 Missing | | | +| `OP_SetTitleReply` | 🟠 Missing | | | +| `OP_SharedTaskMemberList` | 🟠 Missing | | | +| `OP_SharedTaskAddPlayer` | 🟠 Missing | | | +| `OP_SharedTaskRemovePlayer` | 🟠 Missing | | | +| `OP_SharedTaskMakeLeader` | 🟠 Missing | | | | `OP_SharedTaskMemberInvite` | 🔴 Not-Set | | | -| `OP_SharedTaskInvite` | 🔴 Not-Set | | | -| `OP_SharedTaskInviteResponse` | 🔴 Not-Set | | | -| `OP_SharedTaskAcceptNew` | 🔴 Not-Set | | | -| `OP_SharedTaskMemberChange` | 🔴 Not-Set | | | -| `OP_SharedTaskPlayerList` | 🔴 Not-Set | | | -| `OP_SharedTaskSelectWindow` | 🔴 Not-Set | | | -| `OP_SharedTaskQuit` | 🔴 Not-Set | | | -| `OP_TaskTimers` | 🔴 Not-Set | | | -| `OP_Shielding` | 🔴 Not-Set | | | -| `OP_ShopDelItem` | 🟡 Unverified | | | -| `OP_ShopEnd` | 🟡 Unverified | | | -| `OP_ShopEndConfirm` | 🟡 Unverified | | | +| `OP_SharedTaskInvite` | 🟠 Missing | | | +| `OP_SharedTaskInviteResponse` | 🟠 Missing | | | +| `OP_SharedTaskAcceptNew` | 🟠 Missing | | | +| `OP_SharedTaskMemberChange` | 🟠 Missing | | | +| `OP_SharedTaskPlayerList` | 🟠 Missing | | | +| `OP_SharedTaskSelectWindow` | 🟠 Missing | | | +| `OP_SharedTaskQuit` | 🟠 Missing | | | +| `OP_TaskTimers` | 🟠 Missing | | | +| `OP_Shielding` | 🟠 Missing | | | +| `OP_ShopDelItem` | 🟢 Verified | Passthrough. Handler `sub_14020FA30`@`0x14020FA30` → `sub_140476CF0`@`0x140476CF0`. Client reads no payload fields; packet arrival triggers merchant window refresh only. Dispatched via cmp/jz sub-base. | | +| `OP_ShopEnd` | 🟢 Verified | Passthrough. S→C: inline case at 0x1401f6ff5; no body read, calls g_pMerchantWnd vtable[0x138](true) to close window. C→S: AboutToHide sends 8B (NPC spawn_id+player spawn_id); server uses INr (raw). | | +| `OP_ShopEndConfirm` | 🟢 Verified | Passthrough. Inline case @ 0x1401F61A2; no packet fields read. Calls g_pMerchantWnd->vtable[0x27](1) to close merchant window. Client never sends. | | | `OP_ShopItem` | 🔴 Not-Set | | | -| `OP_ShopPlayerBuy` | 🟡 Unverified | | | -| `OP_ShopPlayerSell` | 🟡 Unverified | | | -| `OP_ShopSendParcel` | 🟡 Unverified | | | -| `OP_ShopDeleteParcel` | 🟡 Unverified | | | +| `OP_ShopPlayerBuy` | 🟢 Verified | S→C: handler sub_1401E3E20@0x1401e3e20, encoder correct. C→S: fixed Merchant_Sell_Request_Struct from 20→24 bytes (added unknown20); client sends 24-byte payload, DECODE_LENGTH_EXACT was rejecting all | | +| `OP_ShopPlayerSell` | 🟢 Verified | Bidirectional. C→S decoder correct. Fixed S→C encoder: removed npcid from Merchant_Purchase_Response_Struct (client sub_14047A3A0@0x14047A3A0 reads TypelessInventorySlot at byte 0 and quantity at [rsi+8]; npcid was offsetting both). | | +| `OP_ShopSendParcel` | 🟢 Verified | Bidirectional. C→S: 224B RoF2 Parcel_Struct; inherited RoF2 decoder correct (TypelessInventorySlot→item_slot, send_to, note). S→C: no encoder; server sends empty (success) or 220B item_slot=0xFFFF (cancel). Handler sub_14020FAB0@0x14020FAB0→sub_14047B380@0x14047B380; unconditional UpdateSlots covers all cases. | | +| `OP_ShopDeleteParcel` | 🟢 Verified | S→C passthrough. Handler msg_merchant_mail_clearslot@0x14020fa40 reads [rcx+8] (parcel_slot_id+parcel_item_id as int64) → CMerchantWnd::ClearMailSlot. ParcelRetrieve_Struct matches. | | | `OP_ShopRespondParcel` | 🔴 Not-Set | | | -| `OP_ShopRetrieveParcel` | 🟡 Unverified | | | -| `OP_ShopParcelIcon` | 🟡 Unverified | | | -| `OP_ShopRequest` | 🟡 Unverified | | | -| `OP_SimpleMessage` | 🟢 Verified | | | -| `OP_SkillUpdate` | 🟢 Verified | | | -| `OP_Sneak` | 🟡 Unverified | | | +| `OP_ShopRetrieveParcel` | 🟢 Verified | Passthrough. S→C empty ack (SendParcelRetrieveAck). C→S uses ParcelRetrieve_Struct (merchant+player entity id, parcel_slot_id, parcel_item_id). Handler @ client_packet.cpp:17225. | | +| `OP_ShopParcelIcon` | 🟢 Verified | Passthrough S→C. ParcelIcon_Struct (uint32 status: 0=off,1=on,2=overlimit) matches client reads exactly. Inline case @ 0x1401f277f; drives show/hide+overlimit on CPlayerWnd (sub_140201A20/A40) and updates LocalPC (sub_140201690). | | +| `OP_ShopRequest` | 🟢 Verified | S→C handler sub_1401E3E60 @ 0x1401E3E60 (cmp-dispatch). Fixed close case: OUT(npc_id) always; player_id=0 for close triggers client close path. Decoder correct (4-byte npc_id only). | | +| `OP_SimpleMessage` | 🟢 Verified | Passthrough. Handler `msgTokenText` @ `0x140207BF0` (Pattern A). Reads `string_id`/`color`/`unknown8` as dwords at +0,+4,+8. String IDs 469/471/14261 also open CTipWnd. | | +| `OP_SkillUpdate` | 🟢 Verified | Handler sub_140214AB0 @ 0x140214ab0 (676B). Pattern A. Handles skills 0-99 and languages 100-131. active=1 hardcoded correctly; server struct omits active by design. | | +| `OP_Sneak` | 🟢 Verified | C→S only. Empty packet (size=0) sent to activate sneak. No struct or decoder needed. Send @ `sub_1401032A0` (0x1401032A0). S→C not handled by client. | | | `OP_Some3ByteHPUpdate` | 🔴 Not-Set | | | | `OP_Some6ByteHPUpdate` | 🔴 Not-Set | | | -| `OP_SomeItemPacketMaybe` | 🔴 Not-Set | | | -| `OP_Sound` | 🟡 Unverified | | | -| `OP_SpawnAppearance` | 🟢 Verified | | | -| `OP_SpawnDoor` | 🟢 Verified | | | +| `OP_SomeItemPacketMaybe` | 🟠 Missing | | | +| `OP_Sound` | 🟢 Verified | Passthrough. Inline handler @ `0x1401e9799` (Pattern B). Reads mob_id (+0x00), copper/silver/gold/platinum (+0x14–0x20) at correct offsets; plays sound WavePlay(0x8D). Ignores target_id, exp, faction, items. | | +| `OP_SpawnAppearance` | 🟢 Verified | Handler `msg_stat_change` @ `0x140215060` (cmp-dispatch @ `0x1401f3b90`). Fixed encoder: now sets both `eq->parameter` and `eq->lock_id` — half the TOBAppearance types (MaxHealth, HP, PVP, Sneak, Linkdead) read value from lock_id (offset +16). | | +| `OP_SpawnDoor` | 🟢 Verified | Handler @ 0x1401ED624 (inline Pattern B); EQSwitch ctor @ 0x14025E990 reads 132-byte _EQClientSwitch array; all server fields correct; extra TOB-only fields (AdventureDoorId, DynDoorID, RealEstateDoorID, speeds, flags) sent as 0 | | | `OP_SpawnPositionUpdate` | 🔴 Not-Set | | | -| `OP_SpecialMesg` | 🟢 Verified | | | -| `OP_SpellEffect` | 🟡 Unverified | | | -| `OP_Split` | 🟡 Unverified | | | -| `OP_Stamina` | 🟢 Verified | These values are 0-32k instead of 0-127 | | -| `OP_Stun` | 🟡 Unverified | | | -| `OP_Surname` | 🔴 Not-Set | | | -| `OP_SwapSpell` | 🟢 Verified | | | +| `OP_SpecialMesg` | 🟢 Verified | Inline @ `0x1401f6c6e` (Pattern B); encoder correct; link conversion applied to message | | +| `OP_SpellEffect` | 🟢 Verified | Passthrough. Handler sub_1402C0BE0 @ 0x1402c0be0. All struct fields matched. Unknown025 (+0x19) is a gate flag (0 = skip); Unknown026 (+0x1A) not read by client. | | +| `OP_Split` | 🟢 Verified | Passthrough. Bidirectional. S→C inline case @ 0x1401F1420 (Pattern B): reads platinum/gold/silver/copper at +0,+4,+8,+C; calls BuildMoneyText then displays chat. C→S: DoSplit @ 0x1402755E0. | | +| `OP_Stamina` | 🟢 Verified | Passthrough. Handler: msg_fwater_update @ 0x14020bc30. Pattern A. food@+0, water@+4 (both uint32). Client clamps to [0,32000]; original 0-127 comment reflects older protocol range. | | +| `OP_Stun` | 🟢 Verified | S→C only. Handler: `CharacterZoneClient::StunMe` @ `0x1401003C0` (Pattern A). Encoder correctly passes `duration`; `unknown004` sent as 0 (no server field); offsets +5/+6 hardcoded but not read by client. | | +| `OP_Surname` | 🟠 Missing | | | +| `OP_SwapSpell` | 🟢 Verified | Passthrough. Case block @ 0x1401eabca reads from_slot (+0) and to_slot (+4) inline → CSpellBookWnd::SwapSpellBookSlots @ 0x1404f6170. Send: HandleRightClickOnSpell @ 0x1404f40e0. | | | `OP_SystemFingerprint` | 🔴 Not-Set | | | -| `OP_TargetCommand` | 🟡 Unverified | | | -| `OP_TargetHoTT` | 🔴 Not-Set | | | -| `OP_TargetMouse` | 🟡 Unverified | | | +| `OP_TargetCommand` | 🟢 Verified | Passthrough. Bidirectional 4-byte SpawnID. S→C inline case @ `0x1401ef810` sets `g_pTargetPlayer`. C→S send sites in AdvancedLootWnd, `/target` handler (`sub_140235E60`), and zero-byte un-target in HWM. | | +| `OP_TargetHoTT` | 🟠 Missing | | | +| `OP_TargetMouse` | 🟢 Verified | Passthrough. C→S only. Sends `uint32 new_target` (spawn_id from `[g_pTargetPlayer+0x168]`, 0 to un-target). 3 send sites: DoPassageOfTime, CTargetWnd ctor, CTargetWnd::Init. Matches `ClientTarget_Struct`. | | | `OP_TargetReject` | 🔴 Not-Set | | | -| `OP_TaskActivity` | 🔴 Not-Set | | | -| `OP_TaskActivityComplete` | 🔴 Not-Set | | | -| `OP_TaskDescription` | 🔴 Not-Set | | | -| `OP_TaskHistoryReply` | 🔴 Not-Set | | | -| `OP_TaskHistoryRequest` | 🔴 Not-Set | | | -| `OP_TaskRequestTimer` | 🔴 Not-Set | | | -| `OP_TaskSelectWindow` | 🔴 Not-Set | | | -| `OP_Taunt` | 🟡 Unverified | | | -| `OP_TestBuff` | 🔴 Not-Set | | | -| `OP_TGB` | 🔴 Not-Set | | | -| `OP_TimeOfDay` | 🟢 Verified | | | -| `OP_Track` | 🟡 Unverified | | | -| `OP_TrackTarget` | 🟡 Unverified | | | -| `OP_TrackUnknown` | 🟡 Unverified | | | -| `OP_TradeAcceptClick` | 🟡 Unverified | | | -| `OP_TradeBusy` | 🟡 Unverified | | | -| `OP_TradeCoins` | 🟡 Unverified | | | -| `OP_TradeMoneyUpdate` | 🟡 Unverified | | | -| `OP_Trader` | 🔴 Not-Set | | | -| `OP_TraderBulkSend` | 🔴 Not-Set | | | +| `OP_TaskActivity` | 🟠 Missing | | | +| `OP_TaskActivityComplete` | 🟠 Missing | | | +| `OP_TaskDescription` | 🟠 Missing | | | +| `OP_TaskHistoryReply` | 🟠 Missing | | | +| `OP_TaskHistoryRequest` | 🟠 Missing | | | +| `OP_TaskRequestTimer` | 🟠 Missing | | | +| `OP_TaskSelectWindow` | 🟠 Missing | | | +| `OP_Taunt` | 🟢 Verified | C→S passthrough. Client sends target spawn_id (uint32 at [g_pTargetPlayer+0x168]) matching ClientTarget_Struct. Send @ sub_140102DF0 (0x140102DF0). Pre-send: range/LOS/inanimate checks. | | +| `OP_TestBuff` | 🟠 Missing | | | +| `OP_TGB` | 🟠 Missing | | | +| `OP_TimeOfDay` | 🟢 Verified | Passthrough S→C; case 0x1401edc8b → sub_1401E07F0; reads {hour,minute,day,month:uint8, year:uint32}; exact match with TimeOfDay_Struct. | | +| `OP_Track` | 🟢 Verified | Added ENCODE: uint16 count + variable-length null-terminated names per entry (entityid, distance, level, is_npc, name, is_pet, is_merc). Identical to RoF2. Handler: sub_14051CF50 @ 0x14051CF50. | | +| `OP_TrackTarget` | 🟢 Verified | C→S only. Passthrough. Client sends uint32 EntityID at +0x00 matching TrackTarget_Struct exactly. Two send sites: CTrackingWnd::NotifyServerOfTrackingTarget (0x14051cc10) and UpdateUsingSkill (0x14024a6d0). | | +| `OP_TrackUnknown` | 🟢 Verified | Zero-length C→S companion to OP_Track (skill 53, Tracking). Server handler is a no-op; no struct payload. | | +| `OP_TradeAcceptClick` | 🟢 Verified | Passthrough. S→C handler sub_140212A90 @ 0x140212a90 reads byte ptr [rcx+4] (flag from unknown4) to set trade-accepted state. C→S sends 8-byte struct from ClickedTradeButton/WndNotification. No issues. | | +| `OP_TradeBusy` | 🟢 Verified | Passthrough. S→C handler sub_140212C30 @ 0x140212C30: reads from_mob_id (+4) and type byte (+8). C→S send @ 0x140213020: sends to/from mob IDs + type. Type encoding consistent; no translation needed. | | +| `OP_TradeCoins` | 🟢 Verified | Passthrough. S→C only. Handler sub_140216740 @ 0x140216740. Reads slot (+4, byte, 0–3=copper/silver/gold/platinum) and amount (+8, dword). trader field ignored (client uses g_pTradeTarget). | | +| `OP_TradeMoneyUpdate` | 🟢 Verified | Passthrough. S→C only. Handler sub_140216670 @ 0x140216670. Reads type (byte +4, 0–3=copper/silver/gold/platinum) and amount (dword +8), adds to LocalPC wallet. trader field (+0) unused by client. | | +| `OP_Trader` | 🟠 Missing | | | +| `OP_TraderBulkSend` | 🟠 Missing | | | | `OP_TraderBuy` | 🔴 Not-Set | | | -| `OP_TraderDelItem` | 🔴 Not-Set | | | -| `OP_TradeRequest` | 🟡 Unverified | | | -| `OP_TradeRequestAck` | 🟡 Unverified | | | +| `OP_TraderDelItem` | 🟠 Missing | | | +| `OP_TradeRequest` | 🟢 Verified | Bidirectional passthrough. S→C handler `sub_140213020 @ 0x140213020` reads `from_mob_id` at [rbx+4]; opens trade window or sends `OP_TradeBusy` (0x43B8) with reason code. C→S send in `sub_140275D30`. | | +| `OP_TradeRequestAck` | 🟢 Verified | Bidirectional passthrough. S→C handler `sub_140208C50 @ 0x140208C50` (cmp-dispatch); reads `from_mob_id` at [rcx+4] to open trade window. C→S send in `sub_140528740 @ 0x140528971`; 8-byte `TradeRequest_Struct` matches exactly. | | | `OP_TraderItemUpdate` | 🔴 Not-Set | | | -| `OP_TraderShop` | 🔴 Not-Set | | | -| `OP_TradeSkillCombine` | 🟡 Unverified | | | +| `OP_TraderShop` | 🟠 Missing | | | +| `OP_TradeSkillCombine` | 🟢 Verified | Fixed: TOB struct now 28 bytes (added unknown0x18 at +24). DECODE_LENGTH_EXACT now accepts client's 28-byte packet. container_slot and guildtribute_slot decoded correctly. S→C: 0-byte ack, client calls DecItemPending. | | | `OP_TradeSkillRecipeInspect` | 🔴 Not-Set | | | -| `OP_Translocate` | 🟡 Unverified | | | -| `OP_TributeInfo` | 🔴 Not-Set | | | -| `OP_TributeItem` | 🔴 Not-Set | | | -| `OP_TributeMoney` | 🔴 Not-Set | | | +| `OP_Translocate` | 🟢 Verified | Passthrough correct. Handler sub_140217410 @ 0x140217410. Pattern D (manual reads + GetString). All 8 struct fields read at correct offsets. Complete=1→immediate teleport; Complete=0→popup dialog (DialogResponse sends 0x0611 back). | | +| `OP_TributeInfo` | 🟠 Missing | | | +| `OP_TributeItem` | 🟠 Missing | | | +| `OP_TributeMoney` | 🟠 Missing | | | | `OP_TributeNPC` | 🔴 Not-Set | | | -| `OP_TributePointUpdate` | 🔴 Not-Set | | | -| `OP_TributeTimer` | 🔴 Not-Set | | | -| `OP_TributeToggle` | 🔴 Not-Set | | | -| `OP_TributeUpdate` | 🔴 Not-Set | | | -| `OP_Untargetable` | 🟡 Unverified | | | -| `OP_UpdateAA` | 🟢 Verified | | | -| `OP_UpdateAura` | 🔴 Not-Set | | | -| `OP_UpdateLeadershipAA` | 🔴 Not-Set | | | -| `OP_VetClaimReply` | 🔴 Not-Set | | | -| `OP_VetClaimRequest` | 🔴 Not-Set | | | -| `OP_VetRewardsAvaliable` | 🔴 Not-Set | | | -| `OP_VoiceMacroIn` | 🟡 Unverified | | | -| `OP_VoiceMacroOut` | 🟡 Unverified | | | -| `OP_WeaponEquip1` | 🔴 Not-Set | | | -| `OP_WearChange` | 🟢 Verified | | | -| `OP_Weather` | 🟢 Verified | | | -| `OP_Weblink` | 🟡 Unverified | | | -| `OP_WhoAllRequest` | 🟡 Unverified | | | -| `OP_WhoAllResponse` | 🟡 Unverified | | | -| `OP_World_Client_CRC1` | 🟢 Verified | | | -| `OP_World_Client_CRC2` | 🟢 Verified | | | -| `OP_World_Client_CRC3` | 🟢 Verified | | | -| `OP_WorldClientReady` | 🟢 Verified | | | -| `OP_WorldComplete` | 🟢 Verified | | | +| `OP_TributePointUpdate` | 🟠 Missing | | | +| `OP_TributeTimer` | 🟠 Missing | | | +| `OP_TributeToggle` | 🟠 Missing | | | +| `OP_TributeUpdate` | 🟠 Missing | | | +| `OP_UnderWorld` | 🟢 Verified | C→S only. Client sends position (x,y,z floats) + uint32 at player+0x168 on underworld fall; 18-byte wire. After send, client auto-teleports via zoneHdr_fallThroughWorldTeleportId. No server decoder. | | +| `OP_Untargetable` | 🟢 Verified | Passthrough. Inline case block @ 0x1401EF975. Reads id (uint32 +0) via GetPlayerByID, targetable_flag (byte ptr +4) via SetTargetable vtable. Clears g_pTargetPlayer if flag=0 and player is target. | | +| `OP_UpdateAA` | 🟢 Verified | C→S only zero-payload trigger. Client sends during connect phase; server calls SendAlternateAdvancementPoints(). Send fn sub_1404EFE70 @ 0x1404efe70. No struct, no decoder needed. | | +| `OP_UpdateAura` | 🟠 Missing | | | +| `OP_UpdateLeadershipAA` | 🟠 Missing | | | +| `OP_VetClaimReply` | 🟠 Missing | | | +| `OP_VetClaimRequest` | 🟠 Missing | | | +| `OP_VetRewardsAvaliable` | 🟠 Missing | | | +| `OP_VoiceMacroIn` | 🟢 Verified | C→S only. 3 send sites: do_vtell/do_vraid/do_vgroup @ 0x140229950/CF0/A030. Wire format matches VoiceMacroIn_Struct exactly. Unknown132=Voice ID (GetMyVoice). Passthrough correct. | | +| `OP_VoiceMacroOut` | 🟢 Verified | Passthrough. Handler: VoiceManager::HandleMessage @ 0x14033d570. Reads From[64], Type, Voice, MacroNumber; all match server struct. Unknown068 unused; Unknown080 passed to sub_140242E90 for Tell type. | | +| `OP_WeaponEquip1` | 🟠 Missing | | | +| `OP_WearChange` | 🟢 Verified | Inline S→C @ 0x1401f5f48. All 8 fields confirmed: spawn_id (+0), wear_slot_id (+4), armor_id..new_armor_type (+8..+24 via rep movsb), color (+28). Encoder/decoder correct. | | +| `OP_Weather` | 🟢 Verified | Handler @ 0x1401ECE65, sub_14031BE00 @ 0x14031BE00. TOB wire is 16 bytes (4 uint32s); server struct 12 bytes. Client skips +0x08, reads mode at +0x0C. Added TOB struct + encoder to remap mode. | | +| `OP_Weblink` | 🟢 Verified | Deprecated in TOB client — not implemented. No handler found in HandleWorldMessage or sub-base scan, no C→S send. Passthrough is a no-op. | | +| `OP_WhoAllRequest` | 🟢 Verified | C→S only. TOB sends 176B (RoF2 sends 156B): guildid split into flag+id at 0x94/0x98, type moved to 0xA0. Decoder added; /who and /who all now functional. Trader/Buyer filters TODO (NaN sentinel unmapped). | | +| `OP_WhoAllResponse` | 🟢 Verified | Handler @ sub_1402182A0 (0x1402182a0). TOB client expects extra uint32 (0xFFFFFFFF) between PIDMSGID and Name per player. Encoder added (mirrors RoF2): inserts extra field, sets PIDMSGID=0 (no surname). | | +| `OP_World_Client_CRC1` | 🟢 Verified | C→S only. No struct/decoder. SendExeChecksum @ 0x14024aa10 sends 0x808-byte payload: crc32 + filesize + 256 sampled (offset, byte) pairs. World-server anti-cheat; EQEmu discards it. | | +| `OP_World_Client_CRC2` | 🟢 Verified | C→S only. No struct/decoder. SendBaseDataChecksum @ 0x14024a7b0 sends 0x808-byte payload: crc32 + filesize of Resources\BaseData.txt + 256 sampled (DWORD-idx, DWORD-val) pairs. EQEmu discards it. | | +| `OP_World_Client_CRC3` | 🟢 Verified | C→S only. No struct/decoder. SendSkillCapschecksum @ 0x14024ae10 sends 0x808-byte payload: crc32 + filesize of Resources\SkillCaps.txt + 256 sampled (DWORD-idx, DWORD-val) pairs. EQEmu discards it. | | +| `OP_WorldClientReady` | 🟢 Verified | C→S only. Zero-payload notification sent from DoCharacterSelection @ 0x14026cec0 (hton @ 0x14026cf8d). No struct, no decoder needed. Passthrough correct. | | +| `OP_WorldComplete` | 🟢 Verified | C→S only. 0-byte payload (opcode-only ACK). Client sends after receiving zone-connect data. Send @ 0x1401eeaaf in HandleWorldMessage. No S→C handler. No encoder/decoder needed. | | | `OP_WorldLogout` | 🔴 Not-Set | | | -| `OP_WorldObjectsSent` | 🟢 Verified | | | -| `OP_WorldUnknown001` | 🟢 Verified | SetServerTime. emu doesn't currently send it so setting it to verified, but the reference is 0x140292550 | | -| `OP_XTargetAutoAddHaters` | 🔴 Not-Set | | | -| `OP_XTargetOpen` | 🔴 Not-Set | | | -| `OP_XTargetOpenResponse` | 🔴 Not-Set | | | -| `OP_XTargetRequest` | 🔴 Not-Set | | | -| `OP_XTargetResponse` | 🔴 Not-Set | | | -| `OP_YellForHelp` | 🟡 Unverified | | | -| `OP_ZoneChange` | 🟢 Verified | | | +| `OP_WorldObjectsSent` | 🟢 Verified | Empty packet both ways; handler @ `0x1401f47e4` checks g_world, acks with C→S empty send at `0x1401f4882`. | | +| `OP_WorldUnknown001` | 🟢 Verified | SetServerTime (S→C). Handler `CEverQuest::SetServerTime` @ 0x140292550. Seeds CPacketScrambler with seeds at +0x00/+0x10; stores ServerTimeBase at +0x08. EQEmu doesn't use scrambler so packet never sent. | | +| `OP_XTargetAutoAddHaters` | 🟠 Missing | | | +| `OP_XTargetOpen` | 🟠 Missing | | | +| `OP_XTargetOpenResponse` | 🟠 Missing | | | +| `OP_XTargetRequest` | 🟠 Missing | | | +| `OP_XTargetResponse` | 🟠 Missing | | | +| `OP_YellForHelp` | 🟢 Verified | Passthrough. S→C: handler sub_140219A80 reads yeller spawn_id (+0) and target spawn_id (+4); server sends only 4 bytes so "with [target]" display is non-functional (server limitation, not TOB issue). | | +| `OP_ZoneChange` | 🟢 Verified | Bidirectional (100-byte TOB struct). S→C via ServerStatusPacketHandler (not HWM). Encoder/decoder correct; 3 extra unknown fields (068/072/096) are 0-filled/ignored. success-=1 for negative codes. | | | `OP_ZoneComplete` | 🔴 Not-Set | | | -| `OP_ZoneEntry` | 🟢 Verified | unknown fields in C->S struct are various CRCs, emu doesn't use them | | +| `OP_ZoneEntry` | 🟢 Verified | Bidir. S→C: ENCODE_FORWARD(OP_ZoneSpawns); handler msgEQAddPlayer@0x140205610 (Pattern D). C→S: 92-byte; decoder extracts char_name only; unknown00/68-88 are crc32/SpellFileCRC/fingerprint, unused. | | | `OP_ZoneGuildList` | 🔴 Not-Set | | | | `OP_ZoneInUnknown` | 🔴 Not-Set | | | -| `OP_ZonePlayerToBind` | 🟡 Unverified | | | -| `OP_ZoneServerInfo` | 🟢 Verified | | | +| `OP_ZonePlayerToBind` | 🟢 Verified | Inline CSB in HWM case @ 0x1401eb3bc. WorldLocation (20B: ZoneBoundID+Y+X+Z+Heading), zone_name string, then 3 ints (HP/End/mana — hardcoded 60/0/51). Extra WriteUInt32(41) not read by client; remove it | | +| `OP_ZoneServerInfo` | 🟢 Verified | S→C only. Inline Pattern B @ `0x1401ee9cc`. `rep movsb ecx=0x82` copies 130 bytes; port read at offset 128. Struct matches exactly. Client acks with OP_WorldComplete (0x1223) to world server. | | | `OP_ZoneServerReady` | 🔴 Not-Set | | | -| `OP_ZoneSpawns` | 🟢 Verified | This is deprecated in the client (and emu never sends it directly) | | -| `OP_ZoneUnavail` | 🟢 Verified | The client discards all content of this packet | | -| `OP_ResetAA` | 🟡 Unverified | | | -| `OP_UnderWorld` | 🟡 Unverified | | | +| `OP_ZoneSpawns` | 🟢 Verified | Deprecated in TOB client — case block @ 0x1401EEDDD calls fdebug only, no parsing. Encoder fans out each Spawn_Struct as individual OP_ZoneEntry (0x713D) packets. Client never receives this directly. | | +| `OP_ZoneUnavail` | 🟢 Verified | Passthrough. Inline case @ 0x1401EE4CC: sets global ZoneUnavailable=1, clears PendingCharacterName[0]. Client reads no packet fields — opcode arrival is the entire signal. | | diff --git a/utils/patches/patch_TOB.conf b/utils/patches/patch_TOB.conf index f6709f730..2274240af 100644 --- a/utils/patches/patch_TOB.conf +++ b/utils/patches/patch_TOB.conf @@ -577,7 +577,7 @@ OP_DisarmTraps=0x7362 OP_Disarm=0x5a91 OP_Sneak=0x7f05 OP_Fishing=0x3cdb -OP_InstillDoubt=0x3cdb +OP_InstillDoubt=0x0000 OP_FeignDeath=0x3d9f OP_Mend=0x3bac OP_Bind_Wound=0x580f diff --git a/zone/client.cpp b/zone/client.cpp index 389a5d230..cb7946407 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -2305,6 +2305,7 @@ void Client::SetStats(uint8 type,int16 set_val){ m_pp.CHA=set_val; break; } + *reinterpret_cast(iss->unknown13) = GetID(); QueuePacket(outapp); safe_delete(outapp); } @@ -2395,6 +2396,7 @@ void Client::IncStats(uint8 type, int16 increase_val) } break; } + *reinterpret_cast(iss->unknown13) = GetID(); QueuePacket(outapp); safe_delete(outapp); } diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index e23c1f621..cdaf808de 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -8868,9 +8868,9 @@ void Client::Handle_OP_Hide(const EQApplicationPacket *app) // newer client respond to OP_CancelSneakHide with OP_Hide with a size of 4 and 0 data if (app->size == 4) { auto data = app->ReadUInt32(0); - if (data) - LogDebug("Got OP_Hide with unexpected data [{}]", data); - return; + // only return if data is 0. data == 1 (or potentially non-zero) is the client requesting a hide roll + if (data == 0) + return; } if (!HasSkill(EQ::skills::SkillHide) && GetSkill(EQ::skills::SkillHide) == 0) diff --git a/zone/tradeskills.cpp b/zone/tradeskills.cpp index 14efb1762..a58f0575a 100644 --- a/zone/tradeskills.cpp +++ b/zone/tradeskills.cpp @@ -973,6 +973,22 @@ void Client::SendTradeskillDetails(uint32 recipe_id) { uint32 total = sizeof(uint32) + dist + datalen; + // The TOB client reads 4 bytes of trivial after the 10 component slots. + // Fetch the trivial from the recipe table and append it so the client doesn't read garbage. + uint32 trivial = 0; + std::string trivial_query = StringFormat( + "SELECT trivial FROM tradeskill_recipe WHERE id = %u LIMIT 1", recipe_id); + auto trivial_results = content_db.QueryDatabase(trivial_query); + if (trivial_results.Success() && trivial_results.RowCount() > 0) { + auto trow = trivial_results.begin(); + if (trow[0]) { + trivial = (uint32)Strings::ToInt(trow[0]); + } + } + uint32 trivial_net = htonl(trivial); + memcpy(buf + total, &trivial_net, sizeof(uint32)); + total += sizeof(uint32); + auto outapp = new EQApplicationPacket(OP_RecipeDetails); outapp->size = total; outapp->pBuffer = (uchar*) buf;