Commit Graph

100 Commits

Author SHA1 Message Date
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
7d28405559 test(messages): DownloadPropertyData against a live capture packet
Adds FromLiveCapture, which reads a whole captured game message packet
into a struct and requires the struct to write the same bytes back, and
checks DownloadPropertyData (716) with an unowned property packet from
the 2011/2012 live captures.

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
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
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
dce2037d9d feat: worlds starting up on the dashboard; the prestarted worlds are a setting
The dashboard shows worlds master has launched but that aren't connected
yet (Starting) and those shutting down, on the main page's world list
and in /api/worlds (state: starting|stopping), without a shutdown
button. Master's server list now carries each world's state (appended
after the UGC fields, one byte per world) and master pushes the list to
the dashboard whenever a world is launched, becomes ready, is told to
shut down or goes away, instead of the dashboard only seeing it on its
30 second poll. Starting worlds are kept apart from the running ones,
so counts, events and shutdown requests still only see running worlds.

prestart_worlds (masterconfig.ini, zone ids) lists the worlds master
starts when prestart_servers is on, instead of the hardcoded character
select (0) and Venture Explorer (1000), which stay the default when it
is missing or empty. It is on the Settings page as a zone list (restart
only), shown when prestart_servers is on.

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
69803448bb feat(master): report the UGC server in the server list and wait for it on shutdown
The server list now carries whether master starts the UGC server, whether it
is connected and the pid it was started as. Settings reloads reach it like
auth and chat, and shutdown waits for it. The UGC server sends its totals and
storage with its traffic reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:05 -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
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
Aaron Kimbrell
7fb1e404f0 fix(wire): write SetStatusImmunity flags in the client's order
WIRE FIX. The 1.10.64 client writes and reads the immunity flags in
alphabetical order after the u32 state (GameMessage::SetStatusImmunity::
Serialize @ 0x00d8f140; the field offsets are named by the Flash export
at 0x00d8f410): BasicAttack, DOT, ImaginationGain, ImaginationLoss,
Interrupt, Knockback, PullToPoint, QuickbuildInterrupt, Speed. DLU wrote
them in declaration order, so e.g. a knockback immunity reached the
client as an imagination-gain immunity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:04 -05:00
Aaron Kimbrell
e3a75a689e fix(wire): send NotifyNotEnoughInvSpace with its own message ID
WIRE FIX. DLU sent NotifyNotEnoughInvSpace with the ID of
VehicleNotifyFinishedRace (1396). The 1.10.64 client registers it as
NOTIFY_NOT_ENOUGH_INV_SPACE (1516, 0x00545c90) and reads freeSlotsNeeded
followed by the optional inventoryType (0x00d8b850), which is what the
payload already was. Only the message ID changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:04 -05:00
Aaron Kimbrell
13ad679df1 feat(net): count every server's packets and send a traffic report every 5 seconds
TrafficStats keeps packets and bytes in and out per second and the busiest
packet and game message types. dServer counts at its send and receive calls
and adds RakNet's connection statistics (datagrams, resends, ping); the report
goes to master as SERVER_TRAFFIC (appended), which passes it to the dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:01 -05:00
Aaron Kimbrell
fec4159ea5 refactor: modular build items and root part come from ModularBuildComponent
ModularBuildFinish hardcoded the item a finished build becomes (6416 for 3
parts, 8092 for 7) and the car chassis part (8129) that the every-part-
swapped check skips. They now come from ModularBuildComponent: createdLOT,
<numberOfParts> and the <ExamplePartLOT> of the <rootPart> module (new
CDModularBuildComponentTable). Same results with the 1.10.64 cdclient;
tests cover the xml parsing and the lookup.

Refs #691

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:59 -05:00
Aaron Kimbrell
6ce261c58c fix: brick by brick and model placement work the way the client expects
Mapped from the 1.10.64 client and live captures (docs/BuildWorkflow.md).

Model placement (PropertyManagementComponent):
- A brick built model placed from the inventory spawned at the world origin
  with no rotation and without PlaceModelResponse/PreCreate, and was saved
  to properties_contents with ugc_id 0; it is now placed where the client
  put it, keeps its UGID and blueprint and is saved with them.
- Picking up, putting away and taking apart a brick built model gave it to
  MODELS_IN_BBB and then deleted it; every way off the property now puts it
  in MODELS (carried when picked up), as live, with its blueprint config.
  Taking a premade model apart no longer deletes it either.
- Placing and removing a model saves the property at once, so a crash or a
  disconnect before PropertyEditorEnd does not lose it.
- DoneArrangingWithItem only answers when something new is picked (not when
  leaving), with the subject as build area; SetBuildModeConfirmed's
  warnVisitors matches live.

Brick by brick (BrickByBrick):
- BBBLoadItemRequest moves the model to MODELS_IN_BBB keeping its id and
  fails cleanly when the player has no such model.
