Commit Graph

757 Commits

Author SHA1 Message Date
Aaron Kimbrell
9b386caf1e fix: a launch pad that does not use its table row is ruled by its level
LWORocketLaunchComponentCommon::LoadConfigData (0x00c55150) reads the
level's targetZone, targetScene, GMLevel, propertyDefaultTargetZone,
rocketLaunchPreCondition, rocketUseLaunchPrecondition and the alternative
landing keys, and the table row replaces them only while
rocketLaunchUseDBDefaults is true, its default (0x00c12ea0). The server
used the row for every pad.

31 pads in the 1.10.64 maps turn the key off. Their zones and scenes are
their rows'; what only the level has are the launch preconditions in use
on the Venture Explorer's four pads ("237;8;127|128;6|55;107;282") and
on Avant Gardens' pads to Nimbus Station, its two properties and the
Venture Explorer. Those are now checked before a launch, as the row's are.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:55:07 -06:00
Aaron Kimbrell
02623be7fb feat: save the mount item as live did
Every live save had <mnt a="..."/>, which
LWOMountControlComponent::LoadFromSaveData (0x00bf99c0) reads as the
character's mount item (0 on the 450 saves looked at). The server wrote
no such element.

MountControlComponent now writes it: the item of the mount being ridden,
0 on foot. The saved value is read and kept; nothing is mounted from it,
as riding does not go on over a world change. A save from before gains
<mnt a="0"/> and nothing else in it changes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:54:54 -06:00
Aaron Kimbrell
c5c90f73a2 fix: write what the client reads for a chest, a choice build and a roller
The three components were made from the registry and wrote nothing, so
the client read the next component's bits as theirs.

- LWOChestComponent::Deserialize (0x00c22010), construction only: the
  player (u64) and the loot offer count (u32).
- LWOChoiceBuildComponent::Deserialize (0x00bf26d0): a flag, then the
  state (u32, 0 waiting, 1 building), the built object and the user
  (u64 each). Always in a construction.
- LWORollerComponent::Deserialize (0x00c15920), construction: the level's
  activity_camera_pos and activity_camera_lookat_pos (3 floats each) and
  the projectile skill (u32, the client starts at 1348); then the flag of
  the block about its user, which is never set.

LWOGameObject::Deserialize (0x00f99690) reads the components in the order
of the object's component list, which ObjectLoader2::DoObjectComponentLoad
(0x0105ebb0) fills in the order of FillComponentOrder (0x0101f8e0): the
roller after the shooting gallery and before the moving platform, the
choice build after the moving platform and before the LUP exhibit and the
model, the chest after the zone control and before render. They are
placed against the components their objects have.

The gate rush control (103) is left out: its two registry entries have no
object, and there is no component for it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:54:54 -06:00
Aaron Kimbrell
b2fba9d35b feat: save play time as live did: time before the session, stime in it
Live kept the total before the running session in <char time> and the
session's seconds in <char stime>; both changed at a login, together with
llog (time 262486 and stime 7344 became time 269830 at the next login).
The client reads both (LWOCharacterComponent::LoadFromSaveData 0x00ca4910)
and shows their sum (SendMessage 0x00d34330, GetTimePlayed). The server
wrote everything into time and no stime.

Now time + stime is read (a missing stime is 0, so a save from before
keeps its total) and time and stime are written as live had them. A
session starts when the character is picked on the character list:
charinfo.last_login is loaded with the character, a save whose llog is
another login has its stime moved into time, and llog is written as the
session's login. Without a known login the session goes on.

GetTotalTimePlayed is still every second played. The dashboard's save
check knows stime as a number.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:54:54 -06:00
Aaron Kimbrell
baf8b24ad1 fix: a property is rented only with the level's required mission
LWOPropertyVendorComponent::onCheckUseRequirements (0x00c2e210) lets a
player use the property guard only when the level's
property_required_mission (LoadConfigData 0x00c2e550) is finished. The
server opened the vendor and rented the property on any request.

The guard is now not opened without the mission, and a rent request
without it is answered with PropertyRentalResponse code 9, which
onPropertyRentalResponse (0x00c56d70) shows as "you failed to rent" with
UGG_PROPRENT_Dont_have_achievement. A mission that was finished and is
being repeated counts, as in the client's list of finished missions. A
guard without the key asks for nothing.

