Commit Graph

1334 Commits

Author SHA1 Message Date
Aaron Kimbrell
ebcfd54915 fix: tell the player when the zone they go to gets no instance
The master server dropped a transfer request it started no instance
for, so the world server never heard back and the player waited. It
now answers the request with an empty address and the reason in place
of the port: no room for another instance (max_instances), a zone that
is not in the ZoneTable, or another reason. A request for a private
zone that does not exist is answered the same way.

The world server passes this on as a TRANSFER_TO_WORLD without an
address, which the client shows as a failed map change, and leaves the
character where it is. The auth server fails the login when there is
no character select to send the player to.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:28:34 -06:00
Aaron Kimbrell
24294561ae fix: a changed trade offer takes both accepts back
A player could change their offer after the other player gave the final
accept, and the trade completed with the new offer on their own accept.
Both clients drop their accepts when an offer changes, the one that
changed it and the one that receives the update, so the trade now does
the same and both players have to accept the offers as they are.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:28:34 -06:00
Aaron Kimbrell
6f7fd5f84e fix: a trade offer takes what the trade window takes
The client's trade window has 8 slots for each player and takes neither
a bound item nor an equipped one. An offer with more than 8 items, a
bound item or an equipped item is now refused like the other offers the
window can not make, when it arrives and again when the trade
completes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:28:34 -06:00
Aaron Kimbrell
b3f5cd74df fix: a vendor sells up to 999 of an item at once
The limit was 1000. The client's buy dialog counts up to 999 for an
item from a vendor, whatever the item's stack size is, so that is the
most a purchase can ask for.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:28:34 -06:00
Aaron Kimbrell
66e6137a9f fix: a launchpad sends its rockets to its own zone
A zone transfer from a launchpad took the zone from the message when
the message asked for that. The client never does, so the zone is now
always the launchpad's default zone or the zone the player picked on
it. The log line no longer uses the sender named in the message.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:28:34 -06:00
Aaron Kimbrell
718bd78e8c fix: check for the player before handling the messages a player sends
Several messages used the player, the character or the user they named
without checking that there is one. A match request and a trade request
are now only handled for the player that sent them, a linked mission
request and a build mode change only for the sending player, and the
remaining handlers return when the player, character or user is
missing.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:28:34 -06:00
Aaron Kimbrell
0f1bce32fb fix: only the owner of a property changes it
The messages that set a property's privacy option and name, start and
finish building, place, move and remove models and change a model's
behaviors were handled for any player on the property, and outside of
a property they used a property that is not there. They are now only
handled for the owner of the property the player is on. Only an item
from a model inventory is placed as a model.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:28:34 -06:00
Aaron Kimbrell
081e26ded8 fix: a player's own messages have to be sent for that player
The die request, the smash request, the loaded notice, chat commands
and the racing pickup notice were handled for whichever object they
named. They are now only handled for the player that sent them, or for
the vehicle that player drives where the client sends them for it. A
racing pickup also has to be an object of a race track that is not a
player, a vehicle or the zone's own object.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:28:34 -06:00
Aaron Kimbrell
f170b46a5e fix: check the player's score before a level up
A level up was applied every time the client reported one. The next
level now has to exist, the player needs the score that level asks
for, and the message has to come from the player it is for.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:28:34 -06:00
Aaron Kimbrell
9ffed42772 fix: limit the count of a vendor purchase and work its price out in 64 bits
A purchase took any count, and the coin, alternate currency and
commendation prices were worked out as 32 bit numbers that wrap for a
large count. A purchase is now for 1 to 1000 items and the prices are
64 bit numbers.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:04:52 -06:00
Aaron Kimbrell
2d24608186 fix: pay a vendor sale by the items that left the inventory
Selling paid for the count in the message while the move stopped at
what the stack held. The count now has to be between 1 and the stack's
count, the item can not be one the vendor already holds for buy back,
and the coins and alternate currency are worked out from the number of
items that left the inventory.

