DownloadPropertyData sent the same zonePosition (548, 406, 178) and a
maxBuildHeight of 128 for every property. Both now come from the
property's PropertyTemplate row (zoneX/zoneY/zoneZ, maxBuildHeight), the
same row the template ID, vendor map and spawn name already come from.
A map with no row still sends the old values.
Live rows: Avant Gardens small (1150) keeps 548/406/178 and 128; Avant
Gardens medium (1151) is 428/413/82 with a build height of 256; the
Nimbus Station (1250, 1251), Gnarled Forest (1350) and Forbidden Valley
(1450) properties get their own positions with 128.
Check in game: on the medium Avant Gardens property (1151) models can be
placed higher than before (up to 256), and on the small one (1150) the
limit is unchanged; on the other worlds' properties the property screens
(plaque, news/claim UI) still show and building works as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent the launchpad's FireEventClientSide("RocketEquipped") to every
client in the zone: 1,196 of 1,265 captured were other players'
launches. On each client it starts that player's launch
(LWORocketLaunchComponentCommon::msgFireEventClientSide sends
BeginLaunch to the sender), so others see the rocket take off; DLU sent
it only to the launcher. param1 is the property clone for property
launches (7 of 7 equal the clone in the following TransferToZone) and
param2 the picked world of a LUP entrance (0-3, one pad only).
The capture notes read the RocketEquipped and ChangeObjectWorldState
seen before LEVEL_LOAD_COMPLETE as the arriving player's own rocket:
all 21 are other players launching (their sender is never the loading
player), so arrival itself is left as it is.
Check: with two players at a launchpad, launch one: the other sees the
rocket launch animation; launch to a property and to a LUP world and
land in the right place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent, right before TRANSFER_TO_WORLD on every rocket launch (131
captured): NotifyClientFlagChange(32, true), TransferToZone
(check_transfer_allowed, the target map, the landing spawn point, the
clone only for properties, no position or rotation),
TransferToZoneCheckedIM (no queue) and NotifyClientFlagChange(32,
false). DLU went straight to TRANSFER_TO_WORLD.
In the client TransferToZone runs the civilian INVALIDMAPTRANSFERLIST
check, and TransferToZoneCheckedIM pauses the player's controls, starts
the target zone's loading screen and leaves the gameplay state
(LWOCharacterComponent, cases TransferToZone and
TransferToZoneCheckedIM). They are sent from the transfer callback, so
a failed zone request never leaves the client paused. Instance exits
through a message box used TransferToLastNonInstance in live instead
and are not changed here.
Check: launch rockets to Nimbus Station, Gnarled Forest and your
property: controls lock and the destination's loading screen shows as
the rocket leaves, and you land at the usual spawn point.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live pushed a mail NotificationResponse (NewMail, the unread count)
right after the respawn checkpoint in loads where the player had
unread mail, without the client asking (e.g. a new character's first
load: ServerDoneLoadingAllObjects, PlayerReachedRespawnCheckpoint,
NotifyMissionTask, MAIL). DLU had that call commented out, so the
mailbox icon only lit up once the client asked or new mail arrived.
It is sent only when there is unread mail (inferred: the 223 captured
loads without one are loads without unread mail; only 3 of 226 loads
carry it).
Check: send a character mail, log it out and back in without opening
the mailbox: the new-mail indicator shows right after loading; a
character with no unread mail shows none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent PlayerReachedRespawnCheckpoint right after
ServerDoneLoadingAllObjects on 143 of 157 captured loads, with a
rotation: the spawn point the player arrived at, or a checkpoint they
had reached before. DLU sent it before constructing the zone's objects,
only when a checkpoint was saved for the zone, and always unrotated.
It now goes right after ServerDoneLoadingAllObjects on every load: the
saved checkpoint when there is one (DLU saves no rotation for it, so
that one stays unrotated), otherwise the position and facing the
player loaded in at. Inferred: that the loaded-in spot matches live's
spawn point choice (live's is the spawn point object, a few tenths of
a unit from the player's start).
Check: log in fresh to a zone, die before touching any checkpoint and
respawn: you come back where you arrived, facing the same way; reach
a checkpoint, change zones and come back, die: you respawn at that
checkpoint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live answered PlayerLoaded with PlayerReady to the player and then a
second PlayerReady to the zone control object (0x3FFFFFFFFFFE), both
to the loading client: 235 of 237 captured loads carry exactly that
pair. DLU sent only the first. The zone control object's client-side
zone scripts are what can react to it (inferred: the client's
component handlers only act on the player's own PlayerReady).
Check: load into Avant Gardens, Nimbus Station, a property and an
instance (a survival or dragon battle): the zone plays its intro,
music and UI as before and nothing is stuck waiting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent the player ModifyPlayerZoneStatistic(set false, value 1, the
current map) for "QuickBuildsCompleted" (227) after a quickbuild's
EnableRebuild and for "AchievementsCompleted" (186) at the end of an
achievement's completion. DLU counted both server side (they are saved
and shown at the next load) but never told the client, so the zone
summary only caught up after a zone change. The client counts
enemies, coins and bricks itself and sends those to the server.
Check: complete a quickbuild and an achievement, then open the zone
summary (passport / map zone stats) without changing zones: both
counts went up by one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent SetStunned to every dead object with a combat AI in the
packet right after its Die: 7,098 of 7,105 such deaths (7,424 decoded
stuns, all the same: no originator, push, can't attack, move or turn,
ignore immunity). Objects without combat AI (smashables, 9,393 deaths)
never got one. DLU sent none, so a dying enemy could keep turning or
start an attack while its death plays.
Check: kill enemies mid-attack and while they chase you; they stop
moving, turning and attacking the moment they die, and the death and
loot still play normally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent duration 0.0 in all 744 successful EnableRebuild (cancels
carry the time spent building, which DLU already matches); DLU sent
the reset time.
Tests get EntityManager::_addEntity/_removeEntity to make an entity
built outside CreateEntity findable, and helpers that read game
messages back out of captured packets.
Check: finish a quickbuild; the build completes, the celebrate
animation plays and the built object stays up for its usual time
before resetting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live DropClientLoot (12,501 decoded): use_position is set only when a
player is the source (1,432, all activity rewards such as the dragon,
Frakjaw and BONS chests); drops from enemies, smashables and chests
leave it clear, so the client spawns the loot at the source object as
it sees it and falls back to spawn_position only if that object is
gone (LWODestroyableComponent::GetLootSpawnPos). Coins land where they
spawn (final = spawn in all 3,158 coin drops); items land 10 units
away (median 9.998, uniform direction) at spawn height. DLU always set
use_position and scattered everything 4.2 units away.
Inferred: the 10-unit rule for player-sourced items (live spread there
ranges 0-58 units, source unknown).
Check: kill enemies and smash crates: coins pop out of the object,
items fly about 10 units away; open an activity chest (e.g. after a
dragon or Frakjaw) and the loot still appears around you.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live filled SetCurrency's source fields per source type (16,816 decoded
live SetCurrency): pickups carry the position the client picked the
coins up at (the one in its PickupCurrency) and no source; mission,
achievement and activity rewards name the player (object and LOT 1);
vendor buys, sells and buybacks name the vendor and its LOT; trades
carry the trade ID; death, mail and everything else name nothing. No
live SetCurrency sets loot_type. DLU wrote a present source LOT of 0,
no object and a zero position for all of them.
The position is the one the client uses: for pickups it projects it to
the screen as the start of the coin counter animation
(LWOCharacterComponent::SetCurrency), so it started from the world
origin before. PickupCurrency now reads that position (the client
always sends it). The trade ID is an 8-byte object ID in the client;
DLU declared it 4 bytes, which only never broke because it was never
set.
Check: pick up coins and watch the coin counter animate from where the
coins were; buy and sell at a vendor, finish a mission and trade coins
with another player, and the coin total updates each time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live filled Die's direction_relative_angle_xz, _angle_y and _force on
11,606 of 19,428 deaths. The values are the dir_angle_xz, dir_angle_y
(degrees, sent as radians) and dir_force parameters of the BasicAttack
that dealt the killing blow: per killing skill the triple is constant
(skill 1210: -25 degrees, force 12 on 540 of 549 kills; skill 10:
force 7 on 1,830 of 1,835), and deaths with no killer (5,948) never
have one. BasicAttack is the only behavior with these parameters apart
from one Grab. The client passes them to PerformSmashableDeath, which
throws the smashable's pieces that way; DLU sent 0.
The radians are rounded the way live did (-25 is 0xbedf66f3, 20 is
0x3eb2b8c3). Not covered: use_caster_velocity (one behavior, racing).
Check: smash crates and enemies with different weapons; the pieces fly
off to the side/up per weapon instead of straight out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent client_death=false on every Die, players included (19,428 in
the live captures, 452 of them player deaths); DLU sent true for
players. The client only uses the flag to match a death it already
predicted locally (LWODestroyableComponent::msgDie): with it set, a
Die that arrives while a post-death respawn is pending is dropped.
Check: die to an enemy, to falling and to a quickbuild trap; the death
animation, coin loss and respawn prompt all still appear, and other
players see you die.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sends used_mouse false, no caster latency, no clicked position and
a zero originator rotation (present, all four components 0) in every
echo: 78,841 of 78,841 in the live captures, player and server casts
alike. DLU forwarded the casting client's values and wrote the caster's
facing for server casts. The client copies the rotation into its local
StartSkill (LWOSkillComponent::msgEchoStartSkill), which is why a real
rotation made power-up pickups jitter; the per-call override that
worked around that is no longer needed and is removed.
Check: with two players, attack, use a thrown or ranged item and pick
up power-ups; the other player sees the same animations and the
caster no longer snaps or jitters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EventGating (eventName, date_start, date_end; 8 rows) is only in the
CDClient for the server: the 1.10.64 client has no string for it. The
dates are Unix times (UTC), inclusive: pirateDay 2011-09-19 00:00 to
2011-09-20 23:59:59 (Talk Like a Pirate Day), buildNexusTower 2010-03-09
to 2011-03-14, buildNexusTowerEnd, frostburgh2010_notused and test rows.
The live Lua asked for an event with GetHolidayEvent{eventToCheck =
name}.isValid; the only callers are the Crux Prime random spawners
(L_BASE_RANDOM_SPAWNER.lua, L_AM_ZONE_SERVER.lua), whose only event is
pirateDay. DLU's BaseRandomServer::CheckEvents was a TODO.
- CDEventGatingTable loads the table; HolidayEvents::IsActive(name) is
true while the dates include now, or when event_1..event_8 names the
event (every live date is in 2010/2011, so this is how a server turns
one on; the same settings already switch gatingOnFeature objects).
- BaseRandomServer::CheckEvents: during pirateDay the str and zip areas
use the event loads from the Lua (80%: 5 pirates on each of type1-3,
20%: 5 admirals each; multipliers secA 1, secB 1, secC 1.2 for str).
Inferred: every spawner's Lua put the whole {str, zip} table in its
zones, which its getRandomLoad walks with ipairs and so finds no load;
DLU uses each table for the area it names, as L_AM_ZONE_SERVER.lua's
layout intends.
- sharedconfig.ini says event_N also takes EventGating names.
Check in game: with event_2=pirateDay in sharedconfig.ini, the Crux Prime
areas filled by the str and zip random spawners (spawner networks
em_str_* and em_zip_*) spawn only Maelstrom pirates and admirals; the
other two areas and a server without the setting spawn the usual mix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CollectibleComponent (79 rows: id, requirement_mission) is only in the
CDClient for the server: the 1.10.64 client has no string for it. DLU
never read it.
What the column holds (1.10.64 CDClient, joined with ComponentsRegistry,
Missions and MissionTasks): for 63 rows it is the mission the collectible
belongs to. Mostly that is the achievement whose collection task
(taskType 3) targets the collectible's LOT (the flags, imagination bricks,
Johnny Thunder collectibles). For a few it is a mission to accept from an
NPC that does not collect the item itself: the four Ninjago dragon relics
(16483-16485) are collected by the hidden achievements 2064-2067 but
their requirement is 2040 (accepted from LOT 13789, "complete 2064-2067").
Other values: -1 and 66666666 (no such mission) on test rows.
So a collectible whose requirement_mission is a mission to accept
(Missions.isMission) now only counts (HasBeenCollected progresses
nothing) while the player has that mission accepted and not handed in
(ACTIVE, READY_TO_COMPLETE or their repeat states). Achievements, missing
missions and rows without one are unchanged. Without this, a player who
had not accepted 2040 could collect the relics early and have 2040
complete as soon as it was accepted. The gate itself is inferred from the
data: the captures show collections but not the live server's check.
The collectible's object report shows the requirement mission.
Check in game: in Ninjago Monastery, before accepting the dragon relic
mission (2040), touch a dragon relic: it does not count; accept 2040 and
collect them: each counts and 2040 completes after the fourth. Flags,
imagination bricks and Johnny Thunder collectibles still count as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client reads <pet a> as its active pet's database ID with
GetInt64Attribute (LWOPetControlComponent::LoadFromSaveData 0x00c18120);
DLU never wrote it. Live wrote a="0" on every character (226 live
charxmls, 45 with pets): the charxml is only read when the player loads,
before any pet is out, and a pet summoned afterwards registers itself
(RegisterPetDBID). So a is written as 0.
The pet taming type p@t stays 0 as DLU already wrote it: it is an int
(IntAttribute), the client sets it to 0 for every pet it is given
(AddPetToPlayer in LWOPetControlComponent::HandleMessage 0x00d0fe00,
which ignores the message's elemental type) and every live pet had t="0".
Commented so it isn't "fixed" later.
Check in game: with a tamed pet, summon it, change zones and log out and
back in: the pet menu and pet naming work as before and no pet is shown
as out until you summon one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client reads the cooldowns still running from the charxml on load
(LWOSkillComponent::LoadFromSaveData 0x00c2a9c0): <skil sc="..."> split
on ';' and ':' (the delimiter string at 0x015b1714 is L";:") into a
cooldown group and the seconds left, each put in its cooldown map under
{hasCooldownGroup = 1, group}, the key it checks when the player casts a
skill of that group (0x00c11400). Live wrote <skil/> with nothing running
and e.g. <skil sc="17:15.6958;"/> otherwise (226 live charxmls; groups
8, 17 and 78 seen, the times below the groups' SkillBehavior cooldowns).
The claude-client-re-docs page described sc as "id;time" pairs; the
live saves and the delimiters show "group:time;".
DLU wrote no <skil>, so relogging or changing zones cleared every
cooldown (e.g. the 90 s imagination and faction skills).
- A player's successful cast (CastPlayerSkill) starts its
SkillBehavior.cooldowngroup's cooldown (groups 0 and up, cooldown > 0),
counted down in Update.
- Saved as live wrote it; loaded back when the player's SkillComponent
is created, so a cooldown survives several zone changes. Old saves
without <skil> load with none running.
Check in game: use a skill with a long cooldown (e.g. a faction kit's
special skill, a consumable with a cooldown), change zones or log out and
back in right away: its cooldown is still shown and counts down from
where it was (minus the loading time on the server).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOCharacterComponent sends ModifyGhostingDistance (1485: an optional
fDistanceScalar, default 1) on load. All 239 live packets were
the single byte 00 (default scale), so there is nothing for the server to
change; it was logged as an unknown game message. It is now read and
ignored.
Check in game: nothing changes; the world server log no longer has
"Received Unknown GM ... 1485" on each load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>