802 Commits

Author SHA1 Message Date
Kelsi
8e97625ac1 fix(addons): a script error no longer reports itself
Lua errors were routed to addUIError, which fires UI_ERROR_MESSAGE — a Lua
event, and UIErrorsFrame is registered for it. Reporting a script error
therefore ran script, and when that script errored it was reported the same way:
fireEvent, handler, error, fireEvent, without end, through both the C++ and Lua
stacks inside a single frame.

Errors now go to a path that shows them without telling any script, which is
also where the real client puts them — UI_ERROR_MESSAGE carries the game's own
refusals, not broken addons.

Event dispatch additionally refuses to nest more than eight deep, so no other
pair of handlers can drive each other the same way, and names the event it
stopped.
2026-08-02 21:42:40 -07:00
Kelsi
3db9c397f7 fix: read WotLK spline flags at their own bit positions
The flag table was the pre-WotLK one and was being applied to 3.3.5 as
well, where the enum is MoveSplineFlag and every value from Final_Point
up sits one bit lower. That is not a misreported flag: the facing that
follows the flags is four bytes for an angle, eight for a target guid
and twelve for a point, so reading 0x10000 as Final_Point took twelve
bytes where the server had written eight and left the rest of the
movement block shifted — which is dropped as a failed update.

The same table decided whether monster-move waypoints are twelve bytes
each or four, testing Cyclic, Enter_Cycle and Animation where it meant
Catmullrom, Cyclic and Enter_Cycle.

The two parsers are already separate per expansion, so the WotLK one
takes its own values; the shared monster-move body is told which
generation it is reading, and defaults to the old one so nothing changes
for Classic or TBC without a caller saying so.
2026-08-02 07:42:53 -07:00
Kelsi
9b3cb1bf72 feat(pet): let a companion be dismissed by pressing it again
There was no way to put a companion away. It has no aura, so nothing appears in
the buff bar to right-click, and pressing it in the spellbook only ever summoned
— producing the same critter again. CMSG_DISMISS_CRITTER was in the opcode
tables and nothing sent it.

A companion announces itself only through UNIT_FIELD_CRITTER on the player, so
that field is now read and the guid remembered along with the spell that called
it, identified the same way the mount aura is: the spell just cast from the
ground, rather than a guess at "some spell", which would fire the toggle on the
wrong button. Casting that spell again dismisses instead of summoning.

The local state is left alone until the server clears the field, so a dismiss it
refuses does not leave the client believing the companion is gone.

WotLK only, on both halves: no earlier expansion publishes the field or carries
the opcode. On those the field index resolves to missing and nothing changes.

Tested by pinning the field index against neighbours whose values are
independently known — a wrong index here would not fail, it would quietly read
whatever sits at that offset and hand the dismiss a guid that is not a
companion — and by asserting the older expansions claim no index at all.
2026-07-31 22:21:49 -07:00
Kelsi
499e9f869d fix(mount): stop a dismount blinking the mount back on mid-way
Dismounting made the character strike a pose. The mount was going off, back on,
and off again in about two hundred milliseconds, and the character was caught
holding the seated rider animation in between — which on an unmounted body,
arms forward and legs apart, reads as a pose.

The player's mount display field keeps its old value for a few frames after a
dismount request goes out. The values-update handler took that at face value and
re-mounted them seven milliseconds after they got off. The restored value then
made the server's own SMSG_DISMOUNT look transient, so that was discarded too —
throwing away the one authoritative confirmation the dismount had — and the
player stayed mounted until the field finally caught up.

A locally-initiated dismount now marks itself pending for a moment. While it is,
a still-set mount field is not believed, and an SMSG_DISMOUNT is taken rather
than dismissed, since a stale field is precisely what that packet is arriving to
correct. Either the field reading zero or the packet clears the pending mark, and
it times out on its own so a dismount the server refuses recovers instead of
leaving the player unable to look mounted.
2026-07-31 22:02:39 -07:00
Kelsi
76bf48c874 feat(transport): animate transports on the server's route clock
A WotLK MO_TRANSPORT publishes where it is on its route: GAMEOBJECT_LEVEL is
the route's period in milliseconds, and the high int16 of GAMEOBJECT_DYNAMIC is
how far through that period the hull currently is, as a fraction of 65535.
Neither field was being read, so the client animated on a period it worked out
for itself from distance over speed — and when that came out shorter than the
server's, the ferry simply lapped its shore until the schedule caught up.