Buying back needs an item in the buy back inventory and a count the
stack holds. Both prices are worked out as 64 bit numbers.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:04:30 -06:00
Aaron Kimbrell
fe4a1e5e98 fix: check the mission state before completing it from a dialogue
MissionDialogueOK accepted or completed the mission for whichever
object the message named as the responder. The responder now has to be
the player that sent the message, the object the dialogue belongs to
has to give or take the mission (on its own or through a random
mission's pool), a mission is only accepted when the player can accept
it and only completed when it is ready to complete. The object's
script runs after these checks.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:04:01 -06:00
Aaron Kimbrell
c0b7a8f643 fix: check a trade offer against the items the player holds
A trade used the LOT and the count from the offer as the player sent
it. The offer is now resolved when it arrives and again when the trade
completes: the LOT comes from the held item, the count stops at the
held count, and an offer that names an unknown item, the same item
twice or a count of 0 is refused. The other player only receives an
item that was taken from the offering player.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:03:21 -06:00
Aaron Kimbrell
4bbf22f912 feat: keep every brick by brick save before it is split into models
The save is stored as the client sent it in the new ugc_unsplit table, and each model split from it points back to it (ugc.unsplit_id), so the whole build can be looked at or restored later. A save is kept while any model split from it exists: deleting models removes saves no model points to any more, and deleting a character removes its saves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 11:53:01 -06:00
Aaron Kimbrell
d870207a9f fix: place brick by brick models by the bottom middle of their bricks' box
When a brick by brick save is split into models, each model was centred on
the middle of its bones' origins, on the lowest origin. A brick's origin is at
a corner of it, not its middle, so a single 2x2 came back offset by half a
brick, and the model's floor was taken from the origins rather than from the
bricks.

Now each model is placed as the live server placed it: at the bottom middle
of the box around its bricks. Each brick's own box comes from the client's
brickdb by its design ID and is turned and moved by its bone (all 8 corners),
and the model's box holds every brick's. Its x and z middle and its lowest y
are the new origin, still snapped to the 0.8 grid on x and z, and every bone
moves by that; rigids and rotations are untouched. A design the brickdb has no
box for counts its bones' origins, as before, so no brick is left out.

Lxfml::NormalizePosition and Split take the boxes by design ID, and the brick
by brick save passes the ones read from the client (read on the first save,
then kept).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 11:52:30 -06:00
Aaron Kimbrell
6ac2faf55a fix: the scene lookup and the angular velocity handlers main added are defined after the merge
Zone::GetScene and SimplePhysicsComponent's Get/SetAngularVelocity handlers were declared and used but their
definitions were lost where the merge kept this branch's side of the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 11:37:31 -06:00
Aaron Kimbrell
fcf8591749 fix: one raw terrain reader, in dZoneManager, after the merge
The dashboard's copy of the terrain reader in dCommon is gone; main's in dZoneManager reads for everyone, and keeps
what the copy had over it: colour and texture maps from before version 32 become RGBA, cut to the resolution the
client keeps. The scene map (ZoneScenes) and the dCommon tests compile against that reader.

A scene ID takes its layer as the zone file numbers it (0 General, 1 Audio) as well as the enum, so main's scene tools
and tests build; /getscene prints the layer as a number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 11:19:33 -06:00
Aaron Kimbrell
0f826bdc99 fix: inventory groups getter declared once after the merge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 11:02:58 -06:00
Aaron Kimbrell
b466c08ee2 Merge origin/main into experimental: behaviors, raw terrain scene lookups, property model fixes, player count
Main's model behaviors (every rotation, DoDamage, OnTimer, OnStartup, MoveBackToStart, moves along the model's
local axes), raw terrain reading with scene lookups and scene tools, property model and brick by brick fixes and the
chat server's player count fix, merged onto this branch's structures:

