diff --git a/common/patches/IBuff.h b/common/patches/IBuff.h index b16078ff4..758c3be2f 100644 --- a/common/patches/IBuff.h +++ b/common/patches/IBuff.h @@ -48,6 +48,7 @@ public: virtual std::unique_ptr RefreshBuffs(EmuOpcode opcode, Mob* mob, bool remove, bool buff_timers_suspended, const std::vector& slots) const = 0; virtual void SetRefreshType(std::unique_ptr& packet, Mob* source, Client* target) const = 0; + virtual bool NeedsWearMessage() const = 0; uint32_t ServerToPatchBuffSlot(uint32_t slot) const; diff --git a/common/patches/titanium.cpp b/common/patches/titanium.cpp index a785cf968..b64a491d5 100644 --- a/common/patches/titanium.cpp +++ b/common/patches/titanium.cpp @@ -4056,6 +4056,8 @@ std::unique_ptr BuffComponent::RefreshBuffs(EmuOpcode opcod return nullptr; } +bool BuffComponent::NeedsWearMessage() const { return true; } + void BuffComponent::SetRefreshType(std::unique_ptr& packet, Mob* source, Client* target) const {} } /*Titanium*/ diff --git a/common/patches/titanium.h b/common/patches/titanium.h index ae049b172..356695be0 100644 --- a/common/patches/titanium.h +++ b/common/patches/titanium.h @@ -74,6 +74,7 @@ public: bool fade) const override; std::unique_ptr RefreshBuffs(EmuOpcode opcode, Mob* mob, bool remove, bool buff_timers_suspended, const std::vector& slots) const override; + bool NeedsWearMessage() const override; void SetRefreshType(std::unique_ptr& packet, Mob* source, Client* target) const override; }; diff --git a/common/patches/tob.cpp b/common/patches/tob.cpp index b344a8ac9..b6d2f01c2 100644 --- a/common/patches/tob.cpp +++ b/common/patches/tob.cpp @@ -3537,6 +3537,20 @@ namespace TOB emu->Initialise = init; } + 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. + // + DECODE_LENGTH_EXACT(BuffRemoveRequest_Struct); + SETUP_DIRECT_DECODE(BuffRemoveRequest_Struct, BuffRemoveRequest_Struct); + + emu->SlotID = TOBToServerBuffSlot(eq->SlotID); + + IN(EntityID); + + FINISH_DIRECT_DECODE(); + } + DECODE(OP_CastSpell) { DECODE_LENGTH_EXACT(structs::CastSpell_Struct); @@ -5632,24 +5646,22 @@ std::unique_ptr BuffComponent::BuffDefinition(Mob* mob, con affect->affect.slots[affect_slot].value = 0; // this is always 0 } - if (!fade) { - // affect info - affect->affect.caster_id.Id = buff.casterid; - affect->affect.caster_id.WorldId = RuleI(World, Id); - affect->affect.caster_id.Reserved = 0; - affect->affect.flags = 0; - affect->affect.spell_id = buff.spellid; - affect->affect.duration = buff.ticsremaining; - affect->affect.initial_duration = buff.ticsremaining; // TODO: this isn't correct, it's the total duration - affect->affect.hit_count = buff.hit_number; - affect->affect.viral_timer = 0; - affect->affect.modifier = static_cast(buff.instrument_mod) / 10.f; - affect->affect.y = static_cast(buff.caston_y); - affect->affect.x = static_cast(buff.caston_x); - affect->affect.z = static_cast(buff.caston_z); - affect->affect.type = 2; - affect->affect.level = buff.casterlevel > 0 ? buff.casterlevel : mob->GetLevel(); - } + // affect info + affect->affect.caster_id.Id = buff.casterid; + affect->affect.caster_id.WorldId = RuleI(World, Id); + affect->affect.caster_id.Reserved = 0; + affect->affect.flags = 0; + affect->affect.spell_id = buff.spellid; + affect->affect.duration = buff.ticsremaining; + affect->affect.initial_duration = buff.initialduration; + affect->affect.hit_count = buff.hit_number; + affect->affect.viral_timer = 0; + affect->affect.modifier = static_cast(buff.instrument_mod) / 10.f; + affect->affect.y = static_cast(buff.caston_y); + affect->affect.x = static_cast(buff.caston_x); + affect->affect.z = static_cast(buff.caston_z); + affect->affect.type = 2; + affect->affect.level = buff.casterlevel > 0 ? buff.casterlevel : mob->GetLevel(); //no idea if these are right; eqlib doesn't seem to know either if (buff.dot_rune > 0) @@ -5710,6 +5722,8 @@ std::unique_ptr BuffComponent::RefreshBuffs(EmuOpcode opcod return std::make_unique(opcode, std::move(buffer)); } +bool BuffComponent::NeedsWearMessage() const { return false; } + // 0 = self buff window, 1 = self target window, 2 = pet buff or target window, 4 = group, 5 = PC, 7 = NPC void BuffComponent::SetRefreshType(std::unique_ptr& packet, Mob* source, Client* target) const { diff --git a/common/patches/tob.h b/common/patches/tob.h index 8f9c511c1..8f15c9819 100644 --- a/common/patches/tob.h +++ b/common/patches/tob.h @@ -72,6 +72,7 @@ public: BuffDefinition(Mob* mob, const Buffs_Struct& buff, uint32_t slot, bool fade) const override; std::unique_ptr RefreshBuffs(EmuOpcode opcode, Mob* mob, bool remove, bool buff_timers_suspended, const std::vector& slots) const override; + bool NeedsWearMessage() const override; void SetRefreshType(std::unique_ptr& packet, Mob* source, Client* target) const override; }; diff --git a/common/patches/tob_ops.h b/common/patches/tob_ops.h index a727fd400..01226f487 100644 --- a/common/patches/tob_ops.h +++ b/common/patches/tob_ops.h @@ -69,6 +69,7 @@ D(OP_ApproveName) D(OP_AugmentInfo) D(OP_AugmentItem) D(OP_BlockedBuffs) +D(OP_BuffRemoveRequest) D(OP_CastSpell) D(OP_ChannelMessage) D(OP_CharacterCreate) diff --git a/tob/opcodes.md b/tob/opcodes.md index fe7af3407..0539e74af 100644 --- a/tob/opcodes.md +++ b/tob/opcodes.md @@ -69,7 +69,7 @@ Below is a status list for the 450 opcodes we currently use on the server for th | `OP_BoardBoat` | 🟡 Unverified | | | | `OP_BookButton` | 🟡 Unverified | | | | `OP_BuffDefinition` | 🟢 Verified | | | -| `OP_BuffRemoveRequest` | 🟡 Unverified | | | +| `OP_BuffRemoveRequest` | 🟢 Verified | | | | `OP_Bug` | 🟡 Unverified | | | | `OP_BuyerItems` | 🔴 Not-Set | | | | `OP_CameraEffect` | 🟡 Unverified | | | diff --git a/zone/client_version.cpp b/zone/client_version.cpp index d8c8db6d7..5a6c9d1e3 100644 --- a/zone/client_version.cpp +++ b/zone/client_version.cpp @@ -113,8 +113,13 @@ void ClientPatch::SendSingleBuffChange(Mob* sender, const Buffs_Struct& buff, in // first, send to self if self is client, which takes the definition and the refresh if (sender->IsClient()) { Client* c = sender->CastToClient(); - FastQueuePacket(c, &IBuff::BuffDefinition, GetClientComponent(c), sender, buff, slot, remove); - FastQueuePacket(c, &IBuff::RefreshBuffs, GetClientComponent(c), OP_RefreshBuffs, sender, remove, suspended, slots); + IBuff* component = GetClientComponent(c); + FastQueuePacket(c, &IBuff::BuffDefinition, component, sender, buff, slot, remove); + FastQueuePacket(c, &IBuff::RefreshBuffs, component, OP_RefreshBuffs, sender, remove, suspended, slots); + + // the client doesn't automatically do this for some reason pre-TOB + if (remove && component->NeedsWearMessage()) + c->SendColoredText(Chat::Spells, spells[buff.spellid].spell_fades); } // the rest of the buff packets do not take the definition, only the refresh @@ -131,8 +136,4 @@ void ClientPatch::SendSingleBuffChange(Mob* sender, const Buffs_Struct& buff, in QueueClientsByTarget(sender, ackreq, ShouldSendTargetBuffs, mutate)( &IBuff::RefreshBuffs, GetClientComponent, OP_RefreshTargetBuffs, sender, remove, suspended, slots); - - // the client doesn't automatically do this for some reason, only send it to the sender (TOB doesn't actually need this, but it doesn't double show the message) - if (remove && sender->IsClient()) - sender->CastToClient()->SendColoredText(Chat::Spells, spells[buff.spellid].spell_fades); } diff --git a/zone/client_version.h b/zone/client_version.h index 709fc6c2b..b1ca4c5d7 100644 --- a/zone/client_version.h +++ b/zone/client_version.h @@ -123,6 +123,7 @@ void FastQueuePacket(Client* c, Fun fun, Obj* obj, Args&&... args) if (app) { // FastQueuePacket specifically takes lifetime management of packet, so release here EQApplicationPacket* packet = app.release(); + LogNetcode("S->C FastQueuePacket {}", DumpPacketToString(packet)); c->FastQueuePacket(&packet); } } diff --git a/zone/common.h b/zone/common.h index 59d20d305..66effd484 100644 --- a/zone/common.h +++ b/zone/common.h @@ -228,6 +228,7 @@ struct Buffs_Struct { uint32 casterid; // Maybe change this to a pointer sometime, but gotta make sure it's 0'd when it no longer points to anything char caster_name[64]; int32 ticsremaining; + int32 initialduration; uint32 counters; uint32 hit_number; //the number of physical hits this buff can take before it fades away, lots of druid armor spells take advantage of this mixed with powerful effects uint32 melee_rune; diff --git a/zone/spell_effects.cpp b/zone/spell_effects.cpp index e53381d77..4a7900877 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -4243,9 +4243,6 @@ void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses) if(!IsValidSpell(buffs[slot].spellid)) return; - if (IsClient() && !CastToClient()->IsDead()) - ClientPatch::SendSingleBuffChange(this, buffs[slot], slot); - LogSpells("Fading buff [{}] from slot [{}]", buffs[slot].spellid, slot); const auto has_fade_event = parse->SpellHasQuestSub(buffs[slot].spellid, EVENT_SPELL_FADE); @@ -4650,8 +4647,12 @@ void Mob::BuffFadeBySlot(int slot, bool iRecalcBonuses) if (spells[buffs[slot].spellid].nimbus_effect > 0) RemoveNimbusEffect(spells[buffs[slot].spellid].nimbus_effect); - buffs[slot].spellid = SPELL_UNKNOWN; + // the client expects remaining duration to be 0 in the single packet change + buffs[slot].ticsremaining = 0; ClientPatch::SendSingleBuffChange(this, buffs[slot], slot, true); + + // don't set the spell to unknown until after the server has sent the single remove packets + buffs[slot].spellid = SPELL_UNKNOWN; ClientPatch::SendFullBuffRefresh(this); // we will eventually call CalcBonuses() even if we skip it right here, so should correct itself if we still have them diff --git a/zone/spells.cpp b/zone/spells.cpp index 9aa67990b..e9ee26f86 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -3720,6 +3720,7 @@ int Mob::AddBuff(Mob *caster, int32 spell_id, int duration, int32 level_override memset(buffs[emptyslot].caster_name, 0, 64); buffs[emptyslot].casterid = caster ? caster->GetID() : 0; buffs[emptyslot].ticsremaining = duration; + buffs[emptyslot].initialduration = duration; buffs[emptyslot].counters = CalculateCounters(spell_id); buffs[emptyslot].hit_number = spells[spell_id].hit_number; buffs[emptyslot].client = caster ? caster->IsClient() : 0;