The phase is a fraction, so it maps onto whatever timeline the client's spline
has without the two periods needing to agree, and it keeps agreeing as the ride
goes on because it advances at the server's rate rather than a derived one.

Both fields are WotLK-only; nothing earlier published a transport's phase. On
those expansions fieldIndex returns 0xFFFF, the clock is never adopted, and the
existing local animation runs unchanged — covered by a test, along with the
wrap and a zero period, which is what every non-transport GameObject reports.

This syncs the cycle, not the position within it. The client still animates one
map's slice of a cross-continent route on its own geometry, so where the hull
sits at a given phase is still the client's own answer; what changes is that it
completes exactly one cycle per server cycle instead of several. Matching the
position too means mapping the phase onto the sub-interval of the full route
that belongs to this map, which is a larger change to how slices are built.
2026-07-31 20:57:24 -07:00
Kelsi
87d8614f18 fix(transport): wait at the pier between crossings; let a rider step ashore
Two things about the Kraken.

It lapped Borean Tundra several times instead of leaving. A cross-continent
route is split into one slice per map and each slice is animated on its own,
sized from its own nodes alone — so its cycle is far shorter than the server's
and the boat simply ferried its shore over and over until the transfer came
due. Each slice now measures the whole route, across every map it touches, and
spends the difference held at the pier: one departure per server cycle, the
boat plainly waiting where a passenger would expect to find it, and never
parked offshore, which is the case the old comment rightly warned about.

And a rider who walked off onto the dock stayed attached to the ship. The
disembark footprint is deliberately generous — larger than any hull — so it
only catches someone clearly away from the ship, and stepping onto the pier
alongside leaves them well inside it. What tells ashore from aboard is having
no deck underfoot, which this was already measuring and using for nothing but
its log line. It now decides, counted over several frames so that jumping is
not mistaken for stepping off.
2026-07-31 20:44:42 -07:00
Kelsi
676e758121 fix(transport): stop a docked ship's machinery instead of running it forever
The Kraken's paddlewheel kept turning while the ship sat at the pier. The
doodad's animation was set once, when it spawned, and never revisited — so
whatever it was given at creation is what it played for the rest of the
session.

Both machinery models carry the sequences for this: ShipStart, ShipMoving and
ShipStop, plus an idle. The animator already knows when a hull is holding at an
authored dock stop, so it records that, and the manager pushes ShipMoving or
ShipStop to the hull's children when it changes. Doodads lacking the sequence
are untouched, which leaves the barrels and lanterns on the same deck alone.

The doodad count is tracked alongside the state because doodads stream in over
several frames: a doodad attached after the last push would otherwise keep the
animation it spawned with, which is the bug again on a smaller scale.

Tests cover the flag going true at the stop and false again on departure — a
stop that never released would simply swap a permanently spinning wheel for a
permanently stopped one.
2026-07-31 20:37:56 -07:00
Kelsi
726bd979f6 fix(transport): free a rider frozen on a ship's ramp; place doodads on attach
Boarding the icebreaker left the character running on the spot and unable to
get off. The deck-floor hold is the cause. It exists for one situation — a
continent transfer registers the transport GO before its WMO collision has
finished uploading — and while it is engaged it discards the rider's walking
motion and reapplies the boarding offset every frame.

But the flag is set on boarding and cleared only by a successful deck query, so
boarding somewhere the query never succeeds keeps it engaged forever, and a
gangway that belongs to the pier rather than the hull is exactly such a place.
The rider then cannot move, cannot walk far enough to trigger disembark, and
so cannot leave. The hold now applies only while the hull's collision is
genuinely still loading; once it is there, a query that finds no deck is a real
answer and the flag is released.

Also, addDoodadToInstance silently did nothing when the parent instance was
missing. The M2 has already been created by then, so dropping it there strands
it at the world origin, drawn nowhere and owned by nothing — indistinguishable
from a load failure. It now says so, and a doodad is moved into place as it is
attached rather than waiting for the parent's next transform push, so it is
never drawn at the origin and never stays there if the parent is static.

The machinery diagnostic now reports where the doodad actually ended up.
"Spawned" only ever meant the model parsed.
2026-07-31 20:20:43 -07:00
Kelsi
2d2e983d29 fix(transport): every hull's bow is at model -X, so they all take the same offset
The route yaw is derived on the premise that a transport WMO faces model-space
+X, with a table naming the icebreaker and the night-elf ferry as the two
bow-reversed exceptions. Measuring the art says the premise is backwards and
the exceptions are the models that need none.

