Commit Graph

1098 Commits

Author SHA1 Message Date
Aaron Kimbrell
3672b64dec feat(rails): answer RequestRailActivatorState
The client's rail activator sends RequestRailActivatorState (1479, no
payload) when it is added to the world (LWORailActivatorComponent::
SendMessage 0x00c00da0) and live answered with
NotifyRailActivatorStateChange (1478, bActive) to that client; the client
stores rail_activator_active and updates the rail's pick type (usable or
not). Live: 152 requests, 413 answers, every one bActive = true. DLU
dropped the request and never answered.

RailActivatorComponent keeps the level key rail_activator_active
(default true; every rail in the live levels sets it to 1) and the
request is answered with it.

Check in game: in Ninjago Monastery and Nimbus Station, rails (spinjitzu
posts, the Nexus Tower rails) show as usable and work as before when
first loading the world and after zone changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:44:06 -05:00
Aaron Kimbrell
51981b7bee feat(property): resend the equipment on ResyncEquipment
The property editor sends ResyncEquipment (1238, no payload)
after the player picks up or sets down a model they carry: 59 live
packets, every one right after PlaceModelResponse. Live answered with no
game message, only replica serializations (packet 0x1b), i.e. the
player's equipment sent again. DLU dropped it.

The handler marks the inventory's equipped items dirty and serializes the
player, so everyone gets the equipment again. That live re-sent the
inventory part of the replica is inferred: the captures show a
serialization but it was not decoded.

Check in game: on your property, in build mode, pick up a model from the
world and place it again a few times, then leave build mode: your hat,
shirt, weapon etc. are all shown on you (and to a second player
watching), none invisible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:44:06 -05:00
Aaron Kimbrell
dd116b9794 feat(skills): end a dead caster's skill when a client reports it (CasterDead)
When a client gets EchoStartSkill (or SyncSkill) from a caster it sees
as dead, aimed at another object, it logs "msgEchoStartSkill
msgCasterDead" and sends CasterDead (120: optional i64Caster, optional
uiSkillHandle) through that target
(LWOSkillComponent::msgEchoStartSkill 0x00d5dc90). 303 live packets, the
target nearly always the attacked player, the caster a spawned enemy;
the next messages were mostly Die and SetStunned for the player. DLU
dropped it.

The server now ends that skill on the caster: its behaviors with that
handle are dropped (end entries run, pending timers and syncs discarded,
its projectiles removed), so a hit an enemy scheduled before dying does
not land later. It only does so when the server also sees the caster as
dead, so a client can't cancel a living enemy's attack. What live did
with the message is inferred from the name and when it was sent.

Check in game: kill an enemy in the middle of a slow or charged attack
(e.g. a Maelstrom horseman or a spider queen add): its attack does not
hit after it has died, and other enemies' attacks still hit normally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:44:06 -05:00
Aaron Kimbrell
8d4422fa5c feat(character): keep the rocket build the client reports (SetLastCustomBuild)
The client sends SetLastCustomBuild (890, a u32-length wide string;
other references) with its rocket build, e.g. "1:14454;1:4714;1:4715;", when it
assembles the rocket the player carries: after EquipInventory at a
launchpad and on load when landing (202 live packets). It stores it as
lastUsedRocketInfo (LWOCharacterComponent::SendMessage 0x00d34330) and
reads it back from char@lcbp on load (0x00ca4910). Live saved lcbp in the
same format on every character.

DLU dropped the message and only set lcbp when the server equipped the
rocket. The handler now keeps the client's build as the rocket config
(saved as lcbp). It does not change whether the player lands on the next
load: DLU uses an empty config to skip the landing after non-rocket
transfers, and that still happens when the server clears it.

Check in game: build a new rocket in the rocket builder, launch from a
launchpad and land: the rocket shown on landing and the builder's default
next time are the new build. Teleport to another world without a rocket
(e.g. from a property): no landing animation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:44:06 -05:00
Aaron Kimbrell
921d2c8c2a feat(character): keep and save the tooltip flags (SetTooltipFlag, char@ttip)
The client sends SetTooltipFlag (469: bFlag, then the tooltip,
2 live packets, both tooltip 24) when a tooltip has been shown, and
follows it with SetFlag for the same ID. DLU dropped it and never wrote
char@ttip, so the client's tooltip bits reset on every load.

- SetTooltipFlag message struct and handler: the bit is set or cleared on
  the CharacterComponent exactly as the client does it
  (LWOCharacterComponent::SendMessage 0x00d34330): tooltips above 127 are
  ignored, set = 1 << tooltip, clear = mask ~1 << tooltip (which also
  clears the lower bits), shifts of 64 or more give 0.
- Saved as char@ttip, a 64-bit value (the client reads it with
  GetLongLongValue in LoadFromSaveData 0x00ca4910). Live wrote it on
  every character (226 live charxmls: "0" or "16777216" = tooltip 24).
  Old saves without it load with 0.