- MoveInventoryBatch moves bricks between BRICKS and BRICKS_IN_BBB (it was
  not handled, so the client and server disagreed until a relog).
- BBBSaveRequest uses up the opened models, places the new ones through the
  property, returns the bricks (or uses them with bbb_consume_bricks=1),
  clears the autosave and sends RequeryPropertyModels. Every save makes new
  ugc rows (is_optimized 0, so the UGC server processes them).
- Quick save: SetBBBAutosave is stored per character (bbb_autosave).
- UnUseBBBModel puts a model back on the property where it was when it came
  from there, otherwise back in MODELS.
- Leaving brick mode without a save, a disconnect or a crash: the autosave
  is rebuilt into models (RebuildBBBAutosaveMsg) or the opened models go
  back to MODELS. MODELS_IN_BBB is saved with the character now and loads
  into MODELS, BRICKS_IN_BBB into BRICKS.

Fixes #1632
Fixes #159
Fixes #1565

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:59 -05:00
Aaron Kimbrell
b9d6b739d5 fix(wire): PlaceModelResponse writes the model's rotation
The client's PlaceModelResponse::Deserialize (0x00dc0170) reads the rotation
as an optional w, x, y, z quaternion, but DLU wrote the 4-byte response
after the rotation flag. With any rotation other than identity the client
ran out of data. A live capture of a model turned 90 degrees shows the
server echoing the rotation the client placed it with. Bytes only change
when the rotation is not identity; a golden test pins the new layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:58 -05:00
Aaron Kimbrell
d4a1a993ba refactor: power-up statistics come from the power-up's pickup skill
TrackLOTCollection hardcoded the 15 life/armor/imagination power-up LOTs. A power-up
(Objects.type "Powerup") now counts towards the statistic of what its pickup skill
restores: a Heal, RepairArmor or Imagination behavior in the skill's behavior tree.
Gives the same result for the 15 LOTs; "HoT Powerup" (8208, heal over time) now also
counts as a life power-up.

Refs #691

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:58 -05:00
Aaron Kimbrell
e44eaf1862 refactor: item set passive abilities come from the CDClient and item scripts
The server hardcoded each item set's passives by set ID. They now come from data:

- On-kill bonuses (Paradox imagination, Sentinel armor repair, Bat Lord heal) are the
  set's DarkInspiration skills in ItemSetSkills. The client turns those into a status
  effect that casts the behavior's action when the wearer kills something of a faction
  in faction_list; the server now runs the same behavior on the smashed enemy, so the
  amounts, faction check and extra effects (e.g. rank 3 Sorcerer team imagination) follow
  the data. A behavior repeated in a higher tier does not stack.
- Low imagination / low armor skills are what the live equipmenttriggers item scripts
  do. Items link to those scripts through their ScriptComponent; the script vars (skill,
  items required, set, cooldown) only exist in the scripts, so they are mirrored in a
  small table keyed by script name. The Sentinel scripts have no cooldown (was 11s).
- Knockback immunity while quickbuilding comes from the Immunity behavior's
  immune_quickbuild_interrupts (the 5 item Assembly rank 2/3 set skill) instead of a
  set ID list; ImmunityBehavior now pops its immunities when an equip skill is uncast.

Removes eItemSetPassiveAbilityID and InventoryComponent::HasAnyPassive (unused now).

Refs #691

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:58 -05:00
Aaron Kimbrell
542f0a89f0 refactor: remove the packet macros, WriteHeader and PacketUtils
Nothing in the server writes or reads a packet by hand any more, so the
helpers for doing so go:
- CBITSTREAM, CMSGHEADER, CINSTREAM, CINSTREAM_SKIP_HEADER, SEND_PACKET,
  SEND_PACKET_BROADCAST and HEADER_SIZE leave dCommonVars.h;
- the free BitStreamUtils::WriteHeader (LUBitStream::WriteHeader writes the
  same bytes) and the unused PacketUtils::SavePacket are deleted.
The last raw reads are replaced: WorldServer builds its input stream
directly, the master packet logs read the header with
LUBitStream::ReadHeader instead of peeking at packet->data[1] and [3], and
MessageInspector reads a sent game message's header with the new
NetGameMsg::ReadPacketHeader (the counterpart of WritePacket) instead of
memcmp/memcpy. packet->data[0] is still compared with RakNet's own
connection IDs.

The frozen oracles keep using the macros verbatim through the test-only
tests/dGameTests/LegacyPacketMacros.h; the HeaderSkip tests, which only
tested CINSTREAM_SKIP_HEADER, are removed.