Two independent measurements over every .wmo under World\wmo\transports. The
hulls taper to a point at -X and stay blunt at +X — transportship 1.0 against
10.1 half-width at the two ends, the icebreaker 4.1 against 14.8, and the same
for the NE ferry, the UD and pirate ships, both zeppelins and the battleships.
And the icebreaker is a paddle steamer whose ICEBREAKER_PADDLEWHEEL doodad,
which belongs at the stern, sits at x=+36.3 on a hull spanning -60.7..+50.1.

So the offset is PI for all of them and the table is gone. The Bravery, which
the table gave no correction at all, was sailing stern-first as a result.

The table could never have been right, because it was fitted while facing came
from a frozen server yaw rather than from the route — what it was correcting
was not the hull. That also empties the docked-restore-spawn-yaw branch, whose
condition was "bow offset is zero"; it is removed rather than left looking live,
and a docked ship keeps its corrected arrival heading as the parallel-berth
ships already did.

Five tests asserted the old table, including one built entirely around the
per-model distinction. Rewritten against the measurements.
2026-07-31 19:59:37 -07:00
Kelsi
a0c4c59791 fix(pet): send the dismiss command instead of telling the pet to stay
CMSG_PET_ACTION's action field is a pair, not an id: the high byte says what
kind of action it is and the low 24 bits say which one. Commands and stances
reuse the same small numbers, so 1 is follow as a command and defensive as a
stance, and only the byte above them tells the two apart.

Four callers each read that differently. Dismiss packed action 0 under the
command type, which is COMMAND_STAY — the pet planted itself and stayed. The
Lua bindings had the type and the action swapped. The chat commands sent a
bare 1..6 with no type byte at all. The action bar labelled slots off a flat
1..6 numbering that does not exist on the wire, so it mislabelled the built-in
buttons and left the id-0 ones blank, and the stance row matched stance ids
against command slots.

The encoding now lives in game/pet_action.hpp and every caller builds through
it. Bar slots the server sends down were already packed correctly and still go
back unchanged, which is why clicking the bar worked while nothing else did.
2026-07-31 19:30:20 -07:00
Kelsi
4ff809f1ba fix(mail): show who the mail is from
The inbox printed mail.senderName, which nothing filled. There is a backfill
that writes it when a name-query response arrives for a mail sender, but no
query was ever sent, so it never ran and every message showed an empty From
line.

Ask for the name when the inbox is parsed, which lets that backfill work and
keeps Reply — which reads the same field — pointing at a real character.

Non-player mail was never covered by the backfill at all. The wire carries a
GUID only for mail from a player; auction, creature and game object mail
carry an entry instead, and the parsers already keep them apart. The sender
is now resolved by type: a name from the GUID, "Auction House" for auction
mail, and the creature or game object name for the rest.

Resolved where it is drawn rather than stored, so a name that arrives after
the inbox was parsed appears on the next frame rather than staying blank
until the mailbox is reopened.
2026-07-31 19:09:49 -07:00
Kelsi
8b2f003bc6 feat(inventory): sorting merges partial stacks before it orders them
Sorting arranged what was there without consolidating it, so two half stacks
of linen stayed two half stacks — just adjacent. Pour them together first,
then sort, so the result is one stack of twenty rather than two of ten.

On the wire a merge is a swap: dropping a stack onto another of the same item
makes the server move what fits and leave the rest behind. So the merges go
through the same queue as the sort, ahead of it, and the arithmetic mirrors
what the server will do — pour into the earliest partial, stop when the
destination is full, drop the source slot when it empties.

Merging plans and applies in one pass rather than splitting into a compute
and an apply the way the sort does. Those two have to be kept in agreement by
hand, and there is no reason to add a third pair.

Skips what the sort skips: full stacks, items that do not stack, and special
containers. The bank's Sort All merges too.
2026-07-31 19:03:07 -07:00
Kelsi
6fcd1d1ad2 fix(spell): let Disenchant pick the item it works on
Casting Disenchant sent CMSG_CAST_SPELL with an empty SpellCastTargets, so
the server evaluated "can this be disenchanted?" against nothing and answered
that it could not. There was no way to choose an item: CastSpellPacket could
target a unit or a game object, and nothing else.

Spell.dbc says which spells want one. Disenchant, Prospecting, Milling and
the enchant formulas all carry TARGET_FLAG_ITEM in Targets, so the rule is
the flag rather than a list of spell ids. Casting one now arms the same
item-picking cursor an enchanting scroll already uses — the click handler,
the greyed-out slots and the Escape/right-click cancel all come with it —
and sends the cast with the chosen item once it is picked.