Check in game: on a new character, trigger a first-time tooltip (e.g.
the one about an item or a new area), change zones and log out and back
in: the character loads normally and the same tooltip does not come back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:44:06 -05:00
Aaron Kimbrell
70df41ae8d feat(missions): keep and save the mission journal type states
The client sends SetMissionTypeState (851) for the journal's mission
types and subtypes (Missions.defined_type / defined_subtype) at load and
after mission updates: 650 live packets, every one with the optional
state left at NEW, the subtype written before the type. DLU
dropped it and never wrote the states, so the client lost them on every
load.

- SetMissionTypeState message struct and handler: the state is stored on
  the player's MissionComponent (byte-sized as the client keeps it,
  LWOMissionComponent::msgSetMissionTypeState 0x00c90d00).
- Saved as live wrote it (226 live charxmls): after <cur>,
  <ts><type v="Build"><st sub="" val="1"/></type>...</ts>, <ts/> when
  empty. Read back per <type>; old saves without <ts> load with none.
  The client's reader (0x00d171c0) reads v from <ts> rather than from
  each <type>, so it files every state under one type; the server keeps
  the types apart.
- Mission::SetMissionTypeState (on accept) records NEW for the mission's
  type instead of doing nothing.

Check in game: accept missions of a few kinds (a location mission, a
battle achievement), open the passport/journal, then log out and back in
and change zones: the journal tabs keep their "new" markers as before,
nothing errors, and the character still loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 03:44:06 -05:00
Aaron Kimbrell
eaba3d8459 feat(inventory): equip and unequip like live
Live, on every equip and unequip (captures: 22,422 ChangeObjectWorldState,
4,269 equip effects, 245 UncastSkill):

- ChangeObjectWorldState on the item to everyone: ATTACHED when equipped,
  INVENTORY when unequipped, proxies (sub-items) included.
- The equip-<slot> / unequip-<slot> effect on the wearer to everyone
  (effect -1, priority 1.07), the slot picked from the item's equip location
  as the client's ItemComponent does (0x00cdafb0): special_l left,
  special_r right, hair head, clavicle, chest, legs; none for others (e.g. a
  rocket's Extra_1); the item's equipEffects with "equip"/"unequip" when it
  has one; no equip effect for noEquipAnimation items, no unequip effect for
  proxies.
- Order when an item replaces another: the new item's state and effect,
  then the old item's, then the old item's proxies taken away
  (RemoveItemFromInventory, UnEquipInventory with ignoreCooldown,
  ChangeObjectWorldState INVENTORY, to the player), then UncastSkill for
  the old item's equip skills (castOnType 1, not its proxies'), then its
  skill removed and the new one added, then the new item's proxies.
- An equipped item taken out of the inventory is followed by
  UnEquipInventory (ignoreCooldown) to the player.

DLU sent none of these (only the rocket's ChangeObjectWorldState, which
now comes from equipping it, before RocketEquipped as live).

Check in game (with a second player watching): equip and unequip a hat, a
sword, a shield, a shirt, pants and a backpack; both players see the equip
animation/effect. Put a ninja hood on, then another hat over it: the hood's
back piece disappears and its passive effects end (no lingering aura or
stat). Launch a rocket: it is still shown during the launch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
b845faee72 fix(messages): ChangeObjectWorldState and UnEquipInventory as the client reads them; add UncastSkill
ChangeObjectWorldState's state is optional on the wire (a flag, then the
state when it isn't INWORLD): live sent 80 80 00 00 00 for ATTACHED. DLU
wrote the bare u32, so the client read the rocket's ATTACHED as INWORLD.

UnEquipInventory gets its optional replacementObjectID (the client always
sends the flag; the server-sent copies in the captures have it too), so it
can be sent server -> client.

New UncastSkill (1206, server -> client: the client ends its running
instance of the skill, LWOSkillComponent::msgUncastSkill 0x00bd86d0).

Tests: all three against live capture bytes.

Check in game: launch a rocket; the rocket is shown in the player's hands
during the launch animation (for the launcher and players nearby).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
aed16a8c80 fix(stats): RocketsUsed and QuickBuildsCompleted where live sent them
RocketsUsed: live counted the rocket when the client asked to go
(FireEventServerSide "ZonePlayer"), right before TransferToZone (captures:
94 of 117 directly before it). DLU counted it when the launch animation
started, so a launch that never went counted too.

QuickBuildsCompleted: live sent it after RebuildNotifyState(Completed) and
the completion effect (507), before EnableRebuild (218 of 227 samples); DLU
counted it before the notify state.

Check in game: launch a rocket to another world; Rockets Used goes up by 1
as the screen fades out. Finish a quick build: Quick Builds Completed goes
up by 1 as the build finishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
fd10e19a06 feat(stats): heals, repairs and restores count what they applied
Live sent TotalArmorRepaired / TotalImaginationRestored with what a repair
or restore applied, 0 included (1,050 and 2,148 zeros in the captures: a
power-up picked up at full), and never a TotalDamageHealed or
TotalDamageTaken of 0. DLU counted every change of the value instead, so
setting stats up (loading, respawning, level changes) counted as healing
and a heal at full health counted as 0 damage taken.

Now Heal, Repair and Imagine count what they applied; health lost and
imagination spent still count wherever they change.

