|
From: <xer...@us...> - 2026-07-18 00:07:39
|
Revision: 1671
http://sourceforge.net/p/seq/svn/1671
Author: xerxes00
Date: 2026-07-18 00:07:36 +0000 (Sat, 18 Jul 2026)
Log Message:
-----------
Add pet buff window, buff-list decode, and buff-freeze zones
Decode OP_BuffList (self) and OP_PetBuffList (pet) as authoritative
per-spawn buff sets: add/update buffs with the server's real remaining
durations, prune buffs the server dropped (so clicking a buff off removes
it from the list), and resolve the owner by spawn id (an unresolved spawn
is skipped, never mis-attributed to the player). Skip the non-list marker
records, and show a spell with no spells_us.txt entry by its numeric id so
it never silently vanishes. Map OP_TargetBuffList and OP_ShortBuffList for
documentation (not yet wired).
Hold buff durations steady in buff-freeze zones (Plane of Knowledge, guild
lobby/hall, Nexus) for spells flagged duration-frozen in spells_us.txt
(new field-125 parse), matching the client. Trim the numeric instance
suffix from spawn names shown in the list.
Modified Paths:
--------------
showeq/branches/xerxes-trunk-dev/conf/zoneopcodes.xml
showeq/branches/xerxes-trunk-dev/src/interface.cpp
showeq/branches/xerxes-trunk-dev/src/spells.cpp
showeq/branches/xerxes-trunk-dev/src/spells.h
showeq/branches/xerxes-trunk-dev/src/spellshell.cpp
showeq/branches/xerxes-trunk-dev/src/spellshell.h
Modified: showeq/branches/xerxes-trunk-dev/conf/zoneopcodes.xml
===================================================================
--- showeq/branches/xerxes-trunk-dev/conf/zoneopcodes.xml 2026-07-17 20:05:20 UTC (rev 1670)
+++ showeq/branches/xerxes-trunk-dev/conf/zoneopcodes.xml 2026-07-18 00:07:36 UTC (rev 1671)
@@ -1008,4 +1008,20 @@
<comment></comment>
</opcode>
-->
+ <opcode id="0ddc" name="OP_BuffList" updated="07/15/26">
+ <comment>Authoritative per-spawn buff set (spellId + remaining ticks + caster); re-fires on every buff change.</comment>
+ <payload dir="server" typename="uint8_t" sizechecktype="none"/>
+ </opcode>
+ <opcode id="9090" name="OP_PetBuffList" updated="07/15/26">
+ <comment>Per-spawn buff set for the player's pet; same format as OP_BuffList, decoded by SpellShell::buffList.</comment>
+ <payload dir="server" typename="uint8_t" sizechecktype="none"/>
+ </opcode>
+ <opcode id="d702" name="OP_TargetBuffList" updated="07/15/26">
+ <comment>Buff set of the current target; same format as OP_BuffList. Mapped to silence the unknown log; not wired to the spell list (avoids target-buff clutter).</comment>
+ <payload dir="server" typename="uint8_t" sizechecktype="none"/>
+ </opcode>
+ <opcode id="0faf" name="OP_ShortBuffList" updated="07/15/26">
+ <comment>Self short-buff/song window (Chorus of Marr etc). Fixed-size format, distinct from OP_BuffList; mapped to silence the unknown log, decoder TBD.</comment>
+ <payload dir="server" typename="uint8_t" sizechecktype="none"/>
+ </opcode>
</seqopcodes>
Modified: showeq/branches/xerxes-trunk-dev/src/interface.cpp
===================================================================
--- showeq/branches/xerxes-trunk-dev/src/interface.cpp 2026-07-17 20:05:20 UTC (rev 1670)
+++ showeq/branches/xerxes-trunk-dev/src/interface.cpp 2026-07-18 00:07:36 UTC (rev 1671)
@@ -986,6 +986,11 @@
// connect the SpellShell slots to ZoneMgr signals
connect(m_zoneMgr, SIGNAL(zoneChanged(const QString&)),
m_spellShell, SLOT(zoneChanged()));
+ // track whether the current zone freezes buff durations
+ connect(m_zoneMgr, SIGNAL(zoneBegin(const QString&)),
+ m_spellShell, SLOT(updateFreezeZone(const QString&)));
+ connect(m_zoneMgr, SIGNAL(zoneChanged(const QString&)),
+ m_spellShell, SLOT(updateFreezeZone(const QString&)));
// connect the SpellShell slots to SpawnShell signals
connect(m_spawnShell, SIGNAL(killSpawn(const Item*, const Item*, uint16_t)),
@@ -1004,6 +1009,16 @@
m_packet->connect2("OP_Buff", SP_Zone, DIR_Server|DIR_Client,
"buffStruct", SZC_Match,
m_spellShell, SLOT(buff(const uint8_t*, size_t, uint8_t)));
+ // OP_BuffList is the authoritative per-spawn buff set, re-sent on every buff
+ // change with real remaining durations; buffList adds/updates and prunes.
+ // OP_PetBuffList is the same format for the player's pet, so it shares the
+ // handler (which scopes everything by the spawn id in the packet).
+ m_packet->connect2("OP_BuffList", SP_Zone, DIR_Server,
+ "uint8_t", SZC_None,
+ m_spellShell, SLOT(buffList(const uint8_t*, size_t, uint8_t)));
+ m_packet->connect2("OP_PetBuffList", SP_Zone, DIR_Server,
+ "uint8_t", SZC_None,
+ m_spellShell, SLOT(buffList(const uint8_t*, size_t, uint8_t)));
m_packet->connect2("OP_Action", SP_Zone, DIR_Server|DIR_Client,
"actionStruct", SZC_Match,
m_spellShell, SLOT(action(const uint8_t*, size_t, uint8_t)));
Modified: showeq/branches/xerxes-trunk-dev/src/spells.cpp
===================================================================
--- showeq/branches/xerxes-trunk-dev/src/spells.cpp 2026-07-17 20:05:20 UTC (rev 1670)
+++ showeq/branches/xerxes-trunk-dev/src/spells.cpp 2026-07-18 00:07:36 UTC (rev 1671)
@@ -176,6 +176,9 @@
m_buffDurationFormula = spellInfo[11].toUShort(); //spells_us.txt layout changed December 2018
m_buffDurationArgument = spellInfo[12].toUShort(); //spells_us.txt layout changed December 2018
m_targetType = uint8_t(spellInfo[32].toUShort()); //spells_us.txt layout changed December 2018
+ // durationfreeze (IS_COUNTDOWN_HELD): set on beneficial buffs that the server
+ // holds steady in buff-freeze zones (Plane of Knowledge, guild lobby, etc).
+ m_buffDurationFrozen = (spellInfo.count() > 125) && (spellInfo[125].toUShort() != 0);
for (size_t i = 0; i < playerClasses; i++)
m_classLevels[i] = uint8_t(spellInfo[38 + i].toUShort()); //spells_us.txt layout changed December 2018
Modified: showeq/branches/xerxes-trunk-dev/src/spells.h
===================================================================
--- showeq/branches/xerxes-trunk-dev/src/spells.h 2026-07-17 20:05:20 UTC (rev 1670)
+++ showeq/branches/xerxes-trunk-dev/src/spells.h 2026-07-18 00:07:36 UTC (rev 1671)
@@ -49,6 +49,7 @@
const QString& name() const { return m_name; }
uint8_t level(uint8_t class_) const;
uint8_t targetType() const { return m_targetType; }
+ bool buffDurationFrozen() const { return m_buffDurationFrozen; }
QString spellField(uint8_t field) const;
@@ -60,6 +61,7 @@
int16_t m_buffDurationFormula;
int16_t m_buffDurationArgument;
uint8_t m_targetType;
+ bool m_buffDurationFrozen;
uint8_t m_classLevels[playerClasses];
};
Modified: showeq/branches/xerxes-trunk-dev/src/spellshell.cpp
===================================================================
--- showeq/branches/xerxes-trunk-dev/src/spellshell.cpp 2026-07-17 20:05:20 UTC (rev 1670)
+++ showeq/branches/xerxes-trunk-dev/src/spellshell.cpp 2026-07-18 00:07:36 UTC (rev 1671)
@@ -129,7 +129,8 @@
m_player(player),
m_spawnShell(spawnshell),
m_spells(spells),
- m_lastPlayerSpell(0)
+ m_lastPlayerSpell(0),
+ m_freezeZone(false)
{
setObjectName("spellshell");
m_timer = new QTimer(this);
@@ -345,6 +346,131 @@
}
}
+// OP_BuffList: the authoritative buff set for a spawn, re-sent on every buff
+// change (zone-in, cast, click-off) with real remaining durations. It is the
+// source of truth for the spell list: it adds/updates the buffs it carries and
+// prunes any it dropped (which is how a clicked-off buff leaves the window).
+void SpellShell::buffList(const uint8_t* data, size_t len, uint8_t dir)
+{
+ if (dir == DIR_Client || len < 15)
+ return;
+
+ // byte 8 == 1 marks an authoritative full buff list; the server also sends
+ // byte 8 == 0 records (a single 0xffffffff slot) that are not a list - ignore
+ // them so the prune step below never runs against a bogus empty set.
+ if (data[8] != 1)
+ return;
+
+ uint32_t spawnId = *(const uint32_t*)data;
+ uint8_t count = data[9];
+ QList<uint16_t> listed;
+
+ // The list is scoped to the spawn id it carries: the player (OP_BuffList),
+ // the pet (OP_PetBuffList), etc. Resolve the owner - player when the id
+ // matches, otherwise the named spawn. An unresolved spawn is skipped rather
+ // than blamed on the player, so a not-yet-tracked pet can never prune the
+ // player's own buffs.
+ uint16_t targetId = uint16_t(spawnId);
+ QString targetName;
+ bool isPlayer = false;
+ if (spawnId == m_player->id())
+ {
+ targetName = m_player->name();
+ isPlayer = true;
+ }
+ else
+ {
+ const Item* tgt = m_spawnShell->findID(tSpawn, spawnId);
+ if (!tgt)
+ return;
+ targetName = tgt->name();
+ // Spawn names carry a numeric instance suffix (e.g. a trailing "00"); show
+ // the clean name in the spell list.
+ while (!targetName.isEmpty() && targetName.at(targetName.length() - 1).isDigit())
+ targetName.chop(1);
+ }
+
+ size_t pos = 15;
+ for (uint8_t n = 0; n < count && pos + 12 <= len; n++)
+ {
+ uint32_t spellId = *(const uint32_t*)(data + pos);
+
+ // Record head: spellId, remaining ticks at +4, then the printable caster
+ // name begins at +12.
+ int32_t remaining = *(const int32_t*)(data + pos + 4);
+ pos += 12;
+
+ QString caster;
+ while (pos < len && data[pos])
+ caster += QChar(data[pos++]);
+ if (pos < len) pos++; // NUL
+ pos += 4; // u32 slot
+
+ listed.append(uint16_t(spellId));
+
+ // A null spell (id not in this spells_us.txt - e.g. a new spell after a
+ // game patch, or no spell file) is still shown and tracked: update() falls
+ // back to the decimal spell id for the name so it stays lookupable.
+ const Spell* spell = m_spells->spell(spellId);
+
+ // remaining <= 0 => permanent buff (no timer; only a click-off removes it)
+ bool permanent = (remaining <= 0);
+ int duration = permanent ? 0 : remaining * 6;
+
+ // For the player's own buffs, match id-agnostically: the profile
+ // (buffLoad) and casts (selfStartSpellCast) may have keyed this buff under
+ // a different player spawn id (it churns at zone-in), so find it by spell +
+ // player name and adopt the current id, merging rather than duplicating.
+ SpellItem* item = NULL;
+ if (isPlayer)
+ {
+ for (QList<SpellItem*>::Iterator it = m_spellList.begin();
+ it != m_spellList.end(); ++it)
+ if (((*it)->spellId() == spellId) && ((*it)->targetName() == targetName))
+ {
+ item = *it;
+ break;
+ }
+ }
+ else
+ item = findSpell(spellId, targetId, targetName);
+
+ if (item)
+ {
+ item->update(spellId, spell, duration, 0, caster, targetId, targetName);
+ item->setPermanent(permanent);
+ emit changeSpell(item);
+ }
+ else
+ {
+ item = new SpellItem();
+ item->update(spellId, spell, duration, 0, caster, targetId, targetName);
+ item->setPermanent(permanent);
+ m_spellList.append(item);
+ if ((m_spellList.count() > 0) && (!m_timer->isActive()))
+ m_timer->start(1000 *
+ pSEQPrefs->getPrefInt("SpellTimer", "SpellList", 6));
+ emit addSpell(item);
+ }
+ }
+
+ // The list is authoritative for this spawn: prune tracked buffs it dropped.
+ // Match the player's buffs by name (id-agnostic) so stale-id entries left by
+ // the zone-in id churn are also pruned; other spawns match by id.
+ QList<SpellItem*> stripped;
+ for (QList<SpellItem*>::Iterator it = m_spellList.begin();
+ it != m_spellList.end(); ++it)
+ {
+ bool owned = isPlayer ? ((*it)->targetName() == targetName)
+ : ((*it)->targetId() == targetId);
+ if (owned && !listed.contains((*it)->spellId()))
+ stripped.append(*it);
+ }
+ for (QList<SpellItem*>::Iterator it = stripped.begin();
+ it != stripped.end(); ++it)
+ deleteSpell(*it);
+}
+
void SpellShell::action(const uint8_t* data, size_t, uint8_t)
{
const actionStruct* a = (const actionStruct*)data;
@@ -486,7 +612,7 @@
m_lastPlayerSpell = 0;
SpellItem* spell;
for(QList<SpellItem*>::Iterator it = m_spellList.begin();
- it != m_spellList.end(); it++)
+ it != m_spellList.end(); it++)
{
spell = *it;
@@ -496,6 +622,24 @@
}
}
+// Zones where the server holds buff durations steady (the client shows them
+// frozen). In these zones, spells flagged duration-frozen in spells_us.txt do
+// not count down in the spell list. This is a maintained list - append new
+// buff-freeze zones (short names) as they are found.
+void SpellShell::updateFreezeZone(const QString& shortZoneName)
+{
+ static const char* freezeZones[] = {
+ "poknowledge", "guildlobby", "guildhall", "nexus"
+ };
+ m_freezeZone = false;
+ for (size_t i = 0; i < sizeof(freezeZones) / sizeof(freezeZones[0]); i++)
+ if (shortZoneName == freezeZones[i])
+ {
+ m_freezeZone = true;
+ break;
+ }
+}
+
void SpellShell::killSpawn(const Item* deceased)
{
uint16_t id = deceased->id();
@@ -549,7 +693,13 @@
// permanent buffs (profile duration -1, e.g. illusions/forms) do not
// count down or expire - only a fade removes them.
- if (spell->permanent())
+ // In a buff-freeze zone the server holds durations steady, so a spell that
+ // is flagged duration-frozen in spells_us.txt (buffDurationFrozen) must not
+ // tick down here either. Both conditions are required: the zone freezes AND
+ // the spell is flagged - detrimental/non-frozen spells still count down.
+ const Spell* sp = m_spells->spell(spell->spellId());
+ if (spell->permanent() ||
+ (m_freezeZone && sp && sp->buffDurationFrozen()))
{
it++;
continue;
Modified: showeq/branches/xerxes-trunk-dev/src/spellshell.h
===================================================================
--- showeq/branches/xerxes-trunk-dev/src/spellshell.h 2026-07-17 20:05:20 UTC (rev 1670)
+++ showeq/branches/xerxes-trunk-dev/src/spellshell.h 2026-07-18 00:07:36 UTC (rev 1671)
@@ -202,10 +202,12 @@
void selfStartSpellCast(const uint8_t*);
void buffLoad(const spellBuff*);
void buff(const uint8_t*, size_t, uint8_t);
+ void buffList(const uint8_t*, size_t, uint8_t);
void action(const uint8_t*, size_t, uint8_t);
void simpleMessage(const uint8_t* cmsg, size_t, uint8_t);
void spellMessage(QString&);
void zoneChanged(void);
+ void updateFreezeZone(const QString& shortZoneName);
void killSpawn(const Item* deceased);
void timeout();
@@ -223,6 +225,7 @@
QList<SpellItem *> m_spellList;
SpellItem* m_lastPlayerSpell;
QTimer *m_timer;
+ bool m_freezeZone;
};
#endif
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|