CastSpellPacket gains an item-target build to go with the unit and game
object ones, routed through the packet parsers so an expansion can override
the layout.
2026-07-31 18:49:57 -07:00
Kelsi
2a8818586d fix(equipment): helms went on the shield mount, and the show-helm toggle did nothing
Three faults, which together left a bald head and no helmet.

The helm was attached at point 0. That is the shield mount — 11 is the helm,
which the fallback would have used had the first attempt not succeeded. So it
attached, reported success, and hung off the forearm. Detaching 0 on every
equipment refresh was dropping the shield with it.

Show Helm flipped a bool, sent CMSG_SHOWING_HELM and printed a message.
Nothing read the flag. It now suppresses the model, restores the hair, and
marks equipment dirty so both happen when the button is pressed.

Hair was hidden for any head item at all, so a circlet left the character
bald and wearing nothing visible. Whether a head item covers hair is in the
data: ItemDisplayInfo points at a HelmetGeosetVisData row per gender, and the
row crowns and circlets use is all zeroes while a plate helm's is not. The
columns move between the 23-field and 25-field builds — 12/13 against 13/14 —
so they are found by asking which columns reference the visibility table
rather than kept as a per-expansion index, which is how the facial-feature
columns went wrong.
2026-07-31 18:32:25 -07:00
Kelsi
df987136c0 feat(spell): cast heals and buffs on yourself when nothing friendly is targeted
Healing yourself mid-fight meant dropping the target, casting, and picking it
back up again. Retail falls back to the caster when a beneficial spell is cast
with nothing friendly selected.

A heal and a nuke cannot be told apart by effect id or school — both are
APPLY_AURA, both can be Holy. Spell.dbc's EffectImplicitTargetA can: it is
what the spell expects to be aimed at. Verified against the shipped data,
where Flash Heal, Rejuvenation, Mark of the Wild, Arcane Intellect and
Blessing of Might all read 21 while Smite, Fireball, Shadow Bolt and Shadow
Word: Pain read 6. The column moves with the expansion — 82 in vanilla and
Turtle, 86 in TBC and WotLK — so it is added to each layout with the index
checked against that expansion's own file.

Only the "needs an ally" case falls back. A spell that takes either target,
like Dispel Magic, is left alone: choosing between cleansing yourself and
purging an enemy is guessing at what the keypress meant. Self-only spells
already resolve through their range and are untouched.
2026-07-31 17:29:49 -07:00
Kelsi
92a2cdba31 refactor(facing): one way to face something, one conversion to get there
The Smite fix worked but was a patch on one call site, and it hand-inverted
a conversion that already existed elsewhere — the same mistake that made the
bug possible.

Facing lives in two representations: the renderer holds the character's yaw
in degrees, the game side holds canonical yaw in radians, and the frame loop
converts render to game every frame. That makes the renderer the source of
truth, so setting movementInfo.orientation and sending MSG_MOVE_SET_FACING
is always undone on the next frame. Three places did exactly that: casting
at a target, using a game object, and correcting after the server answers
SMSG_ATTACKSWING_BADFACING. The first was the reported bug; the second is
the same failure waiting for a gather node, which has a cast time; the third
told the server to keep telling us.

They now share GameHandler::faceCanonicalYaw, which turns the character and
sends the packet. Both directions of the degrees/radians conversion move
into core::coords, replacing four hand-written copies — two of which were
inverting the other two from memory — with one documented pair, and a test
that walks a full turn checking they are still inverses.
2026-07-31 17:19:57 -07:00
Kelsi
12d67befcd fix(spell): face the target for real, not just in the packet
Smite failed with "unit not in front" on a target that was plainly in front.
The log dates the failure precisely: the cast goes out at 14.688 and the
rejection arrives at 16.346, 1.66s later — Smite's cast time. The server
accepted the facing at cast start and rejected it at completion.

Facing a target before a cast only set movementInfo.orientation and sent
MSG_MOVE_SET_FACING. But the frame loop resyncs orientation from the
character's visual facing every frame and re-sends it whenever it has moved
more than three degrees, so a facing that exists only in the packet is
undone on the very next frame. Instant spells land before that happens;
anything with a cast time is re-checked against the restored orientation and
fails the arc.