docs/PacketArchitecture.md: "where we are" now describes the final state
and what still touches raw bytes (RakNet IDs, replica headers, behavior bit
streams), and a new section collects the known wire discrepancies found
during the conversion, with client addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:56 -05:00
Aaron Kimbrell
b2a1e9e0b8 refactor: remaining game messages as structs, switch removed
Every game message still written or read by hand is now a NetGameMsg with
Serialize and Deserialize, in per-domain files:
- MovementMessages: teleport, platforms (resync and its request), orient to
  angle, node rotation lock, gravity scale, jetpack mode, control scheme,
  respawn checkpoint, rails (set/start, ready, cancel, arrived), mount
  inventory ID, dismount complete, possession ack, ghost reference override
  and position, camera cycling (eCameraTargetCyclingMode moves here).
- ZoneMessages: player loaded (the old PLAYER_LOADED case), ready for
  updates, player ready, restore to post load stats, server done loading,
  invalid zone transfer list, zone summary display/dismissed, level
  processing complete, object world state, and the localized announcement
  WorldMigration wrote by hand.
- PlayerMessages: chat mode, GM level, LEGO score, currency, reputation,
  GM invis, pickup currency, zone and player statistics, chat commands, bug
  reports, verify ack.
- ObjectMessages: fire event client/server side, notify client
  (zone) object, notify object, script network vars, failed preconditions,
  terminate interaction, set name, request use, request server object info.
- QuickBuildMessages: notify state, enable, cancel.
- ActivityMessages gains match response/update/request, leaderboard request
  and data, shooting gallery score/rotation/fire, activity state change.
- MissionMessages gains MissionDialogueCancelled (a no-op, as before).
The wire structs left in GameMessages.h move to their domains (tooltip and
emote to Effects, loot and item use to Inventory, model build to Building,
behavior sound to Property, skill sets to Skill) and gain the missing
direction. The dismount logic moves to PossessorComponent::OnDismountComplete.

Every inbound message is registered in the GameMessageHandler map; the
switch is gone, and GameMessages.cpp only holds the GameMsg/NetGameMsg base
code. Call sites build the structs (NotifyClientObject, TerminateInteraction,
Teleport, PlatformResync, FireEventClientSide, NotifyObject and
NotifyClientZoneObject get convenience constructors like PlayFXEffect).
Dead senders are dropped: SendSetShootingGalleryParams (no callers, field
order was a guess), SendTeamPickupItem (the struct already existed),
SendRequestActivitySummaryLeaderboardData (the struct covers it).

Verified with RemainingMessagesTests: every old Send* function is frozen
verbatim in Legacy/RemainingMessagesLegacy.h and compared byte for byte
(same bits, destination and broadcast flag) over grids of inputs; every old
Handle* read sequence is frozen as a Read* oracle and compared with the
struct's Deserialize; round trips, truncation and a golden packet.
PlayerLoaded (0x00dc36f0), SetGMLevel (0x00dd6230), MissionDialogueCancelled
(0x00d9cc10) and LocalizedAnnouncementServerToSingleClient (0x00f23c50)
were checked against the client. Behaviour notes: an inbound message that
fails to deserialize is dropped, so ParseChatMessage over MAX_MESSAGE_LENGTH
is dropped instead of truncated, and PLAYER_LOADED / READY_FOR_UPDATES /
MISSION_DIALOGUE_CANCELLED now read their (unused) client fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:56 -05:00
Aaron Kimbrell
caebff4dca fix: give the destroyable and buff components their real component types
eReplicaComponentType had the destroyable component's registry type (7) named
BUFF, the real buff component (98) as BUFF_REAL, and a made-up DESTROYABLE =
1000 that DestroyableComponent was stored under. Now DESTROYABLE = 7 and
BUFF = 98, as in ComponentsRegistry and the client.

Undone with it:
- Destructible stats came from whichever of the "buff" (7), quick build and
  collectible registry ids was set, so the few objects without a type 7 entry
  read a DestructibleComponent row with an unrelated id (the NJ dragon relics
  16482-16485 via their collectible id, 125 quick build LOTs when placed with
  is_smashable). The type 7 entry is used now; objects without one keep the
  defaults (is_smashable objects: 1 health, smashable, factions -1 and 6;
  collectibles: an empty destroyable). The client does the same
  (LWODestroyableComponent::AllocateComponents / DoObjectComponentLoad).
- DestroyableComponent::Reinitialize, an unused copy of that pick order.
- WriteComponents' destroyableSerialized flags: the components are written from
  a list in the client's order, and where the destroyable goes (its own place
  after the buff, right before a quick build that has no registry entry for it,
  or after the render component) is one function.