- game messages stay structs; the angular velocity messages join them and the handler sets a message's target
- strips own their speed again and the block names, moves and rotations stay in the shared action table
  (PropertyBehaviorActions, which the dashboard's behavior player reads; it now lists rotations too)
- the zone keeps reading its file through ZoneFile and takes the boundaries from it, then loads the raw terrain as main
  does; the old dNavigation terrain reader is gone
- the show all response stays a struct and logs a player count mismatch as main does
- the property management component tells models who sent a chat message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 10:57:45 -06:00
Aaron Kimbrell
0ff5d2bf87 fix: property model and brick by brick fixes (#2041)
* test: the server mock keeps a copy of the last bitstream sent

The bitstreams sent are usually on the sender's stack, so the pointer it kept was dangling once Send returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: PlaceModelResponse writes the model's rotation

The client's PlaceModelResponse::Deserialize (0x00dc0170) reads the rotation as an optional w, x, y, z quaternion, but the server wrote the 4-byte response after the rotation flag, so with any rotation other than identity the client ran out of data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: naming a model no longer renames the property

The model info window sends UpdatePropertyOrModelForFilterCheck with isProperty false (client 0x00cc42e0); only the property editor sends it true (0x00c2e710). A model's name and description aren't stored yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep a model item's blueprint across saves

A brick built model item's config has blueprintid (lowercase), which the saved item XML didn't keep (only blueprintID), so a picked up model lost its blueprint on the next load and could not be placed again. It is now saved as bp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: write model behaviors only for property and inventory models

The client makes the mutable model component, which reads the behaviors and editing info, only when the object's config has propertyObjectID or inInventory (ObjectLoader2::LoadModelBehaviorsComponent); every other model gets the plain one, which reads only the model block. Writing the behaviors for every model made the client read them as the next component's data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: the model vault uses the models' inventory groups, as the client

LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) and msgUpdateInventoryGroupContents (0x00ce1f50) look the inventory type up in m_VaultToPlayerInventory first, which maps VAULT_MODELS to MODELS. The server refused group updates for the model vault.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:34:31 -05:00
Aaron Kimbrell
9bf2ff4d44 feat: Raw reading on zone load and scene tools (#1910)
* feat: raw terrain parsing for scene data

Replace old dNavigation/dTerrain raw parser with new Raw module in
dZoneManager. Parse heightmaps, color maps, and scene maps from .raw
files to determine which scene a position belongs to. Build scene
adjacency graph from terrain data and scene transitions.

Adds NiColor type, SceneColor lookup table, eSceneType enum, terrain
mesh generation with OBJ export, and debug slash commands for scene
visualization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: deduplicate terrain grid lookup logic, remove unnecessary cast

Add IsValidForSceneLookup(), GetSceneIDAtGrid(), and GridToWorldPos()
to Raw::Chunk. Use them in GenerateTerrainMesh, GetSceneIDFromPosition,
SpawnScenePoints, and SpawnAllScenePoints instead of duplicated inline
math. Remove unnecessary static_cast in LoadSceneTransitionInfo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add version < 32 support to raw terrain parser

The parser was only correct for v32 files. For v<32:
- colorMapResolution is width-1, not width
- Color map reads width*width*4 bytes (BGRA per-pixel), not colorMapRes^2*4
- No DDS lightmap
- No texture settings byte or blend DDS
- Scene map: v31 reads (colorMapRes+1)^2 cells with border skip,
  v<31 skips 1 byte
- No mesh data

Verified against all 309 .raw files across multiple client versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: log and reject legacy raw terrain format, clean up ReadRaw API

Log a clear message and throw if the zone uses a legacy raw format
(LUZ version < 30) since no known maps use it. Remove ReadLegacyRaw
and the mapVersion parameter from ReadRaw.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: revert v33 scene field renames to unknown1/unknown2

These fields are skipped by the client in every known build (ptr += 12
and ptr += 4). No client ever reads them into a struct. Keep the
original unknown1/unknown2 names from main.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: load the zone without scene lookups when its raw terrain is missing or unreadable

Log at debug when it parses; build the terrain mesh only for the OBJ export; read the zone name and description only for version 31 and later, as the client does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: look up the scene at a position as the client does, in constant time

Chunk bounds span (width - 1) * scale; the nearest scene map cell is used and the far edge is kept one cell inside. Chunks are indexed by grid cell when they form a grid. Skipping reads instead of seeking, so a cut-off file can't seek past the end. Terrain mesh indices follow the vertex order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: scene point commands take a step and use brick LOTs from the BrickIDTable

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: rename NiColor::ToXYZ to Luminance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: raw terrain reading, scene lookup, scene graph and scene commands

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: close the OBJ file before removing it so the test passes on Windows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep the zone boundary lines and read the luz scene count as the client does

Each boundary line's destination is read as a map ID and an instance ID into a LWOZONEID. The scene count is a byte before version 37 and a u32 from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: name the fourth flair colour byte padding

The client never sets or reads it, so it holds leftover memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep the scene map of version 31 and older raws

These files have a (resolution + 1)^2 scene map, kept in sceneMapLegacy. Version 31 uses its first resolution rows and columns; older versions are all scene 0, as in the client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: simplify the raw heightmap size check

The dimensions are capped at kMaxResolution, so the product can't overflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: remove the scene point spawn commands and send /getadjacentscenes as one message

The spawn commands were only for testing. /getscene and /getadjacentscenes stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-27 09:22:28 -05:00
Aaron Kimbrell
eb4d1eabe5 fix(world): a player's client reloading the zone gets its character and the world again
A client that loads the zone again while its player is in the world (the
Happy Flower editor reloads the zone in place) sent LEVEL_LOAD_COMPLETE a
second time; LoadPlayer returned at once because the player existed, so
the client waited forever. Now the server resends the character
(CreateCharacter, ServerStates), forgets what it had sent that client,
constructs every object again and sends the done-loading sequence at the
player's current place. A first load is unchanged.

The Happy Flower editor messages (GetEditorLevel, the HF_* messages) are
logged when they arrive, until the server handles them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:49:35 -05:00
David Markowitz
fc6a7fbd52 feat: Add a bunch of behaviors (All rotations, DoDamage, OnTimer, OnStartup, MoveBackToStart) and make models move based on normals, not world axes (#2038)
* add rotation behavior logic (not working correctly for multiple axes)

* Fix FetchContent_Declare speed

* remove manual quaternion math

* feat: working rotations

* remove flag

* feat: make directional movements based on model rotation and not world axes

* poggers rotation on the strip

* Add OnStartup and OnTimer

* go home works

* Add DoDamage behavior and dont try and start the same node multiple times

* fix: pause bbb models

* interrupt rotations and save interrupt count across resets
2026-10-05 12:33:45 -05:00
Aaron Kimbrell
c42a7ba387 fix(missions): a mission is only turned in when its rewards fit, as the client
LWOMissionComponent::MissionWindowOK (0x00d501f0) turns a mission in only
when CanReceiveAllRewards (msgCanReceiveAllRewards 0x00d17e20,
sub_00d17750) passes for the reward the player picked: the reward items
(the repeat ones once completed, each at least 1) must fit after the items
the tasks take (a gather task's target unless it keeps them, one of a
delivery task's target) with the first completion's bag rewards added.
The fit is LWOInventoryComponent_Common::CheckCanAddItems (0x00d53de0):
turned in items leave first (an item of 1 settles the whole count), a
unique item fails while one is held outside the buyback, then each item
tops up stacks of its LOT (stack size 0 is no limit, LookupItemInfo
0x00c1c190) and takes free slots.

MissionDialogueOK now refuses a turn-in that does not pass, which the
client never sends. The client's max currency part is not checked: the
server writes no cm, so the client has no limit either.
InventoryComponent::GetDefaultInventorySize is split out of GetInventory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:48:44 -05:00
Aaron Kimbrell
2fe489d518 fix(missions): accepting a repeat waits whole minutes of cooldown, as the client
The client's cooldown (LWOMissionComponent sub_00cd7770) counts whole
minutes since the last completion and has none when Missions.cooldownTime
is 0 or below. Accepting used seconds and treated only -1 as no cooldown,
so a cooldown of 0 still refused in the second of completion and other
negative values refused for a while. CanAccept now uses the same check as
the offers (MissionComponent::IsCooldownRunning).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:48:44 -05:00
Aaron Kimbrell
c1079fc20c fix(missions): prerequisites read as the client reads them
The client checks Missions.prereqMissionID in
LWOMissionComponent::getMissionState (0x00cd7400): text without any of
!&^;,|(): is one mission ID (met when 0 or ever completed); anything else
goes through its expression reader (sub_00a32980), left to right with no
precedence, & , ; and, | or, ^ xor, ! ~ not, ( ) groups, and/or skipping
the rest once the result is known. Each operand (sub_00c90a80): "id" is
met once the mission was ever completed; "id:state" when the client's
state mask of that mission equals state.

DLU read the text right to left ignoring groups, so
"(509|229)&(812|813|814|815)" (mission 781) was met by 509 alone; a plain
ID needed the mission's state to be 8, so a repeatable being repeated
blocked the missions after it (1870 after 1869); and id:state compared the
mission's own state. GetClientMissionState moves to MissionPrerequisites
so operands can ask for the mask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:48:44 -05:00
Aaron Kimbrell
529bacd077 feat(missions): keep the viewed flag of missions in progress, as the client
The client keeps a viewed flag on each mission in progress, sets it with
SetMissionViewed (LWOMissionComponent::msgSetMissionViewed 0x00bec7a0, only
for missions in progress) and reads it as v on the mission's <cur> entry
(LWOMissionComponent::LoadFromSaveData 0x00d171c0, a number or "true", false
when missing). The server now handles SetMissionViewed, saves v="1" for a
viewed mission in progress and loads it back; saves without v load as not
viewed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:48:43 -05:00
Aaron Kimbrell
0c298efe4d fix(character): arriving in a world starts at the target scene or the zone's spawn
A player who had been to a world before arrived at that world's saved
respawn point instead of the zone's spawn. The saved point is where a death
in that world respawns, not where arriving starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:13:56 -05:00
Aaron Kimbrell
7bd15298a4 fix(platform): no resync for a platform without a path
A client asking to resync a simple mover that has no path got a
PlatformResync anyway, and the client's simple mover then reads waypoints
from its null path and crashes (LWOPlatformSimpleMover::Resync 0x00bf9ec0,
Path::GetWaypointAtIndex). The two LOTs special-cased for this before are
examples of the same thing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:02:29 -05:00
Aaron Kimbrell
02c96b08bd fix(inventory): nothing is used while stunned from using items, as the client
LWOInventoryComponent_Common::msgUseNonEquipmentItem (0x00cdeba0) asks
GetStunned first and uses nothing when canUseItem is false
(LWOControllablePhysComponent::msgGetStunned 0x00be0df0: stunned from
using items, or dead). The server now counts the use-item stun of every
SetStunned it sends for an entity as msgSetStunned (0x00bf1d10) does
(push/pop, never below 0, characters only, unless immune and not
bIgnoreImmunity), and checks it with IsDead before using an item. The
server never checked either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
a538f32382 fix(inventory): the charxml's items element has nn="1", as live
Live charxmls carried <items nn="1"> (225 of 226 captured).
LWOInventoryComponent_Common::LoadFromSaveData (0x00d1ec40) reads it and
then does not add the inventory component's template items to the loaded
character. No effect on players today (the player LOT's inventory
component has no items); the server never wrote it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
0b5a79cf96 fix(inventory): inventory groups keep their locked flag, as the client
LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) stores
the ADD's locked flag in the group; locked groups don't count toward the
50 user groups (msgGetInventoryGroupCount 0x00bde4b0 counts only unlocked
ones) and msgUpdateInventoryGroupContents (0x00ce1f50) applies the
1000-LOT cap only to unlocked groups. LoadFromSaveData (0x00d1ec40) reads
it from the grp element's u attribute. The server dropped the flag and
always saved u="0"; saves without u load as unlocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
841004e6a4 fix(inventory): an inventory group holds at most 1000 LOTs, as the client
LWOInventoryComponent_Common::msgUpdateInventoryGroupContents (0x00ce1f50)
ignores an ADD to an unlocked group that already holds 1000 LOTs. The
server had no cap, so a modified client could grow the charxml without
limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
042594e3dd fix(inventory): UpdateInventoryGroupContents CLEAR empties the group, as the client
LWOInventoryComponent_Common::msgUpdateInventoryGroupContents (0x00ce1f50)
takes ADD, REMOVE and CLEAR; CLEAR removes every LOT from the group. The
server logged CLEAR as an invalid action and kept the LOTs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
a3d1b6a8b8 fix(inventory): the model vault uses the models' inventory groups, as the client
LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) and
msgUpdateInventoryGroupContents (0x00ce1f50) look the inventory type up in
m_VaultToPlayerInventory first, which Initialize (0x00d55a60) fills with
VAULT_MODELS -> MODELS. The server refused group updates for the model
vault.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
9afe8ccedb fix(inventory): inventory group names are cut to 64 characters, as the client
LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) resizes
the group name to 64 characters on ADD and MODIFY. The server kept names
of any length in the charxml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
0bc1a88e75 fix(inventory): adding an inventory group that exists does nothing, as the client
LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) ignores
an ADD whose group id is already in the inventory. The server added a
second group with the same id, saved to the charxml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
ba6228ef09 fix(inventory): MoveItemInInventory moves only where the item belongs, as the client
LWOInventoryComponent_Common::msgMoveItemInInventory (0x00d20590) looks the
item up in the inventory the message names and moves it only to the
inventory its LOT belongs in (GetInventoryTypeForLOT 0x00c3c4f0), or for
the vaults (VAULT_ITEMS, VAULT_MODELS) the LOT's vault type
(GetValueTypeForLOT 0x00c3c4a0). An item in the target slot swaps into the
moved item's old slot and inventory, and must belong there. The server
moved an item into any inventory destInvType named and left the swapped
item in the destination. Live clients never set destInvType (0 of 61 in
the captures).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
7366799152 fix(inventory): deleting skips equipped items, as the client
LWOInventoryComponent_Common::DoRemoveItemFromInventory (0x00d1fb50)
passes over equipped items when the loot source is LOOT_SOURCE_DELETION.
The server deleted the item while it was still worn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
9497d20027 fix(inventory): deleting more than the stack holds does nothing, as the client
LWOInventoryComponent_Common::msgRemoveItemFromInventory (0x00d203b0)
counts the item (GetInvItemCount) and does nothing when it holds fewer
than iStackCount; a request for an item with no count takes the whole
stack. The server cut the request down to the stack and deleted it, and
deleted nothing for a count of 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
5bf637b02e fix(inventory): the ridden object and the mount item can't be deleted, as the client
LWOInventoryComponent_Common::CanRemoveFromInventory (0x00ce0d20) refuses
the possessed object (GetPossessedObject) and the mount item
(GetMountInventoryID) before the operator bypass and the deletion
restrictions. The server let a player delete the mount they were riding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:55:06 -05:00
Aaron Kimbrell
ea875d859c docs(quickbuild): the level's activityID wins as the client resolves it
LWOActivityComponent::Startup (0x00d006b0) sets actIDovrd, when the level
leaves it out, to whether the level has an activityID, so
LWOQuickBuildComponent::LoadDataFromTemplate (0x00cbb2e0) keeps the level's
activityID; the template's is read only for an explicit actIDovrd=0. Every
quickbuild with actIDovrd=0 in the levels has the template's activityID as
its level one. Comment only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:51:25 -05:00
Aaron Kimbrell
66dca36b63 fix(vendor): dropping the oldest buyback item moves the rest up, as live
Live's 28th sale to a full buyback removed the oldest item, then sent
MoveItemInInventory (inventory 11, VENDOR_BUYBACK) for each of the 26 items
left, to slots 0 to 25 in order, before the sold item was added after them.
The server left the hole, so the sold item took the dropped item's slot
and the buyback page was no longer in sale order. The buyback inventory is
not saved and nothing changes hands; only slots move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:51:25 -05:00
Aaron Kimbrell
1ac1248161 fix(skill): cooldowns start as the client starts them
LWOSkillComponent::msgStartSkill (0x00d45cb0) starts no cooldown for cast
types 1 and 2, and inserts the cooldown (std::map insert), so a cooldown
group already running keeps its time left. The server started one for
every cast type and extended a running group to the longer cooldown.
Live client casts used cast types 0, 3 and 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:15:53 -05:00
Aaron Kimbrell
24932b1282 fix(skill): a dead caster's sync ends the behavior without running it, as the client
LWOSkillComponent::MsgSyncSkill (0x00d454b0) checks whether the caster is
dead before running a synced behavior; if so it only removes the behavior
handle from the skill's pending ones. The server ran the sync anyway, so a
player's delayed hit still landed after they died.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:15:53 -05:00
Aaron Kimbrell
c9929f1c1e fix(skill): a start with a skill handle still in progress is dropped, as the client
LWOSkillComponent::msgStartSkill (0x00d45cb0) logs and ignores a StartSkill
whose skill handle is still waiting on behavior syncs. The server ran it a
second time: the hits, the imagination cost and the mission progress twice.
Live captures hold 22 byte-identical StartSkill resends with the same
handle a few packets apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:15:53 -05:00
Aaron Kimbrell
4fbebf7135 fix(inventory): an opened package reports success in UseItemResult, as live
Item::UseNonEquip declared a second `success` inside the package branch, so
the UseItemResult it sent (and its log line) was always false, even when the
package opened and its contents were given. The client
(LWOInventoryComponent_Client::sub_00c1bb20 (0x00c1bb20)) plays the item's
audioEventUse on true and a failure sound on false; live sent true for 183
of the 187 UseItemResult in the captures.