Turn the character as well, so the visual facing and the orientation on the
wire agree and go on agreeing for the length of the cast. Fishing already
did this for the same reason. It is also what retail does — casting at
something turns you to face it.
2026-07-31 17:15:16 -07:00
Kelsi
7805e3a070 fix(mail): stop offering twelve attachments where the packet carries one
Vanilla's CMSG_SEND_MAIL has a single uint64 item GUID where TBC and later
have a count followed by an array, and the classic builder writes only
itemGuids[0]. The compose window offered twelve slots on every expansion
regardless, so attaching a dozen items on a 1.12 realm sent the first and
left the other eleven sitting in the player's bags with no error, no
message, and a mail that looked like it had gone out whole.

Derive the attachment limit from the wire format: one on classic and turtle,
twelve from TBC on. The compose window sizes itself to that, attaching stops
at it, and sendMail refuses outright rather than dropping anything if the
two ever disagree again.
2026-07-31 16:12:24 -07:00
Kelsi
4f9fac561d feat(bank): sort an individual bank bag
The bank had one Sort button and it sorted everything, which pools every
item into the main slots — the wrong tool for a bag being kept as a
category, since sorting it empties it into the bank proper.

Add sortBankBag and computeBankBagSortSwaps, which order one bag's contents
by the same rule as everywhere else (quality desc, then item id, then stack
size) and address only that bag's container, and put a Sort button on each
bag's header in the grouped view. The existing button becomes "Sort All" so
the difference in scope is legible before it is clicked rather than after.

Both feed the bank's existing swap queue, so the server sees one
CMSG_SWAP_ITEM per frame either way.
2026-07-31 15:49:44 -07:00
Kelsi
d32667192f feat(mount): pressing the mount you are riding dismounts you
castSpell dismounts before casting, so crafting and the like work while
mounted. For a mount spell that meant the button dismounted the player and
then immediately put them back on the same mount, which reads as the button
doing nothing at all. Retail dismounts and stops there.

Recognise the case and return after the dismount. Pre-WotLK mounts are
items rather than spells, so dispatchUseItem gets the same rule — it already
resolves the item's on-use spell, and both bag and backpack use funnel
through it, so a mount used from a bag behaves the same as one on the bar.

This needs the mount's spell id to be right, and it was only a guess: on
mounting, the client scanned for an indefinite self-cast aura and kept the
last match, which is as likely to be a racial or a tracking buff. It now
prefers the spell the player just cast or the item they just used, falling
back to the old scan when neither matches.
2026-07-31 15:09:37 -07:00
Kelsi
c3b909c393 fix(gameobject): fishing schools were harvestable with a right-click
A fishing school is GAMEOBJECT_TYPE_FISHINGHOLE (25), and the only way to
take from one is to fish in it — retail does not even give it an interact
cursor. Clicking one here ran the ordinary game object path: CMSG_GAMEOBJ_USE,
and while the object's query response was still outstanding a CMSG_LOOT as
well, since unknown metadata is treated as possibly-a-container so the first
click on a quest container is not lost. The server answers that with the
hole's loot, so the school emptied in one click.

Ignore the click on type 25 outright, send nothing, and drop it from the
click-picking candidates alongside decorative GENERIC objects so a school
floating in front of a creature cannot take the click either. The deferred
loot open re-checks the type before it fires, which covers the case the
click landed on before the metadata arrived.

Fishing bobbers are GAMEOBJECT_TYPE_FISHINGNODE (17), so reeling in a catch
is untouched.
2026-07-31 15:02:53 -07:00
Kelsi
be33fbe2cc fix(gameobjects): drive animation freezing from game object type
Freezing was decided by matching model paths against a list of portal
names, so every game object with a looping idle sat in its bind pose
unless someone had thought to name it — fishing pools were the reported
case, but braziers, banners and waterwheels were frozen the same way.

Freeze only the types whose pose is server state (door, button, chest,
trap, goober, destructible building, trapdoor) and let everything else
play its idle, which is what retail does. A game object's model spawns
from its display id before its type is known, so an object that spawns
with the query still in flight is frozen conservatively and revisited
when GAMEOBJECT_QUERY_RESPONSE arrives, via a new info callback.
2026-07-31 06:32:26 -07:00
Kelsi
96134fd8a7 fix: .gm fly enables flight (drive flight physics from CAN_FLY)
.gm fly on sets only the CAN_FLY movement flag, but flight physics were
gated on isPlayerFlying() which also requires FLYING (only set once
already airborne) — a chicken-and-egg that left the player unable to take
off. Drive setFlyingActive() from a new canFly() (CAN_FLY set) instead,
and ungate the descend key (X) so it works on foot, not just on a flying
mount. Space ascends, X descends, gravity off while CAN_FLY is set.
2026-07-24 03:16:21 -07:00
Kelsi
82337d71d8 feat: guild bank item deposit + active-tab sync
- Right-clicking a bag item while the guild bank is open now deposits it
  into the first free slot of the viewed tab (guildBankDepositFromInventory).
  Bags auto-open when the guild bank opens so items are reachable, and a
  hint line documents left-click withdraw / right-click deposit.