- The dashboard's registry 7 -> DESTROYABLE mapping; the destroyable type also
  has a name there now (1000 was outside magic_enum's range).

Component types are not stored or sent as enum numbers anywhere besides the
CDClient's own values, which now match. migrations/cdserver/4 is unrelated (it
restores LOT 12916's registry rows that migration 0 overwrote) and stays.

Verified: dGameTests ReplicaComponentOrderTests serialize players, enemies,
smashables, quick builds and collectibles with and without a registry entry,
NPCs, pets, vehicles, models and an entity with every listed component with
the new code and a frozen copy of the old WriteComponents, and expect the same
bits for construction and serialization (a deliberately wrong destroyable place
fails them). Full ctest passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:55 -05:00
Aaron Kimbrell
27c562e242 feat: switch trigger physics volumes on and off at runtime
The ActivatePhysics trigger command was a TODO. The client activates or
deactivates the object's physics component
(LWOPhysicsSystemComponent::msgActivatePhysics, 1.10.64 0x00ccf970);
the server now does the same to phantom physics: switching it off takes
the volume out of the physics world (dpWorld::DetachEntity, without
deleting it) and makes whatever was inside leave, switching it on adds
it back and whatever is inside enters on the next step. This lets
trigger driven volumes like the monument lasers turn on and off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:54 -05:00
Aaron Kimbrell
5d3c2e6f9a fix: filter physics volumes like the client's collision groups
Volumes on the server touched far more than in the client:

- An enemy's body in the physics world was a sphere the size of its aggro
  radius, so trigger and damage volumes caught enemies from far away
  (Cavalry Hill enemies taking damage on spawn). It is now the enemy's
  own radius and collision group from its physics component.
- Trigger volumes ignored their collision group and caught everything.
  dpEntity now filters with the client's collision filter
  (PeCollisionFilter, 1.10.64 0x00fb6940, group table from 0x00fcf9a0):
  POI walls ignore enemies, threat clearing walls ignore players, and
  so on. The aggro sensor keeps seeing only players.
- Rotated boxes were tested as the axis aligned box around them, which
  for a turned wall covers a big square (the AG survival boundary).
  Sphere and point tests now use the box's own axes.

Proximity monitors take an optional collision group like live's
SetProximityRadius; the AM shield generators use live's (10 finds
enemies, 1 finds players).

Fixes #1127
Refs #1971

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:54 -05:00
Aaron Kimbrell
2d76c81bba feat: server side knockback for AI moved objects
The client only simulates knockbacks on the character it controls
(LWOControllablePhysComponent::msgKnockback); enemies and NPCs are drawn
where the server puts them, so a knockback on them did nothing and
scripts stunned them instead.

MovementAIComponent::Knockback now flies the object the way the client
flies its character: a vector longer than 5 lifts it 0.5, throws it with
that velocity under WorldConfig gravity (times its gravity scale) until
it lands on the navmesh, no sooner than 250ms later; a shorter one moves
it by the vector. Walls taller than a step stop sideways motion, the
landing is put back onto the navmesh, and pathing and the combat AI wait
until it lands (destinations set mid air are walked to afterwards).
Position and velocity go out in the normal serialization every tick.

KnockbackBehavior builds the vector like the client's Cast (strength
capped at 300, angle as elevation, relative, caster and ignore_self) and
knocks back server moved targets that aren't immune, both when the
server casts and when a client's skill hits. The blocked bit it writes
now also answers for the target's knockback immunity, so a player whose
client hasn't caught up with a Personal Fortress isn't knocked out of it.

The AM shield generators knock enemies back like live instead of
stunning them.