Check in game: pick up an armor or imagination power-up when full; the
passport's Armor Repaired / Imagination Restored stay the same. Take damage
and heal; Damage Taken and Damage Healed go up by what the bar shows.
Respawn: Damage Healed doesn't jump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
1f29e9604a feat(stats): coins, bricks and smashes counted by the server as live did
The passport totals for coins, bricks and enemies came from the client's
ModifyPlayerZoneStatistic, which live's client sent for its per-zone counts;
live's server counted the totals itself and told the client
(UpdatePlayerStatistic). Now:

- CurrencyCollected (the amount gained) right after every SetCurrency that
  raised the coins (pickups, missions, achievements, selling, activities);
  none for losses. Captures: 15,815 pickups + mission/achievement/vendor/
  activity gains, all with the stat; no loss had one.
- BricksCollected (the count) after every add to the bricks inventory the
  client is told about (captures: 999 of 1,010, pickups and relocations).
- The kill counts go to the killer right after Die, before the loot:
  EnemiesSmashed when the DestructibleComponent's isnpc is set, else
  SmashablesSmashed for a smashable (captures: 2,766 NPC kills and 538
  smashables; faction or AI don't decide it, e.g. the Banana Cluster has AI
  and counted as a smashable). Neither while racing. Before, every kill was
  a SmashablesSmashed.
- ModifyPlayerZoneStatistic from the client only updates the zone counts,
  so nothing is counted twice.

Check in game: pick up coins, sell an item, finish a mission, pick up
bricks, smash a stromling and a crate; each passport number goes up by the
right amount at once and stays the same after relogging (no double counts).
The per-zone statistics still go up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
752f079100 feat(stats): send UpdatePlayerStatistic to the player as live did
Live sent UpdatePlayerStatistic (1481) server -> client for every statistic
the server counted (59,933 messages in the captures, none client -> server);
DLU only counted them, so the passport stayed stale until the next login.
CharacterComponent::UpdatePlayerStatistic now tells the player's client what
it counts (not what a client reported), with the amount left out when it is
1, byte for byte as the live samples.

MetersTraveled: whole meters now count (each position update's distance was
cut to a whole number before, so short steps never counted) and go to the
client once 25 have gathered (live's values were nearly all 25-33 while
moving) and the rest, 0 included, when the player is taken down on leaving
the world (live sent one more after TRANSFER_TO_WORLD). DistanceDriven goes
out every 10 seconds of racing (inferred: live sent it every few hundred
packets, about 1300 units at race speed).

Check in game: open the passport's statistics page, walk around, collect a
power-up, complete a mission; the numbers go up without relogging. Walk
25+ meters; Meters Traveled matches what the passport shows after
relogging. Change worlds and relog: the meters were kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
5b1be32e06 feat(ghosting): scene ghosting on by default
ghosting_scenes defaults to 1 in the worldconfig.ini template, the settings
catalog and when the key is missing. Zones without a terrain scene map keep
distance ghosting; ghosting_scenes=0 turns it off everywhere.

Check in game: with a fresh worldconfig.ini the world server log says
"Scene ghosting is on" in Avant Gardens; objects appear by scene (the whole
scene you are in and its neighbours) instead of within 100-150 units. In a
zone without a scene map (e.g. a property) the log says it is off and
ghosting is by distance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
8d6fe4b116 fix(ghosting): scene ghosting follows the client's scene streaming
Re-checked against client 1.10.64: Zone::Run calls StreamScenesAroundPosition
with the ghost reference position, and with the controlled object's own
position only when the two differ (ghost reference override on). The client
loads the scene under the reference point and the global scene; with the
override on it also loads the scene under the player and its connected
scenes. The cell lookup (floor(v + 0.5), x cell * resolution + z cell), the
transition pairs and FixupInvalidTransitions match what ZoneScenes does.

Scene ghosting now adds the scenes around the player while the ghost
reference is overridden (cinematics), and the comments say the server keeps
each scene's neighbours too (a superset, so objects across a transition
exist before the player crosses it).

Check in game (with ghosting_scenes=1): walk across scene transitions in
Avant Gardens and Gnarled Forest; objects on both sides show up and nothing
pops in at the line. Play a cinematic that moves the camera away (e.g. a
mission cinematic) and objects around the player stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
641fd72969 feat: online players get grant changes at once
Adding or removing a grant sends the existing PLAYER_ACTION refresh through master to every world: REFRESH_ACCOUNT
for an account's grants, REFRESH_CHARACTER for a character's. A world with that account's sessions (or the loaded
character) drops the grants it kept, so the next command reads them again: no relog. The dashboard toasts whether the
player was online. The dashboard's own open pages already get the new rights on their next request, and their
WebSockets are checked again.

Check: with a character in game, grant it /spawn on the dashboard (toast: "Applied in game at once") and use /spawn
without relogging; remove the grant and /spawn is refused again at once. With the player offline the toast says it
applies when they next play.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:16:50 -05:00
Aaron Kimbrell
e00d22152e feat(game): slash commands accept permission grants
A command may be used when the character's GM level allows it or a grant does: a grant of the command, of every
command up to a GM level, or of the dashboard permission the command follows (so accounts_kick covers /kick). A deny
of any of those takes it away even when the level allows it, except from GM 9 accounts (also while they play at a
lower level). Grants never take anyone below a command's floor above GM 1 (/execute), and commands the client handles
keep their fixed level. Expired grants count for nothing. The grants are the account's and the logged-in
character's, read from the database the first time a command is used and kept on the User. /help lists the commands
a player may use this way. The self and rank rules for commands that act on another player count grants of self_* and
manage_equal_rank too. A denied command says it was taken away.