- Fix latent bug: clicking a guild bank tab only sent a query and never
  updated the active tab, so withdraw/deposit always targeted tab 0. Sync
  guildBankActiveTab_ from each SMSG_GUILD_BANK_LIST (server tags it).
- Add ESC-to-close for the guild bank window.
2026-07-24 02:03:46 -07:00
Kelsi
07443e7535 fix: derive quest collect-item progress from bag contents
3.3.5a servers do not push collect-item objective counts (unlike kill
credit, which arrives in the quest-log update fields), so the tracker
relying solely on SMSG_QUESTUPDATE_ADD_ITEM never advanced when quest
items were looted. Reconcile item objectives against actual bag contents
on every inventory rebuild instead.
2026-07-24 00:40:47 -07:00
Kelsi
701bf71603 Revert "Improve item descriptions"
This reverts commit 9a2310f074.
2026-07-23 20:56:21 -07:00
Kelsi
9a2310f074 Improve item descriptions 2026-07-23 20:40:51 -07:00
Kelsi
a8d7582f8a feat: gate and explain target-aura-state abilities (Execute et al.)
Load Spell.dbc TargetAuraState (added to every expansion layout at its
verified column) into the spell cache with getSpellTargetAuraState. The
action bar now dims abilities whose target isn't in the required aura state
(e.g. Execute below 20% health), with a tooltip naming the requirement, and
SMSG_CAST_FAILED result 111 (Target aurastate) now yields 'Target must be
below 20% health.' instead of the opaque protocol label.
2026-07-23 20:03:06 -07:00
Kelsi
47aa55602c feat: at-war and inactive controls in the reputation panel
Add setFactionAtWar / setFactionInactive senders (CMSG_SET_FACTION_ATWAR /
CMSG_SET_FACTION_INACTIVE) with local flag updates, plus isFactionInactive /
isFactionPeaceForced helpers and the FACTION_FLAG_INACTIVE constant. The
reputation panel's right-click menu now toggles war/peace (disabled when
peace is forced) and inactive state; inactive factions hide behind a 'Show
inactive' checkbox and dim when shown.
2026-07-23 19:55:14 -07:00
Kelsi
9b2931ea87 feat: show real achievement icons in the achievements window
Load Achievement.dbc's IconID (added to the WotLK DBC layout, field 42) into
the achievement cache and resolve it through SpellIcon.dbc to the artwork.
The earned list now renders a bordered 32px icon with the name and points
beside it, replacing the gold-star glyph; a star placeholder fills the slot
while an icon streams in or when one is absent. Icon textures are lazily
BLP-loaded and cached in WindowManager with a per-frame upload cap.
2026-07-23 19:26:28 -07:00
Kelsi
e7fc875d69 fix: resolve spell $-tokens everywhere and prefer the full Description
Move the WoW description token resolver onto GameHandler::formatSpellDescription
and route buff/aura tooltips, the spellbook, and item Use/Equip effect lines
through it, so 'increased by $s1' etc. no longer render raw. Add the $/N;
division token used by food regen ('Restores $/5;s1 health per second').

