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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
An object's EnemyPrecondition (level config) decides who it is an enemy of:
whoever asks and doesn't meet the preconditions, whatever the factions
(LWODestroyableComponent::msgIsEnemy 0x00c9e930; ',' ';' ':' join groups
that must all pass, '|' alternatives). Forbidden Valley Siege's objects use
it today; any level object can.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RequestResurrect resurrected whoever sent it, so a living player could reset
their health and imagination, and repeated requests stacked respawns. The
client's destroyable resurrects only when smashed and not resurrecting
(LWODestroyableComponent::SendMessage 0x00d29e40); live, all 17 requests in
the captures came after the player's Die and before its Resurrect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The damage reduction behavior set the reduction to its amount and back to 0
when it ended, so a second reduction replaced the first and ending either
removed both. The client's behavior sends AddDamageReduction
(DamageReductionBehavior::Cast) and the destroyable adds it to the current
reduction; removing takes off that amount, never below 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client keeps an object's children as a set of ids
(LWOGameObject::childObjects). Every multi-child list in the live captures
(57) was in ascending id order. A child added twice is now kept once. The
child count also counted entries the loop then skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOGameObject loads these itself: interactionDistance from its Objects row
(16 without one) and the level's interaction_distance over it,
CheckPrecondition and objectWorldState from the level config, before its
components load theirs. The quickbuild, the pet and the race instancer read
them from the object; the race instancer now uses the Objects row's
distance like the client does, instead of a fixed 16.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parent info always said the object doesn't move with its parent. Live
sent true for objects a spawn behavior made with updatePositionWithParent
(all 70 Engineer Personal Fortress constructions in the captures, LOT 9479,
and the Summoner's Scratch, 9644); the client then moves them with the
player. Every other construction in the captures sent false, and none sent
an object world state, which stays unsent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Handlers sat in an unordered_multimap, whose order for one message is up to
the standard library: GCC ran them newest first, so the components loaded
last answered a RequestUse first, and Windows builds could differ. They are
now kept per message in registration order: the object's own handlers, then
its components' in load order (LWOGameObject::PassMessageToComponents).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DoObjectComponentLoad walks the client's load order and calls the type's
allocator from a table, like ObjectLoader2, instead of one 76-case switch.
Components with rules of their own have a static Allocator (the client's
LWOxComponent::Allocator); the rest share a registry allocator. The load
state moves to ObjectLoadState.h so the allocators can take it.
Construction bytes are identical for all 15,962 LOTs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An enemy moved every time its wander timer ran out. The MovementAIComponent
rows give a percent chance (90 or 100) to move each time; enemies with no
row (survival and instance enemies, bosses, behavior spawns) get 0 and now
only move to fight and to return.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client gives a character no mission offer component (LOT 1's registry
has none; DLU's had no missions) and never makes racing stats (74 is not in
its component load order). Same bytes for every LOT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client's script allocator adds them only for a script component it makes
(a registry script with a non-zero id, or a custom script in the config it is
sent). Characters and the registry-id-0 script LOTs no longer get them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOGameObject::MakeReadyForSerialization runs each component's
LoadConfigData and then a character's LoadFromSaveData, in the order the
components were made, once DoObjectComponentLoad has made them all; then
their Startup. Entity::Initialize now does the same with a component list in
creation order (the client's componentList); the walk's own save calls
(mission, physics, destroyable, level, character, skill, ghost) and the
destroyable's call into the buff go. A model's behaviors load in its
LoadConfigData; controllable physics places a non-character at
construction, as the client's physics takes the spawn position. A component
added later still loads at once. Same bytes for every LOT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As LWOControllablePhysComponent's and LWOPhantomPhysicsComponent's
allocators do (like the simple physics one already): the registry's physics
id is left to the model case, which makes the model's physics.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client makes a BBB component (76) for a character (DoObjectPreLoad, LOT 1)
and for registry entries (none in the CDClient), and reads it last
(LWOBBBComponent::Deserialize: a metadata-source-item bit and the item).
DLU wrote a placeholder 0 bit at the end of every construction; now only a
character's BBBComponent writes it. Every other object sends one bit less,
which the client never read (live non-player constructions decode without
it; the zone control object stays byte for byte equal to live).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The destroyable slot makes only the registry's (with its buff); a
collectible's destroyable comes right after the collectible when there is
none, and a smashable object's (is_smashable) after the walk
(LWODestroyableComponent::Allocator), as in DoObjectComponentLoad. A
quickbuild or model with is_smashable now gets its own defaults first, as
the client's order gives it. Same bytes for every LOT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApplyMovementAiIfNeeded gives an object with a script (its registry's or a
custom script) or an attached_path a movement AI component (id 0); 1843 LOTs
gain one. It follows its attached path only when that is a movement path (a
moving platform's path stays the platform's). Not on spawner objects, which
the client never loads. Nothing sent changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.10.64's DoObjectComponentLoad has an EXHIBIT case that makes nothing;
DLU makes ExhibitComponent there, built like LWOExhibitComponent on the
spawner component: its ExhibitComponent row (model offset and size) and the
exhibit_* level keys. It sends nothing in a construction (the client's
Deserialize is empty), so the 12 exhibit LOTs keep their bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entity::Initialize now runs DoObjectPreLoad (what the components will be:
the player's own components, markedAsPhantom, model behaviors, the
component whitelist) and DoObjectComponentLoad (the walk in the client's
order and what the client adds after it), as ObjectLoader2 does, with an
ObjectLoadState that answers the registry through the whitelist. Same
components and bytes for every LOT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client makes a script component for a registry script with a non-zero
id, or for a custom script in the config it is sent (LOT 176 never):
ApplyScriptComponentIfHasScript and LWOScriptComponent's allocator. A
character's script and a server-only custom_script_server stay on the
server and send nothing. The 5 LOTs whose registry script id is 0 (e.g. the
AG concert fan, LOT 3002: live sent no config and no script) lose the bit
the client never read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From ObjectLoader2 (DoObjectPreLoad / DoObjectComponentLoad):
- componentWhitelist N keeps only that whitelist's registry components
(ApplyComponentWhitelist; property models use 1).
- No pet component for an object in an inventory (LWOPetComponent::Factory).
- Havok vehicle physics: use_simple_physics makes simple physics instead,
objectWorldState 2 none.
- markedAsPhantom (any value) turns the registry's simple physics into
phantom physics with that row, and doesn't count when the registry has
phantom physics.
- Vendors get a proximity monitor first when they have none; a mission
offer's object gets one after the walk (113 LOTs gain one).
Same bytes for every LOT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where an object starts (where the level put it) moves into
ControllablePhysicsComponent::LoadConfigData, and where a character comes
into the world (its save, then its respawn point, the target scene or the
zone's spawn when it came from elsewhere) into LoadFromSaveData, out of
Entity::Initialize. Same placement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ScriptComponent row (server and client script) loads in
LoadDataFromTemplate and custom_script_server / custom_script_client in
LoadConfigData, out of Entity::Initialize, which only decides whether there
is one (a registry entry, a character or a custom script), as the client's
allocator does. Same scripts and bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PlatformSubComponent (LWOPlatform: the state between waypoints and its
serialization) is the base of MoverSubComponent (4), SimpleMoverSubComponent
(5) and the new RotatorSubComponent (6), as LWOPlatformMover,
LWOPlatformSimpleMover and LWOPlatformRotator are built on LWOPlatform. The
platform keeps one subcomponent per type the client makes
(LWOMovingPlatformComponent::SetupPlatform: one per platformIs* setting that
is true) and sends them all; the first is the one the server runs. Same bytes
for every platform with one subcomponent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vendor (16), donation vendor (100) and achievement vendor (102) are separate
entries in the client's component walk; no LOT has two of them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ProximityMonitorComponent row's radii and the MovementAIComponent row
(with the level's wanderRadius over it) move out of Entity::Initialize into
each component's LoadDataFromTemplate / LoadConfigData. Same components and
bytes for every LOT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOBaseVendorComponent is the base of the client's vendor, donation vendor and
achievement vendor; DLU's vendor logic becomes BaseVendorComponent with
VendorComponent, DonationVendorComponent and AchievementVendorComponent all
built on it (the last two derived from VendorComponent before). Nothing sent
changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Physics/, Activity/ (activity, scripted activity, shooting gallery,
quickbuild, minigame control, racing control base, racing control, gate rush),
Vendor/, Character/ (what the client makes with a character, plus ghost,
minifig and mission), Sound/ and Property/; the rest stays where it was.
File moves only; DLU_COMPONENT_INCLUDE_DIRS gives every target the folders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOCharacterComponent::Factory makes possession control, mount control, pet
creator, level progression and forced movement, then the character, all with
componentID -1; DLU now makes the same set in the same order (mount control
and pet creator are new, empty). The ghost is added after the component walk,
where the client adds it, for a character with physics. Nothing sent changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client makes 106 from the registry; the only non-character LOT with it
(16684, a test skateboard mount) now has one and writes it between the pet
and the character, the client's place for it. A character still writes it
with its other parts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Minigame control (50) is an activity component in the client
(LWOMinigameComponent) and is made from the registry, so the zone control
object (LOT 2365, registry id 0) has one; it sends what an activity sends
(dirty, no players), not a fixed 0x40000000 (which nothing sent: the old
id > 0 check never made it). The zone's script on that object is server
only: the client makes no script component for it, so it sends nothing.
DLU's zone control construction now equals live's byte for byte (209
captures, e.g. GruntMonkey world2); the decoder reads 50 as an activity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOBaseRacingControlComponent (an activity component) is the base of the
client's racing control (71) and gate rush control (103); DLU gets the same
BaseRacingControlComponent. Nothing sent changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>