Fixes #257
Refs #185

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:54 -05:00
Aaron Kimbrell
f7f5d304d0 refactor: death, stun, buff and knockback game messages as structs
Converts the combat messages to NetGameMsgs in CombatMessages.{h,cpp}:
Die, Resurrect, SetResurrectRestoreValues, SetPlayerAllowedRespawn
(SendToClient, as before), Knockback, SetStunned, SetStunImmunity,
SetStatusImmunity, AddBuff, RemoveBuff, AddRunSpeedModifier,
RemoveRunSpeedModifier and DeactivateBubbleBuffFromServer, and the
received RequestDie, RequestSmashPlayer, RequestResurrect, Resurrect,
ActivateBubbleBuff and DeactivateBubbleBuff. Smash and UnSmash move
here from GameMessages.h (Smash's ghostCapacity is ghostOpacity, the
client's name) and gain a Deserialize.

The callers (DestroyableComponent, BuffComponent,
ControllablePhysicsComponent, QuickBuildComponent, RacingControl,
RailActivator and the scripts) build the structs. SendResurrect's
respawn timer moves to DestroyableComponent::Resurrect. The received
messages are registered in GameMessageHandler's map and their switch
cases are deleted, along with the unused HandleRequestDie overload and
the Send* functions nothing called (SendSmash, SendUnSmash, the run
speed modifiers, SendActivateBubbleBuffFromServer). Handler logic is
unchanged.

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions over an input grid
(every optional field, bool patterns, empty and long strings), received
messages compared with the old handlers' read sequences and truncated
payloads rejected, hand computed golden bytes, round trips, and a
deliberate width mutation made the tests fail.

Known difference from the client, not changed here: DLU writes
SetStatusImmunity's nine flags in its own order; the client reads them
alphabetically (GameMessage::SetStatusImmunity::Serialize @ 00d8f140).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:53 -05:00
Aaron Kimbrell
27de0cf268 refactor: skill and projectile game messages as structs
Converts the skill messages to NetGameMsgs in SkillMessages.{h,cpp}:
AddSkill and RemoveSkill (SendToClient, as before), EchoStartSkill,
EchoSyncSkill and DoClientProjectileImpact, and the received
SelectSkill, StartSkill, SyncSkill and RequestServerProjectileImpact.
The one-off StartSkill, EchoStartSkill, SyncSkill, EchoSyncSkill,
RequestServerProjectileImpact and DoClientProjectileImpact classes are
deleted; SkillComponent, BehaviorContext and InventoryComponent build
the structs, and the dashboard's message decoder reads them.

The received messages are registered in GameMessageHandler's map and
their switch cases are deleted; the handlers call SkillComponent as
before. The echoes still go to every client except the caster, through
the new NetGameMsg::BroadcastExcept. SelectSkill still accepts any
payload, since the old case read nothing. Handler logic is unchanged
(the SyncSkill case's unused hex dump of the payload is dropped).

No wire change. Verified byte for byte against frozen verbatim copies
of the old classes and functions over an input grid (every optional
field set and unset, empty, short and long behavior streams), received
messages compared with the old classes' read sequences and truncated
payloads rejected, the echo's broadcast-except destination compared
with the old send, hand computed golden bytes, round trips, and a
deliberate mutation made the tests fail. Messages that fail to
deserialize are now dropped instead of being handled half read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:53 -05:00
Aaron Kimbrell
2711c69e3e refactor: vendor, donation and trade game messages as structs
Converts the vendor, donation vendor and trade game messages to
NetGameMsgs in VendorMessages.{h,cpp} and TradeMessages.{h,cpp}:
VendorOpenWindow and VendorTransactionResult (SendToClient: the old
functions never broadcast), VendorStatusUpdate, ServerTradeInvite,
ServerTradeInitialReply, ServerTradeFinalReply, ServerTradeAccept,
ServerTradeCancel and ServerTradeUpdate, and the received
RequestVendorStatusUpdate, BuyFromVendor, SellToVendor,
BuybackFromVendor, AddDonationItem, RemoveDonationItem,
ConfirmDonationOnPlayer, CancelDonationOnPlayer, ClientTradeRequest,
ClientTradeCancel, ClientTradeAccept and ClientTradeUpdate. A trade
offer entry (the client's inventory item layout, optional fields and
config block included) is TradeItemEntry, shared by both trade updates.

Selling and buying back move into VendorComponent (SellToVendor,
BuybackFromVendor), adding and confirming donations into
DonationVendorComponent, and VendorComponent builds its own status
update and transaction results. Only the VENDOR component sends its
stock, as before. The received messages are registered in
GameMessageHandler's map, the switch cases and the old functions are
deleted. Handler logic is unchanged.

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions over an input grid
(to one client and broadcast), received messages compared with the old
handlers' read sequences (every optional field of a trade entry,
raw and compressed config blocks) and truncated payloads rejected, hand
computed golden bytes, round trips, and a deliberate width mutation made
the tests fail. ConfirmDonationOnPlayer still accepts a message without
its vendor ID, since the old handler read nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:53 -05:00
Aaron Kimbrell
b2403b09ea feat: contraband list with flagging and optional removal
Staff list contraband items on a new dashboard page (item search, reason, flag or flag and remove;
contraband_manage to edit, reports_view to see). World servers check every inventory when a
character loads and every item a player receives: each find is an economy flag of the new kind
Contraband, shown with the other flags and in the character's related data. Items marked for
removal are taken away, with a character snapshot kept first so they can be given back, an audit
entry and a mail or chat message telling the player why. Staff are skipped unless
contraband_ignore_staff is off. Worlds reload the list when it changes (RELOAD_CONTRABAND, added
at the end of ePlayerAction).

Fixes #1563

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:52 -05:00
Aaron Kimbrell
341ce89a6a feat: refuse stale character saves with a save generation
Every character gets a save generation in charxml. A world bumps it when it loads the character for
play, and every save from that world only goes through while the stored generation is still the
one it loaded or last saved (and moves it on). Dashboard edits, restores and maintenance writes bump
it too. A world that lost the character to another world (a disconnect noticed late, a zone
transfer, an instance migration) or to a dashboard edit can no longer overwrite the newer data: the
save is refused, logged and audited as stale_save_refused, the world stops saving that character
and a player still connected to it is disconnected with the save failure reason so they reload.

Fixes #639

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:51 -05:00
Aaron Kimbrell
20f70ac0de refactor: inventory and item game messages as structs
Converts AddItemToInventoryClientSync, SetInventorySize,
RemoveItemFromInventory, ConsumeClientItem, UseItemResult,
UseItemRequirementsResponse, ResponseMoveItemBetweenInventoryTypes,
NotifyNotEnoughInvSpace, UpdateInventoryUi, MarkInventoryItemAsActive and
MoveInventoryBatch (11 old Send functions) and the received EquipInventory,
UnEquipInventory, RemoveItemFromInventory, MoveItemInInventory,
MoveItemBetweenInventoryTypes, RequestMoveItemBetweenInventoryTypes,
PushEquippedItemsState, PopEquippedItemsState, ClientItemConsumed,
UseNonEquipmentItem, SetConsumableItem, UpdateInventoryGroup and
UpdateInventoryGroupContents (13 handlers) to NetGameMsgs in
InventoryMessages.{h,cpp}. The received messages are registered in
GameMessageHandler's map and handled by InventoryComponent (new On* methods
holding the old handler logic verbatim); the old functions and switch cases
are deleted. AddItemToInventoryClientSync::SetItem fills the fields that
come from the Item.