Prefer Spell.dbc's full Description column over the short Tooltip in both the
spell_handler cache and the spellbook loader, so food tooltips include the
'become well fed and gain N Stamina and Spirit' clause instead of a bare
one-liner. The talent screen now delegates to the shared formatter.
2026-07-23 19:16:06 -07:00
Kelsi
c6562bb071 fix: auction item picker includes equipped bags, posts by GUID
The Create Auction dropdown only iterated the 16-slot base backpack, hiding
every item held in an equipped bag. It now enumerates the backpack plus all
equipped bags and posts the selected item by its server GUID via the new
auctionSellItemByGuid path, so any container can be listed.
2026-07-23 18:16:43 -07:00
Kelsi
7a4052c428 feat: announce new mail with a chat line and sound cue
Route the unread-mail flag through a rising-edge helper that prints
'You have new mail.' and plays a notification sound once when mail arrives
(or is found waiting at login), alongside the existing minimap indicator.
2026-07-23 18:12:55 -07:00
Kelsi
e08b64c88e feat: bank window sort, contiguous view, and bag-slot prices
Add a Sort button that orders the main bank and bank bags by quality/itemID/
stack (mirroring the backpack sort, one CMSG_SWAP_ITEM per frame), a
"Combine bags" toggle that renders every bank slot as one continuous grid,
and per-slot purchase prices from BankBagSlotPrices.dbc on the bank-bag row
(only the next slot in sequence is buyable; later ones are locked).
2026-07-23 17:53:10 -07:00
Kelsi
2a77f1184b feat: let solo players set target markers locally
The server only broadcasts MSG_RAID_TARGET_UPDATE to a group, so marking
while ungrouped did nothing and the feature could not be used or tested
without a second player. Apply the mark client-side in that case, matching
the server's rule that an icon and a unit each hold one mark. Grouped
marking stays server-authoritative, and a group's full list replaces local
marks when one arrives.
2026-07-23 00:04:03 -07:00
Kelsi
09ccd0edd6 fix: show raid target icons on marked enemies
Route GameHandler's raid mark getters to SocialHandler, which is where
MSG_RAID_TARGET_UPDATE stores marks — GameHandler kept a second array that
nothing ever wrote, so the target frame, nameplates, minimap and social
panel all read zeros. Fix the parse to match the wire format as well: the
full list carries only the icons that are set rather than a fixed eight,
and a single mark leads with the setter's GUID on WotLK but not on
classic/TBC, told apart by remaining size so every expansion decodes.
2026-07-22 23:58:57 -07:00
Kelsi
d0902663cd feat: refresh quest giver markers as soon as an objective completes
Re-query nearby quest giver status on kill-credit and quest-item updates
that may have made a quest turn-in-able, so the ! and ? markers change
without leaving and re-entering the area. Sweeps are coalesced behind a
one-second cooldown ticked from GameHandler::update, since one request
fans out to a packet per nearby giver and completion events arrive in
bursts.
2026-07-22 23:49:36 -07:00
Kelsi
83ce09a87d fix: apply barber shop appearance changes without a restart
Barber cuts updated the character data and inventory preview but never
rebuilt the in-world 3D model, so the new hair/facial hair only showed
after a restart or teleport. Add a player-model-rebuild callback fired on
PLAYER_BYTES / PLAYER_BYTES_2 changes; Application re-spawns the model in
place via the same path teleport uses. Guard on an actual appearance diff
(PLAYER_BYTES_2 also carries rest state), preserve the current position,
and reset equipment dirty tracking so armor and weapons re-composite onto
the fresh model.
2026-07-22 15:33:37 -07:00
Kelsi
0bb601ba40 fix: open key-locked chests by using the key item on them
A locked type-3 chest (e.g. Dead-tooth's Strongbox) is not opened by
CMSG_GAMEOBJ_USE, and casting the key's OPEN_LOCK spell via CMSG_CAST_SPELL
is rejected because the player does not know it. Open it the way the real
client does: send CMSG_USE_ITEM for the key item, targeting the chest
(TARGET_FLAG_GAMEOBJECT), so the server casts the key's on-use spell and
unlocks it. Locate the key wherever it lives (backpack, bag, or keyring)
and add GameObject-target support to UseItemPacket. Chests opened by a
known player spell (Pick Lock) still cast it; unlocked/quest containers
still use a plain CMSG_GAMEOBJ_USE.
2026-07-22 02:22:48 -07:00
Kelsi
04eac38661 feat: add non-traded trade slot for enchanting/crafting without transfer
WoW's trade window has 7 slots; slot 6 (TRADE_SLOT_NONTRADED) holds an item
the partner can enchant/craft on without it changing hands. TRADE_SLOT_COUNT
was 6, so slot 6 was dropped in the parser and never shown. Bump to 7, add
TRADE_SLOT_NONTRADED, render it under a 'Will not be traded' separator, and
set the missing occupied flag in the slot conversion (trade items previously
never rendered).
2026-07-21 18:27:17 -07:00
Kelsi
2300ca3653 feat: show undiscovered flight points on the world map
Draw undiscovered flight masters as green diamonds so players can see
where flight paths exist before visiting them, keeping discovered nodes
gold. Filter to the player's own faction (own-faction or neutral both-
faction nodes only) so the opposite faction's flight masters and
non-flight boat/zeppelin nodes are not shown. Adds GameHandler::
isPlayerAlliance() helper.
2026-07-21 04:58:24 -07:00
Kelsi
b5293a1d3a feat: show flight masters on the minimap
Draw a gold diamond marker with tooltip for any visible flight master,
keyed off the live UNIT_NPC_FLAGS bit so undiscovered ones appear before
the taxi window has been opened.
2026-07-21 04:09:29 -07:00
Kelsi
6aab5b3c0a fix combat dismount and event ownership 2026-07-21 03:09:26 -07:00
Kelsi
2de06c267a add map trackers and fix spell and mount state 2026-07-21 02:23:51 -07:00
Kelsi
19ce1460dc feat: fishing snaps character to camera look direction so bobber lands in view
While standing still the character yaw doesn't follow the free-look camera, so a
fishing cast dropped the bobber toward a stale heading (off-view / on land). A
fishing cast now turns the character to face the camera's look direction and sends
that exact facing before the cast, so the bobber lands in front of where the player
is aiming. Adds a FaceCameraProvider callback (renderer supplies camera yaw) and
removes the temporary [FISH-FACING] diagnostic.
2026-07-21 00:42:53 -07:00
Kelsi
4af68f5de8 fix: parse achievement-earned packed GUID; suppress redundant guild online spam
SMSG_ACHIEVEMENT_EARNED carries a PACKED player GUID in WotLK, not a full uint64.
Reading it as uint64 swallowed 4 bytes of the achievement id, so the message showed
a raw GUID (0x...8951D07) and a garbage id (442831322). Decode the packed GUID so
the name resolves and the achievement id is correct.