The six property maps name missions 768, 872, 873, 874, 1293 and 1322.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:28:18 -06:00
Aaron Kimbrell
53968599d2 fix: a launch pad checks its GM level and launch precondition
LWORocketLaunchComponentCommon does not launch a player under the row's
gmLevel (msgFireEventClientSide 0x00cb78b0) or, with useLaunchPrecondition,
one that fails the row's launchPrecondition (msgCheckUseRequirements
0x00c7d0d0). The server launched on any request. Rows: the test launcher
asks for GM level 9, the pads to Gnarled Forest and Forbidden Valley for
mission 474, Starbase 3001's for mission 956.

The alternative landing precondition pointer also starts out null for a
pad without a row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 15:14:44 -06:00
Aaron Kimbrell
38fe226e6f fix: a proximity monitor row lists proximity types, not radii
LWOProximityMonitorComponent::LoadDataFromTemplate (0x00c2dfb0) reads the
row's Proximities as ids of ProximityTypes rows, and SendMessage
(0x00cbb930) makes, on AddedToWorld, a proximity under the type's name and
radius (30 when missing) for each type loaded on its side.

The ids were read as two radii: every launch pad ("7,8", the client's
RocketSign and WorldLauncher types) got a rocketSmall sphere of 7 and a
rocketLarge one of 8 that nothing used. Now the row's and the type's
LoadOnServer decide; of the 40 rows only a test object's has one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 15:14:44 -06:00
Aaron Kimbrell
b033655e90 fix: a rail that is off or whose preconditions fail is not ridden
LWORailActivatorComponent::msgCheckUseRequirements (0x00c2c5b0) refuses a
use, after the quick build check, when the row's preconditions fail or
rail_activator_active is off, so a client does not ask. The server started
the rail on any use request. The rows ask for an equipped item (44) or a
completed mission (283 to 286).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 15:14:44 -06:00
Aaron Kimbrell
dc5f13cdbc feat: write the character attributes live saved and the client reads
LWOCharacterComponent::LoadFromSaveData (0x00ca4910) reads them, and live
saves had every one (450 of 450 characters saved at least once):
- cm, the most coins a character can hold: 9223372036854775807 on every
  live save, the client's own value without it. Written when missing, an
  existing one is kept, and nothing caps the coins by it.
- edit (editor level), gid and gn (guild id and name).
- mldt (when the property moderation notice was last shown): 0 when
  missing, as most live saves.
- v and vd: the votes left and the day they are for (GetVotesLeft /
  AdjustVotesLeft). Read back; a new day fills them up to the 3 of
  WorldConfig's character_votes_per_day.
- lrx.. lrrw: the last respawn point reached, once one was reached in the
  world.
- lzcs: the checksum of the zone in lzid.