No wire change and no change in recipients. Kept as DLU has always sent them
and documented: NotifyNotEnoughInvSpace goes out with the message ID of
VehicleNotifyFinishedRace, and RemoveItemFromInventory always sets the flag
of the fields DLU fills. The old SendMoveInventoryBatch was never called
and wrote one flag bit fewer than the client reads (it had no moveSubkey);
the struct follows the client's layout (1.10.64,
LWOInventoryComponent_Common::msgMoveInventoryBatch at 00ce1310) and has no
oracle. UnEquipInventory still ignores the trailing replacementObjectID the
client sends, as before.

Verified byte for byte against a frozen verbatim copy of the old functions
over an input grid (AddItemToInventoryClientSync with real Items, extra info
and bind flags), to one client and broadcast; received messages compared
with the old handlers' read sequences and every truncated payload rejected;
hand computed golden bytes; round trips; a deliberate width mutation made
the tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:51 -05:00
Aaron Kimbrell
dae1925d1a refactor: effects, audio, animation and UI text game messages as structs
Converts PlayAnimation, PlayNDAudioEmitter, PlayEmbeddedEffectOnAllClientsNearObject,
PlayFXEffect, StopFXEffect, BroadcastTextToChatbox, Play2DAmbientSound,
Stop2DAmbientSound, UIMessageServerToSingleClient, UIMessageServerToAllClients,
StartCelebrationEffect, DisplayMessageBox, DisplayChatBubble, ChangeIdleFlags,
SetNameBillboardState, ShowBillboardInteractIcon, PlayCinematic, EndCinematic,
SlashCommandTextFeedback, PlayEmote and SetEmoteLockState (21 old Send
functions, 23 with overloads) and the received MessageBoxRespond,
ChoiceBoxRespond, CinematicUpdate and PlayEmote to NetGameMsgs in
EffectsMessages.{h,cpp}. The high traffic messages get a constructor for their
required fields. UI messages own their AMF arguments. Every caller is switched,
the received messages are registered in GameMessageHandler's map, and the old
functions and switch cases are deleted. Handlers are copied verbatim.

No wire change and no change in recipients: functions that always broadcast
whatever address they were given are sent with Send(UNASSIGNED_SYSTEM_ADDRESS),
functions that only did SEND_PACKET use SendToClient. Quirks are kept and
documented on the structs (PlayAnimation's UTF-8 sized name, the always written
null terminator in BroadcastTextToChatbox, StartCelebrationEffect always
writing celebrationID, SetNameBillboardState having no payload).

Verified byte for byte against a frozen verbatim copy of the old functions over
an input grid, to one client and broadcast; received messages compared with the
old handlers' read sequences and every truncated payload rejected; hand
computed golden bytes; round trips; a deliberate width mutation made the tests
fail. The byte-equality helper gains a Broadcast mode for functions that
ignored their address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:51 -05:00
Aaron Kimbrell
5a9a3e380b refactor: master packets as structs
Every MASTER service packet is now an LUBitStream struct (docs/PacketArchitecture.md,
PR 14) and the master server's switch is a dispatch map (PacketDispatcher), as are the
master handlers of the world, chat and dashboard servers.

- dNet/MasterPackets.h: RequestZoneTransfer, RequestZoneTransferResponse, ServerInfo,
  RequestSessionKey, SetSessionKey, SessionKeyResponse, NewSessionAlert, PlayerAdded /
  PlayerRemoved, CreatePrivateZone, RequestPrivateZone (passwords still cut to 50
  characters when read), WorldReady, WorldReadyInfo (WORLD_READY to the dashboard),
  PrepZone, Shutdown, ShutdownResponse, WorldShutDown (SHUTDOWN_RESPONSE to the
  dashboard), ShutdownUniverse, AffirmTransferRequest/Response, RequestServerList,
  ServerListResponse, DashboardShutdown, ConfigReload, InstanceShutdown. The Send*
  functions are gone; MasterPackets::SendToMaster(msg) and SendTo(sysAddr, msg) send a
  struct.