Check: grant a GM 0 account /spawn (or "every command up to GM 8") on the dashboard, relog, and /spawn works and
shows in /help; deny /spawn from a GM 8 character: "it was taken away"; grant accounts_kick and /kick works (the rank
rules still apply to whom). dGameTests SlashCommandGrantsTest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:16:49 -05:00
Aaron Kimbrell
266f8b81c5 fix(ugc): send the served mesh checksums when a player loads a property
A client asks for a served model's HKX and is answered with the model's
LXFML, so it builds its own collision. That build also writes the
client's own .nif over the served one it downloaded and caches that
file's MD5 as the NIF's manifest info (client 1.10.64:
LWOBBBInterface::GenerateModelFromLxfml 0x00b6c220, MainThread_Process-
ModelResponse 0x00b5a1e0; valid entries never expire, 0x010186d0). On the
next load of the property the client used its cached checksum and file
without asking, and drew its own build instead of the served mesh.

Now, before the property's objects are constructed for a player, the
world sends them the served NIF checksum of every made model placed there
(UgcManifest::OnPropertyLoading). A client whose cached checksum is its
own build's downloads the served mesh again; one that has it already
downloads nothing. Nothing is sent unless ugc_manifest and
ugc_manifest_models are 1.

Check in game (ugc_manifest=1, ugc_manifest_models=1): visit a property
with made models, leave, come back in the same session (and after a
client restart): the models show the served mesh every time, and still
have collision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:05:42 -05:00
Aaron Kimbrell
bd1d76193f fix(ugc): switch a client to a served mesh only once its own build is done
When a model finishes on the UGC server while players are on the property,
each client was sent the served NIF checksum, NotifyClientUGCModelReady and
the model constructed again at once. A client that was sent the model's
LXFML shortly before (the property load, a request for a model not made
yet, the owner's brick by brick save) is still building it then: the build
writes its own .nif over BrickModels/UserMade/<id>.nif and caches that
file's MD5 as the NIF's manifest info when it finishes (client 1.10.64:
LWOBBBInterface::GenerateModelFromLxfml 0x00b6c220, MainThread_Process-
ModelResponse 0x00b5a1e0), undoing the switch, so it kept its own build.
That is the usual case: another player's client asks for a model the owner
just placed, gets its LXFML, and that request makes the UGC server make the
model at once.

Now such a client is switched 30 seconds after the last LXFML sent to it
(UgcManifest::ServedMeshSwitches, BUILD_SETTLE); another LXFML sent
meanwhile starts the wait again. Other clients are switched at once, as
before. The served checksum is looked up when the switch happens. HKX
requests are still answered with the LXFML, so collision stays the
client's own.

Check in game (ugc_manifest=1, ugc_manifest_models=1, two accounts on one
property): the owner saves a new model; within about 30 s of the UGC
server making it, the other player's copy blinks out and back with the
served mesh (vertex AO, look of the dashboard's viewer), and still has
collision (walk into it). The owner's copy switches the same way. The
world log shows "Switching ... to the served mesh of model ...".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:05:42 -05:00
Aaron Kimbrell
a61a01c4fb test(messages): UpdatePlayerStatistic against live capture packets
Live sent UpdatePlayerStatistic (1481) server to client; the struct
already reads and writes those packets. Documents what the client does
with it. Needs the FromLiveCapture test helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:44:44 -05:00
Aaron Kimbrell
7621a4fcdc feat(messages): RemoveBuffsAppliedByObject struct
Game message 1726 (optional object ID, default 0), as the 1.10.64 client
reads it. Live sent it to every player when an object left the world;
nothing sends it yet. Tested against a live capture packet. Needs the
FromLiveCapture test helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:44:44 -05:00
Aaron Kimbrell
a52e777097 fix(messages): client names for game messages 792, 793, 915 and 916
The 1.10.64 client's message table (GameMessage::<Name>::Initialize) names
792 DeletePropertyResponse, 793 CreateModelFromClient, 915
PropertyModerationAction and 916 PropertyModerationActionResponse.
Only the enum names change; the IDs stay pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:44:44 -05:00
Aaron Kimbrell
7440e5744d fix(rails): rider flags default to the RailActivatorComponent row
StartRailMovement's bDamageImmune, bNoAggro and bShowNameBillboard came
only from the level keys rail_activator_damage_immune, rail_no_aggro and
rail_show_name_billboard, which 23 of the 80 level rails do not have, so
those rails sent false. The client reads the same flags from the
RailActivatorComponent row (DamageImmune, NoAggro, ShowNameBillboard; all
11 rows are 1) in LWOPlayerForcedMovementComponent::LoadRailData
(0x00c85dc0), and the message's values replace the row's unless bUseDB
(msgStartRailMovement 0x00ccd9c0). The server now takes the row's value
and lets a level key replace it when the key is there. Wire format is
unchanged; only the flag values sent for the 23 rails change (to true,
matching the 5 live samples).

In game: ride the 23 rails without the keys, all in the Ninjago earth gauntlet
(earthgauntlet levels: dart spinners, 8 spinners, millstones, boss) near enemies: you are not
damaged or targeted while riding, and name billboards behave as on the
other rails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:34:55 -05:00
Aaron Kimbrell
54cd9abd7a feat(loot): smashables use the level's smashable_loot_matrix
A level object's smashable_loot_matrix replaces the DestructibleComponent
LootMatrixIndex when smashable_loot_matrix_set is true or absent and the
matrix is not -1, as the client resolves it
(LWODestroyableComponent::LoadConfigData 0x00c44cb0). DLU only read the
key in BaseInteractDropLootServer, so tagged smashables dropped their
template's loot. Resolution in DestroyableComponent::GetLevelLootMatrix,
with tests. No wire change.

In game: smash level smashables that carry smashable_loot_matrix_set=1
(11 level objects in the 1.10.64 levels) and ones with a matrix but no _set key; they drop the
level's loot instead of the template's. Ordinary smashables
(smashable_loot_matrix_set=0) drop what they did before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:34:35 -05:00
Aaron Kimbrell
721925add0 fix(factions): override_faction 0 keeps the template's factions
The client (LWODestroyableComponent::LoadConfigData 0x00c44cb0) uses a
level object's set_faction only when override_faction is absent or true;
with override_faction=0 LoadDataFromTemplate (0x00c9f900) puts the
DestructibleComponent factionList back. 2948 level objects have
override_faction=0, and DLU applied their set_faction anyway. The
resolution is now DestroyableComponent::GetLevelFactions, with tests.
No wire change.

In game: enemies and smashables placed in levels with a set_faction but
override_faction=0 (most smashables, many enemies) are targeted and
aggro as before for the common case; check a few enemies in AG/GF/FV
still fight you and friendly objects are not attacked, and that
smashables still smash and give credit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:34:22 -05:00
Aaron Kimbrell
107437e72f fix(inventory): a save in build mode keeps the equipment from before it
Entering build mode pushes the equipped items (PushEquippedItems); the
thinking hat and a model picked up off the property are then equipped
until PopEquippedItems puts the old equipment back. A character save in
between (the world's periodic save, a disconnect, a zone change) wrote
what was equipped at that moment, so a carried brick built model (LOT
6662) was saved in MODELS with eq="true" and equipped again on every
load. While the equipment is pushed, the save now writes the pushed
equipment as equipped instead. Nothing is dropped from the save.

In game: on a property, pick up a brick built model and carry it, wait
for a save (or log out) while carrying it, log back in: the model is in
the Models bag, not equipped, and your normal gear is worn. Characters
already saved with an equipped model keep it until it is put away or
the save is cleaned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:33:58 -05:00
Aaron Kimbrell
109a936798 feat: live updates move every server onto a new build without a restart
With new binaries in place, master moves everything onto them while the
server keeps running (the dashboard's Live update, /liveupdate or SIGUSR2 to
master):

- database migrations of the new build first; a failure stops there
- UGC finishes the jobs it is running (queued rows stay pending), auth
  restarts, chat hands its teams to master for the next chat server; master
  starts the new processes and retries ones that don't come back
- once the new chat server is up (CHAT_SERVER_READY) every world connects at
  once and sends its players again (LoginSessionNotify resync, no login logged)
- every world instance is replaced with an instance migration: public worlds
  and private ones (same password) at once, properties after the old instance
  saved and froze the property (MIGRATE_PREPARE: no building, claiming or
  saving there any more), activity zones and character select once their
  players left or after a wait; empty instances just stop, zones in
  prestart_worlds get a new one first
- the dashboard restarts last and picks the status up again

Players land where they stood (position carried in CarriedPlayerState, also on
properties and Moon Base). Draining instances get no new players
(InstanceMigration::AcceptsNewPlayers) and show as "Moving players" in the
world list. The order lives in LiveUpdateMachine.h without master state and is
unit tested; master's glue is LiveUpdateCoordinator. Master itself is not
replaced. Message IDs are appended only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:23 -05:00
Aaron Kimbrell
8d57cc82cf feat: /reprocessproperty, make a property's models again and reload it
A GM 8 command: every brick built model on the property you are on goes
back to the UGC server's queue. Once all are made (at most 15 minutes),
everyone on the property is sent the new mesh checksums and transferred
back into the same zone and clone, so their game loads the property again
and downloads the new meshes. Swapping a model's mesh in place doesn't
work in the client, so the whole property is loaded again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:21 -05:00
Aaron Kimbrell
ebf3a05704 feat(property): best friends of the owner can build on the property
property_bff_build (off by default, as live): while the owner is in build
mode, their best friends can join it and place, move and pick up models,
build brick by brick and edit behaviors, each from their own inventory.
Only the owner starts build mode; a best friend still in it when the owner
leaves keeps building until they leave it.

The client lets only the property's owner edit, so each player now gets
their own DownloadPropertyData, with their own id as the owner while they
can build, sent again whenever that changes. SetBuildModeConfirmed and
GetModelsOnProperty go to each player instead of everyone. The first
builder makes the property private and pauses the models; the last one
restores them. Saving no longer needs the owner in the world.

A model taken off the property goes to whoever placed it; if they aren't
here it stays placed. Privacy and the property's name stay owner only
(neither was checked before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:20 -05:00
Aaron Kimbrell
5396594690 fix(ugc): served models keep the served mesh; physics from the LXFML sent for the HKX
Sending every model's LXFML at property load and then switching to the
served mesh (served checksum, NotifyClientUGCModelReady, the model
constructed again) left the client drawing its own build. Now made models'
LXFML is left out of the property load again: the client downloads and
draws the served mesh, and when it asks for the HKX it gets the LXFML,
builds the model and loads its own HKX, so the model has collision while
the mesh drawn stays the served one. The load-time switch is removed; the
switch for a model made again while players are there stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:20 -05:00
Aaron Kimbrell
92a87fa735 fix: save_property_location saves the position on the property too
The zone was saved on a property but the position wasn't (the physics
component kept its own "not a property" check), so logging back in put the
character at their old main world coordinates on the property map, outside
it. Both now use Character::SavesLocationInThisZone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:20 -05:00
Aaron Kimbrell
5596f0fa2d fix(ugc): a client back at character select starts its UGC switches afresh
Switching characters keeps the connection (the client sends
CHARACTER_LIST_REQUEST to the world it's in), so the per-client UGC state
(waiting requests, pending mesh switches, the 3-switches-per-model cap)
carried over to the next character. It's now dropped as on a disconnect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:20 -05:00
Aaron Kimbrell
e27f2085dc fix(ugc): construct the model again a second after taking it down
Taken down and constructed again in the same batch, the client dropped the
construction of the object it hadn't deleted yet, and most models
disappeared. The construction now follows a second later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:20 -05:00
Aaron Kimbrell
379f09dd7c feat: save_property_location, logging back in on a property
A character's location was only saved outside properties, so logging out
on a property and back in always went to the last world before it (as
live). save_property_location=1 (worldconfig.ini, dashboard: world
settings) saves it on properties too, and login returns to that property.
Off by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:19 -05:00
Aaron Kimbrell
32448a8556 fix: taking an object down for one player leaves the other players' load queue alone
DestructEntity for one player (ghosting, or a model constructed again for
one player) also removed the object from the queue of every player still
loading, so a player loading at that moment never got it. Now only the
player it was taken down for loses it from their queue; taking an object
down for everyone still clears it for all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:19 -05:00
Aaron Kimbrell
d1a61450f9 fix(ugc): construct the model again only after the client's flush
Constructed right after NotifyClientUGCModelReady, the flush the client does
on its load thread could land after the new object had loaded its mesh, and
most models disappeared. The model is now constructed again 1.5 seconds
later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:19 -05:00
Aaron Kimbrell
b118ecfb73 fix(ugc): swap to the served mesh by constructing the model again
NotifyClientUGCModelReady only flushes the client's cached NIF, HKX and
LXFML and loads them again as preloads (0x00ca6430): an object already
drawn keeps its mesh, so the switch to the served mesh after a property
load (and a model made again) changed nothing on screen. Now each player
also has the model taken down and constructed again: the new object loads
the NIF, whose cached checksum is the served one (sent first), and the HKX,
whose checksum is still the one the client built, so it keeps collision.
Only models shown to that player are constructed again. Each switch is
logged.

Also corrects the address of the client's brushed steel texture
registration in the docs: 0x00467090 (RegisterBrushedSteelTextures) in
the 1.10.64 client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:19 -05:00
Aaron Kimbrell
e5a230e6a3 fix(ugc): served player models keep the client's own collision
With ugc_manifest_models the world left made models out of the LXFML it
sends when a property loads, so the client never built them and had no HKX:
the UGC server makes no physics, and the HKX request got a 404, so served
models had no collision.

Now every model's LXFML is sent and the client builds each one (NIF and
HKX). For the models whose mesh the UGC server made, once the client has
loaded and 3 seconds after, the world sends it the served NIF's checksum and
NotifyClientUGCModelReady: the client drops what it cached for the model and
asks again, downloads the served mesh (its own NIF no longer matches) and
keeps its own HKX (still matching). An HKX request is answered with the LXFML
too, followed by the same switch, at most 3 times per client and model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:19 -05:00
Aaron Kimbrell
83b87744c1 feat: scene ghosting, as the client streams a zone's scenes
The client keeps the scene under the player loaded, the scenes the zone
file's transitions connect to it, and the global scene
(Zone::StreamScenesAroundPosition 0x0108a3f0, TerrainManager::GetSceneAtPos
0x01069010, the connected scenes at 0x01066500; transitions naming a
missing scene dropped as Zone::FixupInvalidTransitions 0x010842e0 does).

- ZoneScenes (dCommon): the terrain's scene map lookup and the scene graph,
  shared by the world server and the dashboard.
- Objects remember the scene they were placed in (spawners pass theirs on).
- ghosting_scenes=1 (world config, off by default): players get the objects
  of their loaded scenes instead of the ones within the ghosting distances;
  objects from no scene go by the scene under them. Zones without a scene
  map keep distance ghosting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:17 -05:00
Aaron Kimbrell
9d76fe9550 feat(ugc): placed models from the UGC server without 3D services
A placed model's client (1.10.64, UGCUSE3DSERVICES=7:0) always loads its
blueprint's NIF and HKX (its BlueprintComponent sets renderUserGen and
physicsUserGen itself) and its LXFML, asks the world for each file's
manifest first and waits for the answer with no timeout.

With ugc_manifest=1 and the new ugc_manifest_models=1 (default 0) the world:
- leaves the models whose mesh the UGC server made out of the LXFML it
  sends when a property loads,
- answers their NIF with the UGC server's checksum, their LXFML with the
  stored LXFML's (worked out once and kept) and their HKX as not known,
- sends a model that isn't made its LXFML (once for the three requests),
  so the client builds it itself and no model is left waiting.

When the UGC server writes a model's mesh with a new checksum it sends
UGC_MODELS_MADE (a new master message, appended) to the master, which
passes it to every world; a world with that model placed sends its
players the new NIF checksum and NotifyClientUGCModelReady (game message
909, the blueprint id), so clients switch to the served mesh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:15 -05:00
Aaron Kimbrell
03842d794a fix: character select starts on the last used character again
Sorting the list by creation order (as live did) picked the selected
character by Character::GetLastLogin, which is never loaded, so the
oldest character was always selected. Select the one the database lists
first (ORDER BY last_login DESC), as main does, and keep the live order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:14 -05:00
Aaron Kimbrell
b040058cc8 fix(ugc): give old cars and rockets a build id when a character loads
Cars and rockets built before builds were recorded have no subkey and no
ugc_modular_build row, so the client has no blueprint id to ask for their
icon with. When a character loads, such an item (a ModularBuildComponent
createdLOT with assemblyPartLOTs but no subkey) gets what a new build
gets: a persistent id as its subkey and a ugc_modular_build row with its
modules and owner. The next save keeps the subkey; nothing is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:14 -05:00
Aaron Kimbrell
79235702b3 feat(ugc): answer the client's UGC manifest requests without 3D services
With UGCUSE3DSERVICES=7:0 (the client's default) the client asks its world for
a blueprint file's MD5 and size (REQUEST_UGC_MANIFEST_INFO, world 27) and then
downloads BrickModels/UserMade/<id % 1000>/<id>.<ext>.sd0. Layouts checked in
the 1.10.64 client: the request is a u64 blueprint id and a u8 resource type;
the answer (UGC_MANIFEST_RESPONSE, client 60) repeats them and adds a u8 valid,
the u32 size and the 16 byte MD5 of the inflated file, 37 bytes after the 0x53
exactly or the client drops it.

- dNet: WorldPackets::RequestUgcManifestInfo, ClientPackets::UgcManifestResponse
  (eUgcResourceType), with byte tests against the client's layouts.
- Database: ugc_file_checksums (per model or module combination and file) and
  ugc_modular_build.combination_id (migrations 89 / 72), GetUgcFileChecksum
  looks a blueprint up as a model, else as a build through its combination.
- UGC server: every download is also written as .sd0 (Sd0::Compress); workers
  hand the checksums back and the main thread stores them; old items get their
  sd0 icon and checksum, and builds their combination id, once at start-up, a
  few per tick; serves <dir>/BrickModels/UserMade/<bucket>/<id>.<ext>.sd0 under
  client_path, /<folder>/UserBrickModels and the root (.hkx 404).
- World: UgcManifest answers on the main thread with one indexed query per
  request; files not made yet are answered when they are (looked at again
  every 5 seconds), and a model in its quiet period is made right away. No
  worker threads, HTTP or file reads in the world.

Off by default (ugc_manifest=0): checked in game, the client then downloads
from http://127.0.0.1:80/lwoclient/UserBrickModels/ whatever its boot.cfg says
and logs the player out when it can't connect, so icons need the UGC server on
port 80 of each player's machine. docs/UgcServer.md has the details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:14 -05:00
Aaron Kimbrell
8ab7e496b0 feat(bbb): answer FetchModelMetadataRequest for brick built models
The client asks for a model's metadata (name, owner, behaviors and its blueprint's
bricks and box) when it shows a brick built model item's tooltip, a model on a
property or an exhibit, and shows BBB_LOADING_BLUEPRINT until it gets it. The
world server never answered. It now does as live did: UG data for the model
(found among the player's items by subkey, else among placed models) and, for a
brick built model, the blueprint data from its ugc row and LXFML.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:10 -05:00
Aaron Kimbrell
1a2758feab feat(ugc): icon light matched to the game, debounce, shared car icons, purge, icon editor
Brighter icons: a world light, a fill from the camera, a highlight, exposure
and contrast; with the defaults the icons' mean luminance matches the game's
own model icons (118 against 120 on a scratch set). Every icon parameter is
listed once (UgcIconParams: key, setting, range, default); the settings,
the dashboard's settings entries and the icon editor come from that list.
Presets per kind (player models, each car or rocket build type from
ModularBuildComponent) and overrides per model or module combination are in
ugc_icon_settings.

Saved models wait ugc_debounce_seconds (sharedconfig) after the owner's last
save before they're made (ugc.process_after); a client asking for one, the
owner leaving the world or a reset ends the wait.

Cars and rockets: one icon per combination of modules (sorted LOTs), shared
by every build of it; builds of a combination made already are marked made
right away, the client's per-blueprint downloads serve the shared files.

The dashboard can delete one item's files, purge by filter or all, preview
icons with any values on the UGC server (/admin routes, master password),
save presets and overrides, and draw a kind's icons again (icons only).

Migrations dlu/mysql/86 and dlu/sqlite/69. Not done yet: the dashboard
editor's lighting controls in the page script (routes are there), docs for
it, the empty-model state, /ugc?item= links, the shared fetch helper; the
storage size and property loading bugs are next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:09 -05:00
Aaron Kimbrell
0de9a57e3f fix(inventory): 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 x@bp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:08 -05:00
Aaron Kimbrell
6febb6d63c fix(racing): put racers going the wrong way back on the track
The client works out on its own when its racer goes the wrong way and
shows a 6 second countdown, but it only moves the car when the server
sends RacingSetPlayerResetInfo, which DLU never did for this.

The server now follows the reset planes the way the client's
LWORacingControlComponent does (1.10.64): each path waypoint is a plane
facing along its rotation; the racer starts between planes 0 and 1,
moves forward when in front of the upcoming plane and back when behind
the last one (CheckUpcomingResetPlane @ 0x00c7edf0, CheckLastResetPlane @
0x00c7f1a0, CrossResetPlaneBackward @ 0x00cba380). Driving back
through a second plane in a row starts the client's countdown
(UpdateWrongWayCount @ 0x00be5c10, 6 seconds); going forward through a plane ends it. When
it runs out, the racer gets the same reset as an unsmashed reset: reset
info for their furthest point and RacingResetPlayerToLastReset. Resets
sent for smashes keep the planes in step as well.

Fixes issue 764.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:05 -05:00
Aaron Kimbrell
c759bd1a40 fix(vendor): keep 27 buyback items and drop the oldest
The buyback inventory grew by 9 slots whenever it was nearly full, so
the vendor's buyback page kept resizing. Live kept 27 items: a 2014
capture of 29 sales shows that on the 28th, the server sent
RemoveItemFromInventory (buyback inventory) for the first item sold,
then added the new one.

The buyback inventory now keeps its size. Before a sale needs a new
buyback slot and the inventory holds 27 or more items, the oldest items
(lowest object ID: each sale gives a new, higher ID) are removed. Sales
that fit on an existing buyback stack remove nothing. The removal is not
counted again by the economy ledger, which counted the items as gone
when they were sold.

Fixes issue 1129.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:05 -05:00
Aaron Kimbrell
02108055e7 feat(inventory): enforce DeletionRestrictions when deleting items
Deleting an item now follows its ItemComponent delResIndex row in the
DeletionRestrictions table, the way the client's shared inventory code
decides it (LWOInventoryComponent_Common::CanRemoveFromInventory @
0x00ce0d20, CheckDeletionRestrictionIndex @ 0x00c94c20):
- missing, unrestricted, unknown-type or empty rows allow it;
- LOTS_INCLUDED: another item of any listed LOT must remain;
- LOTS_EXCLUDED: other items of every listed LOT must remain;
- ANY_RESTRICTION / ALL_RESTRICTIONS: any / all listed rows allow it;
- ZONE: only in the listed maps; ALWAYS_RESTRICTED: never.
Operators (GM level 9) may delete anything, as in the client. A refused
delete is logged and the item stays.

ItemComponent.minNumRequired is not used: the client never reads it, so
its meaning can't be verified.

Issue 960: the rocket (6416, row 8) and the classic rocket parts (rows 1-3)
have rows that keep at least one rocket or part, so the last rocket can
no longer be deleted and strand the player.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:04 -05:00
Aaron Kimbrell
4399daad4c fix(login): select the last played character in a stable list
The character list selected index 0 and was ordered by last login, so
the characters swapped places on the selection screen after each
session. Live kept them in the order they were made and selected the
one with the latest last login (a new character counts as logged in
when it is made): 2014 captures show e.g. a new fourth character
appended and selected (index 3), and after the next login the list in
the same order with index 0 for the character played last.

Characters are now sorted by ID (creation order) and the one with the
latest last login is selected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:04 -05:00
Aaron Kimbrell
d43cdef104 feat(mail): notify online recipients of new mail on any world
Mail sent to someone who is not in the sender's world (system mail with
no address, and all player-to-player mail) now reaches them: the world
sends a MailNotify (Chat::MAIL, unused until now) to the chat server,
which passes it to the world the receiver is in, and that world sends
the client its unread count with a NewMail NotificationResponse.
Receivers in the same world are told directly. Player-to-player mail
did not notify the receiver at all before.

Replaces the TODO in Mail::SendMail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:04 -05:00