Saves without them load as before. stime is not written: live kept the
session's seconds in it and the time before the session in time, which
would change what time means in existing saves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:50:43 -06:00
Aaron Kimbrell
95e0746441 fix: a worn jetpack works again after a world change
Jetpack mode is not saved, and the equip skill that turns it on was cast
only when the item was put on, so a player still wearing a jetpack lost
it in the next world. Live sent SetJetPackMode (use, bypass checks, effect
167, the values of the jetpack's equip skill) shortly after the player's
own construction in 12 of 28 world loads of a player with a jetpack, and
constructed the player in jetpack mode in the other 16.

The jetpack behaviors among the equip skills of the equipped items are
run again when the player has loaded. Nothing new is saved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:50:33 -06:00
Aaron Kimbrell
5ac39dca8b fix: an activity's construction always carries its players
LWOActivityComponent::Deserialize (0x00d002b0) reads a flag, the player
count and each player with its 10 values; a construction is the only way
a client coming into range learns who is in the activity. The block was
written only while it was waiting for the next update. Live finish line
constructions (LOT 4967) had the flag and the count in 40 of 40, one of
them with a player in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:50:33 -06:00
Aaron Kimbrell
9265ab0e10 fix: run speed modifiers stack as the client's do
LWOControllablePhysComponent::msgAddRunspeedModifier (0x00be0af0) and
msgRemoveRunSpeedModifier (0x00be0720): a modifier over 1000 or of exactly
500 counts for nothing, any slow (under 500) wins with the lowest one, and
otherwise the highest boost is the speed. Before, the last one applied won,
so a small boost cast after a big one, or a boost cast while slowed,
replaced it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:50:33 -06:00
Aaron Kimbrell
e786d2b1c5 fix: constructions keep pending health, quick build and equipment changes
The destroyable's health and threat blocks, the quick build's state and
the inventory's equipped items are each read behind a flag bit
(LWODestroyableComponent::Deserialize 0x00d2bbd0 and the others). DLU
cleared their dirty flags when it wrote a construction, which goes only
to the client the object came into range of, so a change waiting for
the next update never reached the clients already watching: a health
bar, a quick build's state, a player's new gear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:26:46 -06:00
Aaron Kimbrell
d3f5c20323 fix: a LUP exhibit's construction always carries the exhibit
LWOLUPExhibitComponent::Deserialize (0x00c63790) reads a flag and the
exhibit's LOT, and that is the only way the client learns which exhibit
is shown. DLU wrote the block on a construction only while it was dirty,
so a player who came into range after the first update saw no exhibit
until the next one was picked, up to 20 seconds later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:26:46 -06:00
Aaron Kimbrell
1382606bd1 fix: constructions keep pending changes of five more components
The level, forced movement, possessable, possessor and combat AI parts
are each read behind a flag bit (LWOLevelProgressionComponent::
Deserialize 0x00c63bd0 and the others). DLU cleared their dirty flags
when it wrote a construction, which goes only to the client the object
came into range of, so a change waiting for the next update never
reached the clients already watching: a level up, a mount or dismount,
a rail ride.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:26:46 -06:00
Aaron Kimbrell
8dd279cb45 fix: write a collectible's id on construction only
LWOCollectibleComponent::Deserialize (0x00c45ec0) reads the 16-bit id
on construction and nothing on an update. DLU wrote the id on every
serialization, so the client read 16 stray bits as the flag bits of the
components after it. The replica decoder follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:26:46 -06:00
Aaron Kimbrell
599f33d436 fix: a phantom physics construction keeps the pending effect change
LWOPhantomPhysicsComponent::Deserialize (0x00c34db0) reads the effect
behind a flag bit. DLU cleared the dirty flag when it wrote a
construction, which goes only to the client the volume came into range
of, so the clients already watching it never got an effect change that
was waiting for the next update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:26:46 -06:00
Aaron Kimbrell
5832567bc4 fix: trigger volume effects go out as live sent them
A trigger's SetPhysicsVolumeEffect is "type,amount,x,y,z,use distance,
min,max". In live phantom physics constructions:
- the direction has the amount as its length: "Push,50,0.91,-0.34,-0.22"
  went out as 45.68, -17.07, -11.04. DLU multiplied the arguments as
  they were, so a direction that isn't a unit vector pushed harder;
- a zero direction ("Repulse,150,0,0,0,true,25,100") is no direction;
- the distances go out when use distance is "true" ("true,150,175" as 1
  and 175, "true,20,50" as 1 and 50) and not when it is "1"
  ("Push,40.0,0,1,0,1,20,50"). DLU never wrote them.
LWOPhantomPhysicsComponent::Deserialize (0x00c34db0) reads the two
distances as floats and keeps the least of the min and 1, and a max of at
least that.

Seven arguments no longer read an eighth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:26:46 -06:00
Aaron Kimbrell
a269ce0cc7 fix: a simple physics construction keeps pending changes for the update
LWOSimplePhysicsComponent::Deserialize (0x00be4b00) reads the velocity
and the motion state each behind its own flag bit. DLU cleared both dirty
flags when it wrote a construction, which goes only to the client the
object came into range of, so the clients already watching it never got
a change that was waiting for the next update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:26:46 -06:00
Aaron Kimbrell
f4d3d794fd fix: construct a player in jetpack mode with its jetpack
LWOControllablePhysComponent::Deserialize (0x00c45770) reads the jetpack
mode, effect, flying and bypass checks bits on construction, and
LWOControllablePhysComponent::SendMessage (0x00d2f020) keeps the same
from SetJetPackMode. DLU sent the message but never kept it, so a player
who came into range of someone flying was told they had no jetpack.

Live player constructions in jetpack mode carried effect 167, flying 0
and bypass checks 1, and a 0 after the pickup radius (the client reads
that bit into the flying flag). DLU wrote the jetpack mode there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:26:46 -06:00
Aaron Kimbrell
102abc32f5 fix: a character's activity or PVP change is serialized once
LWOCharacterComponent::Deserialize (0x00ca3a40) reads the GM and PVP
block and the current activity each behind its own flag bit. DLU never
cleared the two dirty flags, so after the first quick build or pet taming
every later serialization of the player carried both blocks again. They
are cleared after an update now, as the guild block's is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:26:46 -06:00
Aaron Kimbrell
9bbe335acc fix: a player with PVP on is not its own enemy
LWOCharacterComponent::SendMessage (0x00d34330), IsValidPVPTarget: the
target is never the player itself. LWODestroyableComponent's IsEnemy asks
it for every character. DLU called a player with PVP on an enemy of
itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 13:07:27 -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
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
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
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