- The dashboard and instance migration structs (PlayerAction, DataChanged, Dashboard
  messages, MessageCapture, InstanceMigration) are LUBitStreams of the MASTER service now
  and moved to dNet/master/, included by MasterPackets.h. Their payloads are unchanged;
  master forwards them by re-serializing the struct instead of copying raw bytes.
- InstanceManager, ZoneInstanceManager, MigrationCoordinator, dServer (server info, zone
  transfer response), auth (SET_SESSION_KEY), the world (session keys, player added and
  removed, world ready, shutdown response, affirmations, prep zone, shutdown universe) and
  the dashboard (server list, instance shutdown, config reload, announcements, player
  actions, message capture) send and read structs.
- The login stamps are a `stamps` field of RequestZoneTransfer and
  RequestZoneTransferResponse (read leniently as before: a message without them reads as
  empty); master adds its stamps in the REQUEST_ZONE_TRANSFER handler and when it answers,
  as it did.
- InstanceManager::GetInstanceBySysAddr takes a const address.

Verified: tests/dGameTests/dNetTests/Legacy/MasterPacketsLegacy.h is a verbatim copy of
the old writers and readers; MasterPacketsTests requires identical bytes for a grid of
inputs, checks the old readers read what the structs write, round trips and truncation,
hand written golden packets, that zone transfers without stamps still read, that the dashboard/migration structs write what
"header + Serialize" wrote, and that the dispatcher drops truncated packets. No wire
bytes changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:50 -05:00
Aaron Kimbrell
796fd1b941 refactor: chat packets as structs
Every packet of the chat service is now an LUBitStream struct in dNet/ChatPackets.h
(docs/PacketArchitecture.md, PR 13), and nothing in the chat server or the chat side of
the world server reads or writes a packet by hand any more.

- World <-> chat: LoginSessionNotify, UnexpectedDisconnect, GMLevelUpdate, GMMute,
  Announcement (GM announce), CreateTeam, TeamUpdate (TEAM_GET_STATUS to worlds),
  AchievementNotify, ShowAllRequest, FindPlayerRequest.
- Client -> world -> chat (friends, ignore list, teams, general and private chat):
  GetFriendsList, AddFriendRequest, AddFriendResponse, RemoveFriend, GetIgnoreList,
  AddIgnore, RemoveIgnore, GeneralChatMessage, PrivateChatMessage, TeamInvite,
  TeamInviteResponse, TeamLeave, TeamKick, TeamSetLeader, TeamSetLoot, TeamGetStatus.
  The 77 bytes the handlers skipped are named fields now (the sender block the client
  fills in); the 4 unused bytes after the player ID are kept as `unknown`.
- What the client receives. Chat service (ChatPackets::Client): GeneralChatMessage
  (replaces SendChatMessage; SendSystemMessage stays as a helper built on it),
  PrivateChatMessage. Client service (ClientPackets, as structs go in the file of the
  ServiceType in their header): SendCannedText (replaces SendMessageFail), GetFriendsListResponse, AddFriendRequest,
  AddFriendResponse, RemoveFriendResponse, UpdateFriendNotify, WhoResponse,
  ShowAllResponse, Get/Add/RemoveIgnoreResponse, TeamInvite, TeamInviteInitialResponse,
  and the team game messages chat writes (TeamInviteConfirm, TeamGetStatusResponse,
  TeamSetLeader, TeamAddPlayer, TeamRemovePlayer, TeamSetOffWorldFlag) as TeamGameMsg
  structs, since chat doesn't link dGame's NetGameMsg.
- WORLD_ROUTE_PACKET is ChatPackets::WorldRoutePacket (its own file, dNet/WorldRoutePacket.h,
  since it carries packets of other services): the target and the inner packet.
  ChatPacketHandler::SendRouted replaces the per-function route headers and
  SendRoutedMsg.

Dispatch: dNet/PacketDispatcher.h is a dispatch map from packet ID to (struct, handler
function); packets that fail to Deserialize are logged and dropped. The chat server's
switch and the world's HandlePacketChat switch are now maps. The handlers take the
structs; their logic is unchanged. World code sends to chat with ChatServerLink::Send
(dGame) instead of writing to Game::chatServer by hand. eChatChannel,
eChatMessageResponseCode and eAddIgnoreResponse moved to dCommon/dEnums so the structs
can use them.

