Commit Graph

2729 Commits

Author SHA1 Message Date
Aaron Kimbrell
3daff99556 feat(db): measure the database's size per table and keep the samples
IDatabaseUsage: MySQL/MariaDB from information_schema.TABLES (fresh
statistics on MySQL 8), SQLite from its page counts and the dbstat table
(now compiled in) with counted rows. Samples go to database_usage and
database_usage_tables (migrations 105 / 88).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:57:55 -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
df3f46b43b fix(character): the character data packet sends txfring=true, as live
LWOInventoryComponent_Client::LoadConfigData (0x00cde200) reads txfring
from the character data; without it the client runs its inventory-full
tips (msgPlayerReady 0x00bde5e0) on every world load. Of 226 captured
live character data packets, 222 had the key, all true; 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
Aaron Kimbrell
0253fea006 feat(destroyable): enemy preconditions from the level, as the client
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>
2026-10-02 09:31:35 -05:00
Aaron Kimbrell
4b84677cba fix(destroyable): resurrect only a dead player that isn't already resurrecting
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>
2026-10-02 09:17:28 -05:00
Aaron Kimbrell
cf1e8a54c6 fix(destroyable): damage reductions add up, as the client
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>
2026-10-02 09:11:20 -05:00
Aaron Kimbrell
3236d9f08a fix(entity): children once each in id order, and the count matches the ids sent
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>
2026-10-02 08:55:04 -05:00
Aaron Kimbrell
6f4fd4054a feat(entity): the object's interaction distance, preconditions and world state, as the client
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>
2026-10-02 08:44:53 -05:00
Aaron Kimbrell
0c215de446 fix(entity): send updatesWithParentPosition from the spawn behavior, as live
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>
2026-10-02 08:30:23 -05:00
Aaron Kimbrell
9aa06f349c fix(entity): message handlers run in the order they registered, as the client
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>
2026-10-02 08:19:18 -05:00
Aaron Kimbrell
260e6cb1b1 refactor(entity): each component type has an allocator, as the client
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>
2026-10-02 08:11:58 -05:00
Aaron Kimbrell
3a79286666 fix(combat-ai): wander only as often as the movement row's WanderChance
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>
2026-10-02 08:01:11 -05:00
Aaron Kimbrell
1068fe904d fix(entity): no mission offer on characters and no racing stats, as the client
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>
2026-10-01 17:56:50 -05:00
Aaron Kimbrell
0fccea55b8 fix(entity): proximity and pathfinding from a script only when the client makes the script
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>
2026-10-01 17:51:44 -05:00
Aaron Kimbrell
b81ffb2d28 refactor(entity): components load their config and save after all exist, as the client
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>
2026-10-01 17:47:30 -05:00
Aaron Kimbrell
818d584c76 feat(entity): a model with behaviors gets no physics from the physics cases
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>
2026-10-01 17:40:29 -05:00
Aaron Kimbrell
6627b022d4 feat(component): the BBB component only where the client has one
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>
2026-10-01 17:18:52 -05:00
Aaron Kimbrell
243b2106bc refactor(entity): destroyables made where the client makes them
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>
2026-10-01 17:13:37 -05:00
Aaron Kimbrell
1481bb7b32 feat(entity): movement AI for scripted objects and ones with a path, as the client
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>
2026-10-01 17:07:47 -05:00
Aaron Kimbrell
f3ae06cee6 feat(component): the exhibit component (32) the client's loader leaves dead
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>
2026-10-01 16:56:25 -05:00
Aaron Kimbrell
477dd9f3e3 refactor(entity): load an object in the client's two steps
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>
2026-10-01 16:52:15 -05:00
Aaron Kimbrell
432f4f6ff9 fix(component): send a script component only when the client makes one
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>
2026-10-01 16:38:56 -05:00
Aaron Kimbrell
c69b26b661 feat(entity): more of the client's object loading rules
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>
2026-10-01 16:32:07 -05:00
Aaron Kimbrell
b24661710b refactor(component): controllable physics places its object itself
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>
2026-10-01 16:05:04 -05:00