SMSG_GUILD_EVENT SIGNED_ON/OFF now only announces a genuine online<->offline
transition (tracked via the roster's cached online flag), matching the real client
and dropping the login-time flood of 'has come online' guild-chat spam.
2026-07-21 00:34:04 -07:00
Kelsi
b8a60bff5f feat: compute random-suffix stat bonuses on inventory items
Reproduce the server's random-suffix roll client-side so a green/blue item shows
the stats its suffix grants (e.g. "of the Bear" = +Sta +Str), not just the name.

Capture ITEM_FIELD_PROPERTY_SEED (the suffix factor) alongside the random-property
id, then a resolver keyed on the same DBCs the server rolled from computes each
SpellItemEnchantment STAT amount as AllocationPct*suffixFactor/10000 (suffixes) or
the enchant's fixed amount (positive properties). buildItemDef merges the results
into the item's stat fields so the existing tooltip renders them as green lines.

Field layouts are expansion-aware: SpellItemEnchantment stat/effect columns derive
from the detected Name field (Arg=Name-3; effect block -6 vanilla / -9 tbc+wotlk);
ItemRandomSuffix enchant/alloc arrays at 19/19+N with N=(fieldCount-19)/2. Also
fixes the suffix-NAME resolver, which read an empty column (field 4 vs the real
field 1), so the name append added earlier now actually shows.
2026-07-20 20:27:08 -07:00
Kelsi
80195888ae fix: track item soulbound + random-property fields per instance
Capture ITEM_FIELD_FLAGS and ITEM_FIELD_RANDOM_PROPERTIES_ID from item update
blocks into OnlineItemInfo and propagate them onto ItemDef. A BoE item now stops
prompting to confirm the equip once it has actually bound (soulbound bit set),
instead of asking on every equip, and randomly-enchanted items show their rolled
suffix (e.g. "of the Bear") in the tooltip like the auction house already does.

FLAGS is STACK_COUNT+7 (expansion-independent); RANDOM_PROPERTIES_ID is DURABILITY-1
on WotLK and DURABILITY-2 pre-WotLK (an ITEM_TEXT_ID field separates it there).
2026-07-20 20:11:35 -07:00
Kelsi
462defc76d refactor: consolidate transport hull orientation into single bow-offset rule
Funnel every ship-facing path through TransportManager::transportModelBowOffset
(facing = direction of travel + fixed per-model bow offset), replacing the
scattered per-entry 180-degree hull corrections. berthRunsParallel() names the
side-on dock routes, and buildTaxiSegmentSpline is extracted static/testable so
the cyclic-wrap behavior is unit-covered without a DBC.
2026-07-20 19:36:21 -07:00
Kelsi
fe0ad4dfdb fix: stabilize Kraken cross-map boarding 2026-07-20 03:35:32 -07:00