Verified: tests/dGameTests/dNetTests/Legacy/ChatPacketsLegacy.h is a verbatim copy of
the old senders and readers; ChatPacketsTests (25 tests) sends a grid of inputs through
both and requires identical bits, bytes and destination, checks the old readers read
what the structs write, round trips every struct, checks truncated packets are refused,
and has hand written golden packets for general chat, canned text, GM mute and a routed
team game message. Breaking one field width (UpdateFriendNotify) and the TeamAddPlayer
zone flag made the tests fail. No wire bytes changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:50 -05:00
Aaron Kimbrell
0c2727ad3c refactor: building game messages and blueprint packets as structs
Converts build mode, arranging, modular build and brick by brick
messages to NetGameMsgs in BuildingMessages.{h,cpp}: StartArrangingWithItem,
FinishArrangingWithItem, ModularBuildEnd and SetBuildModeConfirmed
(sent), and StartBuildingWithItem, DoneArrangingWithItem,
ModularBuildFinish, ModularBuildMoveAndEquip, ModularBuildConvertModel,
SetBuildMode, BuildModeSet, UnUseBBBModel, BBBLoadItemRequest and
BBBSaveRequest (received, registered in GameMessageHandler's map with
their handlers' logic kept). The BLUEPRINT_SAVE_RESPONSE and
BLUEPRINT_LOAD_RESPONSE_ITEMID client packets become the LUBitStream
structs ClientPackets::BlueprintSaveResponse and BlueprintLoadItemResponse,
also used by WorldServer's level load. The never-called
SendBBBSaveResponse is gone.

SetBuildModeConfirmed keeps writing modeValue's and startPos's default
flags as always set, as DLU did. BuildModeSet and UnUseBBBModel now read
their whole client layout (DLU read only the first fields).

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions and inline packet
writes over an input grid, received messages compared with the old
handlers' read sequences with every truncated payload rejected, hand
computed golden bytes, round trips and a mutation check. Layouts
confirmed against the 1.10.64 client in Ghidra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:49 -05:00
Aaron Kimbrell
9458230f0b refactor: property game messages as structs
Converts the property messages to NetGameMsgs in PropertyMessages.{h,cpp}.
Sent: OpenPropertyVendor, OpenPropertyManagement, DownloadPropertyData
(replaces PropertyDataMessage, now with the client's PropertyData field
names), PropertyRentalResponse, PropertyEntranceBegin,
PropertySelectQuery (replaces PropertySelectQueryProperty with the
client's PropertyInfo), GetModelsOnProperty, PlaceModelResponse and
HandleUGCEquipPre/PostDeleteBasedOnEditMode. Received, registered in
GameMessageHandler's map: SetPropertyAccess,
UpdatePropertyOrModelForFilterCheck, QueryPropertyData,
PropertyEditorBegin/End, PropertyContentsFromClient,
ZonePropertyModelEquipped/Rotated, PlacePropertyModel,
UpdateModelFromClient, DeleteModelFromClient, ControlBehaviors,
PropertyEntranceSync, EnterProperty1, UpdatePropertyPerformanceCost,
ReportOffensiveModel/Property and GetHotPropertyData. The news screen's
NewsSendHotPropertiesInfoToClient moves here with the top properties
lookup; the dashboard's player reports now take the decoded text.

Handlers keep their logic and hand the work to PropertyManagementComponent,
PropertyVendorComponent, PropertyEntranceComponent and
MultiZoneEntranceComponent as before. Messages whose payload DLU ignored
(PropertyEditorBegin, PropertyContentsFromClient, ZonePropertyModel*)
now read it with the client's layout. The never-called
SendZonePropertyModelEquipped (which wrote no default flags) is gone.

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions, PropertyDataMessage
and PropertySelectQueryProperty over an input grid (to one client and
broadcast), received messages compared with the old handlers' read
sequences with every truncated payload rejected, hand computed golden
bytes, round trips and a mutation check. Layouts confirmed against the
1.10.64 client in Ghidra. Known wire bug kept as is: PlaceModelResponse
writes response where the client reads a rotation quaternion
(PlaceModelResponse::Deserialize 0x00dc0170).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:49 -05:00
Aaron Kimbrell
527f54488b refactor: pet game messages as structs
Converts the pet taming minigame, naming, command and bouncer messages to
NetGameMsgs in PetMessages.{h,cpp}: NotifyPetTamingMinigame,
NotifyTamingModelLoadedOnServer, NotifyPetTamingPuzzleSelected,
PetTamingTryBuildResult, PetResponse, AddPetToPlayer, RegisterPetID,
RegisterPetDBID, ShowPetActionButton, BouncerActiveStatus, SetPetName,
SetPetNameModerated and PetNameChanged (sent), and PetTamingTryBuild,
NotifyTamingBuildSuccess, RequestSetPetName, StartServerPetMinigameTimer,
ClientExitTamingMinigame, CommandPet and DespawnPet (received, registered
in GameMessageHandler's map; each Handle hands the message to the
player's taming or active PetComponent exactly as the old handler did).
PetComponent, BouncerComponent and the hydrant/catapult scripts build
the structs; the old Send*/Handle* functions and switch cases are gone.
The never-called SendClientExitTamingMinigame is folded into the
ClientExitTamingMinigame struct. MarkInventoryItemAsActive stays with
the inventory messages.

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions over an input grid
(to one client and broadcast), received messages compared with the old
handlers' read sequences with every truncated payload rejected, hand
computed golden bytes, round trips, and a mutation check. Layouts
confirmed against the 1.10.64 client's Serialize/Deserialize in Ghidra.

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