Commit Graph

98 Commits

Author SHA1 Message Date
Aaron Kimbrell
e11493008d feat(net): MATCH_REQUEST and MATCH_TRANSFER chat packets and the chat server's MatchUpdate
MATCH_TRANSFER is appended after GUILD_DISBAND (71); the chat ids from
CREATE_TEAM on are pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 04:29:59 -05:00
Aaron Kimbrell
d71e44f3e1 feat(world-reload): keep properties until empty
Busy property instances on changed files are kept (KEEP_UNTIL_EMPTY) instead
of saved, frozen and moved. Every instance acted on is marked outdated; the
files status carries the flag (bit 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 04:29:54 -05:00
Aaron Kimbrell
891f21daa9 feat(live-update): never move properties; mark old instances outdated
Property instances are left out of the replace plan. Once the database is
up to date every instance running at the start is marked outdated, and a
zone that already got a new instance is not started again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 04:29:54 -05:00
Aaron Kimbrell
8f891229e6 feat(master): outdated instances take nobody new and stop once empty
Instances on an old binary or old zone files are marked outdated: routing,
private passwords and merges skip them. Outdated properties remind their
players every 10 minutes that an update is waiting (ANNOUNCE) and stop once
empty. A new instance of the same property waits for the old one to be gone
before its world server starts, so two worlds never save one property.
The server list carries the flag after the endpoints (read when present).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 04:29:54 -05:00
Aaron Kimbrell
16545d573e feat(master): watch world zone files and replace instances that loaded old ones
Master hashes each reported file on a worker, polls size and mtime every
world_watch_seconds (a change must hold still for one poll), and replaces
stale instances with the instance migration (Mythran shift, or seamless
with world_reload_seamless=1); empty ones are stopped. Also on WORLD_RELOAD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:51:21 -05:00
Aaron Kimbrell
ab395a1af9 feat(net): WORLD_FILES, WORLD_RELOAD and WORLD_FILES_STATUS master messages
Appended at the end of MessageType::Master (47-49); the pin test is
updated. A world sends its zone file list to master once it is ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:51:21 -05:00
Aaron Kimbrell
1f87b2557d feat(cdclient): servers open master's fdb copy, never the client's file
Worlds, the UGC server and the dashboard open the pair named in
resServer/cdclient-current (CDServer.sqlite without an fdb when there is
none). Worlds switch to a new pair on CDCLIENT_RELOAD between frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:20:20 -05:00
Aaron Kimbrell
b05b84ff30 feat(net): CDCLIENT_RELOAD master message
Appended at the end of MessageType::Master (46); the pin test is updated.
With names it tells a server which fdb copy and CDServer.sqlite to switch
to; without them it asks master to check the client's fdb now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:20:20 -05:00
Aaron Kimbrell
1a8e4e387c feat(cdclient): content-addressed copies of the client's cdclient.fdb
FdbSnapshot names copies cdclient-<hash>.fdb / CDServer-<hash>.sqlite, writes
them under temporary names and renames them in, keeps a pointer file naming
the current pair, removes old copies, and decides when a changed file has
settled enough to hash. Nothing in it logs, so it can run on a worker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:20:20 -05:00
Aaron Kimbrell
fb6d73e4bd feat(web): API key traffic is its own Network row, named after the key
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:26:17 -05:00
Aaron Kimbrell
569ac6fb31 feat(web): count HTTP clients by the account they were signed in as
Each address's requests are counted per signed-in account (the dashboard's
session or API key, as the auth middleware found it), so several people
behind one address stay apart, with the account's user name; a WebSocket
upgrade counts under its account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:26:17 -05:00
Aaron Kimbrell
d0c7b089ff feat(net): frame timing section in SERVER_TRAFFIC, profiling messages
An optional section after marker 3 carries the report's frame timing; older readers stop before it and reports without it still read. Phase times go with their count so readers with fewer or more phases read them. PROFILE_REQUEST and PROFILE_RESULT are appended to the master messages (44, 45). Task 96.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:26:17 -05:00
Aaron Kimbrell
06af6c3aaa feat(profiler): frame timing and scope trees of a server's main loop
Profiler.h: frames, named scopes and phases, recorded on the main thread only (other threads' scopes do nothing). Per second: frames, total and longest frame time, a mergeable frame time histogram (TrafficStats::Histogram) and time per phase. Per report: the packet types that took longest, the worst frames and the frames over a threshold with their heaviest scopes. Profiling sessions merge every frame's scope tree for a while into one (folded stacks for flame graphs). Optional Tracy client (DLU_TRACY, off by default) gets the same frames and scopes. Task 96.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:26:17 -05:00
Aaron Kimbrell
d670484b68 feat(dCommon): read cdclient.fdb in place through its hash buckets
FdbMappedFile maps a file read-only (CreateFileMapping/MapViewOfFile on
Windows, mmap elsewhere) and falls back to reading it into memory when
mapping fails. FdbReader reads the table and column headers from it and
looks rows up by their first column through the fdb's own buckets,
decoding every integer as little-endian with bounds checks, so the rows
never get copied out of the file.

Tests write small fdb files (collisions, text, int64, nulls) and read
them both mapped and from memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:07:45 -05:00
Aaron Kimbrell
b67e0ba9ba feat(common): read the memory text the client sends at login
The login request's memoryStats is a run of short parts with no separators
(process working set and private bytes, memory load, physical memory, commit
limit, the 32-bit client's own address space, peaks). ParseMemoryStats splits
it into numbers; a text cut short keeps the parts before the cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:09:13 -05:00
Aaron Kimbrell
9e8f161dd8 feat(traffic): count traffic by peer and by connection
Every server now splits its packet counts by peer: its own connections
(players on auth and worlds), its master link, or other servers (the
worlds on chat, every server on master, a world's chat link, which is
now counted too). HTTP requests carrying X-Darkflame-Server count as
another server's, the dashboard counts its own requests to the UGC
server, and each HTTP client address is counted. The report also gets
each RakNet connection's statistics (worlds name the player on it),
trimmed to the 32 busiest with the rest summed. Counting stays on the
main loop, except the dashboard's UGC fetches, which only touch the
locked recorder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:28:39 -05:00
Aaron Kimbrell
5cd8b183cc test(zone): use neutral names and IDs in the zone file tests
The zone file tests' sample zones now use generic world IDs, file names
and path names instead of ones taken from real files. What they test is
unchanged.

Check in game: nothing (test only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:39:37 -05:00
Aaron Kimbrell
f73f4da0e4 fix(level): split a scene object's config as the client does
LDF_FROM_STRING (1.10.64, 0x010fa220) splits an object's config at
every comma and line break, leaves out empty entries and entries with
no '=', and sets each key=value it finds. The reader split at line
breaks only and parsed every piece, so an object with no config got an
empty entry of unknown type, and a value with a comma would have stayed
whole.

Live scene files are unchanged; objects in older client files with an
empty config no longer get the empty entry.

Check in game: every world spawns its objects as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:39:37 -05:00
Aaron Kimbrell
272fafc886 test(zone): read every zone, scene and terrain file the client reads
ClientZoneFilesTests now
- reads sd0 files instead of skipping them: some clients' loose sd0
  files hold what followed the file in its pack, and the file is the
  first sd0 stream, up to the first chunk that does not fit;
- reads the terrain (.raw) of every zone of version 30 or newer, once
  per terrain file shipped by several clients;
- skips a damaged file (by the MD5 of its bytes) only when it does not
  read, so only the files the client cannot read either are left out:
  a scene file that claims 81 objects and ends after the first, and six
  version 30 terrain files with a width x width scene map per chunk
  where the client reads one byte (RAWReadSceneMap, 1.10.64) and so
  misreads every chunk after the first.

Every other zone, scene and terrain file of every client on disk reads,
in about 80 seconds.

Check in game: nothing (test only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:39:37 -05:00
Aaron Kimbrell
695bcf2352 fix(level): change scene object config as the client does while loading it
ReadLvlObjectData (1.10.64, 0x0103ba20) changes an object's config as it
loads it:
- before version 47 a respawn time is in milliseconds: a float over 100
  or any u32 becomes that many seconds, as a float;
- a model spawner (LOT 176 with spawntemplate 14) gets its subkey as its
  blueprintid unless it has one, DisableModelBehaviors true unless set,
  and preventRenderWrapping true;
- before version 44 an object with sceneIDOverrideEnabled gets
  sceneLayerIDOverride 0.

LevelFile now makes the same changes. On live worlds 1906 spawners in
114 scene files of versions 45-46 have a u32 respawn time; the world
already divided those by 1000 but as an integer, so they now keep their
fraction (a 2500 ms respawn is 2.5 s, not 2 s). No live scene has a
model spawner; 62 objects of version 43 scenes get sceneLayerIDOverride.

Check in game: enemies and smashables respawn after the same time as
before on Forbidden Valley, Avant Gardens and Gnarled Forest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:39:37 -05:00
Aaron Kimbrell
458fb601c9 fix(level): find a scene file's chunks and sections as the client does
SceneLoader::ReadLvlFile (1.10.64):
- chunked files: the file info chunk starts the file and gives where
  the environment, object and particle chunks start, 0 for none
  (DoLvlChunk, ReadLvlChunk1000); the reader walked the chunks one after
  another by their sizes instead;
- older files: the editor settings are a u32 size and that many bytes
  (SceneLoader::ReadEditorSettings, skipped by size); the reader took
  them for a u32 and then a count of 12 byte points;
- older files before version 3 have no objects for the client ("Level
  file is unsupported").

LevelFile now does the same. Every scene file on disk reads the same
objects as before.

Check in game: every world spawns its objects as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:39:37 -05:00
Aaron Kimbrell
96d73c30a4 fix(level): read scene objects as the client does
ReadLvlObjectData (1.10.64, 0x0103ba20) reads a scene object's last
field, from version 7 on, as a render technique count, and when it is
not 0 a 64 byte block and 133 bytes per technique (a 64 byte name, a
u32, a u8 and 16 floats) follow. The reader took the count for an
unknown u32 and read the next object from inside the techniques, and
read the u32 for every version. It also left the node type and glom ID
uninitialized when a file has none (or, for the node type, one outside
0-10); the client uses 1 for both.

The count is now SceneObject::renderTechniqueCount, the techniques are
skipped, and the defaults are the client's. Every scene file on disk
has no render techniques, so what they read is unchanged.

Check in game: every world spawns its objects as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:36:35 -05:00
Aaron Kimbrell
13a3d18789 fix(zone): adjust path values as the client does while reading them
LevelPath::FromBuffer (1.10.64) changes two values as it reads a path:
- a property path's reputation multiplier outside 0-1000 becomes 0;
- a camera waypoint's FOV in a path older than version 11 is divided
  by 1.25.

The reader kept both as written. Only camera paths of older client
files (versions 8-10) change; no live zone has either case.

Check in game: nothing to check (no live zone changes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:36:35 -05:00
Aaron Kimbrell
83c2136e9a fix(zone): spawner and camera paths have their fields only from version 4
The client reads a spawner path's fields (LOT, respawn time, counts,
object ID) and a camera path's next path and rotate-player flag only
when the path is newer than version 3 (LevelPath::FromBuffer, 1.10.64:
pathVersion > 3). The reader read them for every version, which would
misread an older spawner or camera path from there on.

No zone file on disk has a spawner or camera path older than version 4,
so what they read is unchanged.

Check in game: nothing to check (no live zone changes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:36:35 -05:00
Aaron Kimbrell
17425c1e44 fix(zone): read a zone's paths as the chunk the client reads
The client reads a zone file's paths as a chunk of its own
(LuzFile::ReadLUZFile copies the u32-sized chunk, ReadLUZPaths reads
it, 1.10.64) and drops every path when it refuses any of it:
- a chunk version of 2 or more, or 10000 paths or more (ReadLUZPaths);
- a path of version 19 or more, 10000 waypoints or more, or a
  waypoint with more than 99 name/value pairs (LevelPath::FromBuffer).

The reader read the paths straight from the file, trusting the chunk to
be well formed. It now reads the chunk by its size, reads the paths
from it with the client's limits, and when one is refused the zone has
no paths (logged), while the rest of the zone file still reads.

Every zone file on disk reads the same paths as before.

The unit tests' sample zones now give the path chunk its real length.

Check in game: moving platforms, NPC patrols and spawners work as
before on every world.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:36:35 -05:00
Aaron Kimbrell
ee9c6288cb fix(zone): a scene transition links the scenes of its first and last points
Scene transitions of versions 34-38 have five points. The client links
the scenes of the first and the last one (ZoneLoader::ReadZoneFile,
1.10.64: points[0] and points[count - 1], for every version); the scene
graph used the first two, so in those zones it linked a scene with the
one of the transition's second point instead.

Of the 130 five-point transitions on disk, six (all in one older client
zone) have a different scene at the second and last points; the rest
link the same scenes as before.
Live zones are unchanged.

Check in game: nothing to check (no live zone changes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:36:35 -05:00
Aaron Kimbrell
6a8e8ece04 fix(zone): keep the scenes of a zone file the client loads
LuzReader::ReadScenes (1.10.64) keeps a zone's scenes by scene ID and
layer, so a later scene with the same ID and layer replaces an earlier
one. ZoneLoader::ReadZoneFile then goes through them in scene ID order
and
- leaves out every layer of a scene ID that has no layer 0 (General)
  scene;
- before version 41 loads only the layer 0 scene of each ID; from 41 on
  the other layers (Audio, FX) are loaded with it.

ZoneFile now keeps the scenes the same way, so the world loads the same
scenes as the client. No zone file on disk has a duplicate, a scene
without a General layer, or a non-General layer before version 41, and
their scenes are already in ID order: what every file reads is
unchanged.

Check in game: nothing to check (no live zone changes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:36:35 -05:00
Aaron Kimbrell
9f390958f5 fix(zone): scenes of version 30-32 zone files are numbered like the client
Zone files before version 33 have no scene IDs. The client gives each
scene its index in the file as its ID, -1 past 255, with layer 0
(LuzReader::ReadScenes, 1.10.64, via the LWOSceneID from the loop
index); the reader left every such scene at ID 0, so a zone with several
scenes had them all collide on one ID.

The version 30 zones on disk have one scene each, so what they read is
unchanged.

Check in game: nothing to check on live worlds (all are version 36+).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:36:35 -05:00
Aaron Kimbrell
d4fc595e2d feat(zone): read zone files of every version the client reads
Zone files older than version 30 were refused. The client reads them
(1.10.64):
- LuzFile::ReadLUZFile reads a version below 20 as 20;
- LuzReader::ReadScenes reads a scene of a version 20-29 file as a bare
  u32 SceneTable ID, with no file name;
- ZoneLoader::ReadZoneFile takes those scenes in SceneTable ID order,
  looks each one up in the CDClient SceneTable and, when there is a
  row, makes it a scene with the next index (0, 1, ...; -1 past 255) as
  its ID and the row's sceneName as its file; a scene with no row is
  left out.

ZoneFile now reads these files (FileFormatVersion::Oldest = 20 for
anything older), keeps the SceneTable ID on ZoneScene::sceneTableID,
and ZoneFile::ResolveSceneTable does the lookup the client does; the
world looks the names up in the CDClient SceneTable.

The only such file on disk (version 20) now reads.

Check in game: nothing to check on live worlds (all are version 36+).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:36:35 -05:00
Aaron Kimbrell
34ece7f640 test(zone): damaged zone files are skipped by the MD5 of their bytes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 06:21:06 -05:00
Aaron Kimbrell
d5563ad5da refactor(zone): LWOSCENEID lives with the zone code, its layer is an eSceneType
LWOSCENEID and LWOSCENEID_INVALID move from dCommonVars.h to
dZoneManager/LWOSCENEID.h; only the zone code uses them. The layer half
is the scene's layer, now the new eSceneType (dCommon/dEnums) instead of
a uint32_t: General (0), Audio (1, the *_audio.lvl scenes named
"Audio") and FX (2, Nexus Tower's *_fxs.lvl scenes named "FXs"), the
three layers the zone files of every client on disk use. ZoneScene's
sceneType is an eSceneType too, so a scene's LWOSCENEID is built from it
directly, and the dashboard compares against eSceneType::General instead
of 0. The zone checksum still hashes the layer's number.

Check in game: every world loads (the zone checksum the client checks is
unchanged), Nexus Tower included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 04:50:18 -05:00
Aaron Kimbrell
0143ed834c test(zone): read every zone and scene file of the clients on disk
ClientZoneFilesTests reads every .luz and .lvl under DLU_CLIENTS_DIR
(default ~/Documents/luclients) and expects each to read in full; the
tests are skipped when no client is there. Left out: empty files, sd0
files (the loose sd0 files of the 1.7.45 and 1.9.76 clients are all cut
up after their second chunk; the server gets its files uncompressed from
the packs) and one damaged scene file, which claims 81 objects
in 910 bytes.

With the zone and scene reader fixes before this every file of the
0.179.12, 1.7.45, 1.9.76, 1.10.64 and unpacked clients
reads.

Check in game: nothing (test only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 04:50:18 -05:00
Aaron Kimbrell
59e1d8f7c1 fix(level): read pre-chunk scene files of versions 31 and 33
Two fields of the scene files from before chunks were read for versions
the client does not read them for (SceneLoader::ReadLvlFile, 1.10.64):
- the "important" byte after the version and type is there only from
  version 32 (SceneLoader::ReadLvlHeader, 0x010117d0: headerVersion < 32 sets it to 0);
- the five strings after the skydome's file are there only from version
  34 (SceneLoader::ReadSkydomeInfo, 0x0102f550: headerVersion > 33); the reader took
  them from version 33.

Seven scenes of the alpha leak (versions 31 and 33: Gnarled_assetsMIKET,
dennis_gnarled_forest_02/_03-ninja/_03-pirate_0, scale_TEST_2,
scale_test_2_0, scale_test_general) now read with all their objects.
Every other scene file on disk reads the same objects as before.

Check in game: every world spawns its objects as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 04:50:18 -05:00
Aaron Kimbrell
e7ec11759e fix(zone): spawner paths have an activate-on-load byte only from version 9
The client reads a spawner path's activateOnLoad byte only when the
path is newer than version 8 (LevelPath::FromBuffer, 1.10.64:
pathVersion > 8) and otherwise keeps its default of 1
(Path::InitializeSpawnerInfo). The reader read the byte for every
version, which would misread an older spawner path from there on, and
its default was 0. The byte is now read from version 9, and an older
path's spawner is active on load as in the client.

No zone file on disk has a spawner path older than version 9, so what
the world spawns is unchanged.

Check in game: nothing to check (spawners on live worlds are unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 04:50:18 -05:00
Aaron Kimbrell
0d2fcc7dff feat(zone): read version 1-2 paths
Paths before version 3 are laid out differently (LevelPath::FromBuffer,
1.10.64: pathVersion < 3):
- the type is a u8-length wide string, "platform" for a moving platform
  and anything else ("npc") for a movement path, instead of a u32;
- the flags and behavior u32s follow as in later versions;
- every waypoint, whatever the path's type, has a rotation, lock player
  byte, speed and wait time and then its name/value pairs.

The reader treated the type name's first bytes as the type and misread
everything after it. The LUP group zones of the 0.179.12
client (lup_group_1b, _2a, _6a) now read with all their paths; one of
them read before with a garbage type on a waypoint-less path. Every
other zone reads the same as before (no live zone has a path older than
version 3).

Check in game: nothing to check on live worlds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 04:50:18 -05:00
Aaron Kimbrell
942d0ff8ad fix(zone): PrePreAlpha zone files have no zone name or description
The client reads a zone's name and description only when the file is
newer than version 30 (LuzFile::ReadLUZFile, 1.10.64: revision > 0x1e);
a version 30 file ends at its terrain file's name. The reader read both
strings for every version, so version 30 files ran off their end.

The four version 30 zones of the alpha leak (scale_TEST_ASSETS,
Gnarled_assetsMIKET, dennis_gnarled_forest_02/_03-ninja) now read. Some
of them have a stray name string after the terrain file's name, which
the client does not read either. Every newer file reads the same as
before.

Check in game: nothing to check on live worlds (none is version 30).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 04:50:18 -05:00
Aaron Kimbrell
69093c6816 fix(zone): zone files older than version 30 are refused
Before version 30 (PrePreAlpha) a zone file's scene is only a u32 scene
ID, with no scene file name (LuzReader::ReadScenes, 1.10.64:
revision < 30 reads the ID alone; LuzFile::ReadLUZFile treats anything
below 20 as 20). The reader read a file name and then an ID for those
versions, which is not the format, and the world could not load such a
zone's scenes anyway. Reading one now throws a runtime_error that says
why, before anything else is read.

No zone file of any client on disk is older than 30 except an unnamed
editor stub (a res/.luz, version 20).

Check in game: every world loads as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 04:50:17 -05:00
Aaron Kimbrell
04852fce7e fix(zone): zone files of version 37 have a u32 scene count
The scene count is a u8 before version 37 (LateAlpha) and a u32 from 37
on (LuzFile::ReadLUZFile, 1.10.64: revision < 37 reads a byte). The
reader took the u8 for 37 too, so version 37 files misread from their
scenes on and failed.

No live zone is version 37; four older LUP zones
now read in full.
Every other zone file on disk reads the same as before.

Check in game: nothing to check on live worlds (none is version 37).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 04:50:17 -05:00
Aaron Kimbrell
97a1453bf7 feat(zone): read the zone boundary lines of .luz files
Between the scenes and the terrain file's name a zone file has its
boundary lines: a u8 count, then per line a normal, a point, the
destination zone, the destination scene ID and a spawn location. The
reader took that spot for a u8-length "zone path" string, which is the
same bytes only when there are no boundaries; a zone with boundaries
would have misread everything after it.

The destination is one u32 in the client (LuzReader::ReadZoneBoundaryLines,
0x01018490 in 1.10.64: map ID in bits 0-15, instance ID in bits 16-31,
clone 0), read here as two u16s into an LWOZONEID with clone 0. The
boundaries are kept on ZoneFile::zoneBoundaries.

No zone of the 1.10.64 client (or any other client on disk) has
boundary lines, so what is read from them is unchanged.

Check in game: every world loads and zone transfers (launch pads,
rockets, portals) work as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 04:50:17 -05:00
Aaron Kimbrell
e9ff5f402d feat(world): read UgcDownloadFailed and log real download failures
World packet 120 (UgcDownloadFailed: resType u32, blueprint ID, status
u32, character ID; lu_packets, 31 bytes after the 0x53 as the client
sends it) was logged as an unknown world packet. The client sends it
from LWOResMgr2Interface::FinishResourceRequest (0x0105e5c0) for every
blueprint file (player model, car or rocket build) whose request did not
end with HTTP 200, including files it did not download at all (status
0). Live: 1525 packets, 1520 with status 0 (all four file types, around
load and FetchModelMetadataRequest), 5 with 404. Live sent nothing back
and the sessions carried on.

The logout the client does on failed UGC downloads
(NET_DISCONNECT_FAILED_DOWNLOAD_UGC, MainThread_LogoutDueToConnectionFailures
0x0102b9c0) is its own decision after repeated connection failures to the
UGC HTTP server; there is no server message that prevents or triggers it.
So the server only records it: a log line for an HTTP failure (for
finding missing files on the UGC server), a debug line for status 0.

MessageType::World gains UGC_DOWNLOAD_FAILED = 120 (appended; the magic
enum range goes to 120).

Check in game: nothing changes for the player. With a property that has
models, load it: the world server log has no "Unknown world packet 120"
lines; if a model file is missing on the UGC server there is one "failed
to download blueprint" line naming it.

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
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
b5e92ce97c fix(messages): swap the names of game messages 1593 and 1594
The client registers SetPropertyModerationStatus as 1594
(GameMessage::SetPropertyModerationStatus sets id 0x63a; no client
code uses 1593). The enum had the two names one slot off. Names only;
the IDs are unchanged and the pin tests still check both values.

In game: nothing changes; neither message is sent yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:33:36 -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
17513a8390 fix: splitting and normalizing models moves every bone and rigid, in file order
Lxfml::NormalizePosition moved only the first bone of each part, and never
the rigid systems, so after a save a flexible part's other bones and every
Rigid kept their world positions while the rest of the model was moved to
its own origin. Now every Bone and every Rigid moves by the same amount, and
the box that places the model holds every bone. Split maps every bone of a
part too, so a rigid system holding only a later bone keeps its brick.

Also:
- a model with no readable bone is kept as it is at the origin instead of
  getting a center 10000 up
- the math is done in doubles and numbers are written as the shortest text
  that reads back as the same float (-0.4, not -0.400002; 12.1678, not
  12.1677)
- bricks and rigid systems come out in the file's order, so the same model
  always splits into the same bytes
- parts over 5 MB are normalized like any other (the input is capped at
  10 MB), and the loop's safety limit no longer drops every later model

The pivot is still snapped to the 0.8 grid (the bricks don't move in the
world; the model's position stays on the grid), which can put it half a
stud from the middle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:19 -05:00
Aaron Kimbrell
2da96ada83 feat(dashboard): 3D views show a zone's scenes as the game loads them
A "Scenes" choice in the world and property 3D views: every scene (as
before), the scenes the game keeps loaded around the camera or the followed
player (the scene under it from the terrain's scene map, the scenes its
transitions connect to, and the global scene, following as it moves), or
scenes picked from a list (world view). The lighting blends to the lighting
of the scene under the focus, as the client blends between scenes.

The scenery manifest now carries each object's scene, the zone's scenes with
their neighbours and lighting, and the scene map as runs (37 KB for Avant
Gardens); scenery-core.js finds the scene at a point exactly as ZoneScenes
does (checked against it on 2000 points of Avant Gardens).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:17 -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
c939a69240 feat(dashboard): UGC list shows a model's given name, and Took/CPU/RAM columns
The models list's Took, CPU and RAM (est.) are separate columns, each
sortable (sort=slowest|cpu|memory). The File column shows the name a
player gave the model where it is placed, with the upload's extension
(the upload's file name is its tooltip), and the name sort uses it
(case-insensitive, so SQLite and MySQL agree).

The config layer test no longer assumes the build's sharedconfig.ini has
no mysql_host; it checks that the database-supplied value isn't used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:16 -05:00
Aaron Kimbrell
37459e0019 fix(properties): top property slots show only their own world
The news screen's slot tooltip names the slot's own property world
whatever property is sent for it, so a property of another world was
shown under the wrong name. Each slot now only shows a property of its
own world: a location stored by the older per-slot setting is kept in
the table but not used, full auto fills each slot with its own world's
top property instead of the top four across every world, and the
dashboard no longer offers a location or candidates from other worlds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:14 -05:00
Aaron Kimbrell
ceec638e52 feat(common): Sd0::Compress, a default Sd0 and room for chunks that don't shrink
Sd0 gets a default constructor and Sd0::Compress(data), the raw sd0 bytes of
some data (what the client reads for UGC files it downloads without 3D
services). FromData compresses into a heap buffer big enough for a chunk that
grows when deflated (it used a 256 KiB stack buffer, so random data failed and
workers carried a large stack frame), and drops a half-written result.

Tests: chunks as the 1.10.64 client inflates them, incompressible data, empty.

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