Test: UseItemResultTests.AnOpenedPackageReportsSuccess.

Check in game: opening a package (e.g. a starter pack or a reward bag) plays
its open sound rather than the failure sound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:47:24 -05:00
Aaron Kimbrell
2982a7f23c fix(mission offer): RequestLinkedMission offers only a new mission after the one just answered
The client's RequestLinkedMission handler (LWOMissionOfferComponent
0x00cd6eb0) takes the giver's list without the mission the window was
for, and a new mission (AVAILABLE, never completed) at its head is the
one offered. DLU offered every qualifying mission again, so a mission
just accepted was offered once more. The live captures match the client:
no offer after an accept, and after a turn in the next mission of the
chain, once per RequestLinkedMission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:43:08 -05:00
Aaron Kimbrell
04e0c13678 fix(mission offer): offer the first mission of the giver's list, as the client lists them
LWOMissionOfferComponent builds a player's missions for a giver
(0x00c605f0, one entry 0x00bebf30) in priority order: 0 a mission to turn
in here, 5 one in progress, 10 a new one, 15 a repeat. A ready mission is
only listed where MissionNPCComponent.acceptsMission is set, a new one
only where offersMission is set and its cooldown has run out, a mission in
progress by every giver that has it. A random mission (0x00c5ffd0) lists
one of its pool that the giver has: one to turn in first, then one in
progress, else a random one. Missions are kept by mission ID; equal
priorities keep that order.

DLU sent an OfferMission for every mission that qualified, including one
ready to turn in at a giver that does not accept it. In the live captures
every use got one mission's OfferMission pair. A use now offers the head
of the list, and a pick from the multi interaction menu the picked one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:43:08 -05:00
Aaron Kimbrell
8564e293fa fix(mission): GetMissionState answers the client's state bit mask
LWOMissionComponent::getMissionState (0x00cd7400) returns a mask: COMPLETE
once the mission was completed, ORed with the state of the mission in
progress; a mission not in progress gets AVAILABLE when it was never
completed or is repeatable and its prerequisites are met, else 0. DLU's
handler answered UNKNOWN (-1, every bit set) for a mission that cannot be
taken and COMPLETE for a completed repeatable one, and ignored
cooldownInfoRequested. With cooldown info requested (0x00cd7770) the
client only checks the cooldown of a repeat in progress (state 9 or 10):
fewer whole minutes since the last completion than Missions.cooldownTime.

MissionComponent::GetMissionState, used across the server, is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:43:08 -05:00