For a zone file older than version 30 the client reads the terrain in
an older format (OpenTerrain, 1.10.64: mapVersion < 30), and the game
client never reads that format's scene map (only the editor does), so
scene loading goes by the zone's scenes alone. The world now does the
same instead of reading the terrain file as the newer format.
Check in game: nothing to check (all live zones are version 36+).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client reads a terrain chunk of a file older than version 32
(1.10.64):
- its color map as width x width BGRA pixels, keeping the
(width - 1) x (width - 1) before the last row and column, as RGBA
(RAWReadColorandLightMaps); the reader kept all the pixels as
written;
- its texture blend map's pixels as BGRA, kept as RGBA (0x0103aaf0);
- before version 31 no scene map, only a byte: the client's scene map is
all scene 0 (RAWReadSceneMap); the reader had none.
A chunk of width or height 0, which the client reads (no heights, no
color map), no longer fails the whole file.
Live terrain files are version 32, so they read as before.
Check in game: nothing to check (no live terrain changes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReadLvlObjectData (1.10.64, 0x0103ba20) loads every scene object except
those of LOT 1 (the player). The world spawned them; now it skips them.
No live scene has one; one older client scene file has two.
Check in game: nothing to check (no live scene has LOT 1 objects).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
DisplayChatBubble from the server doesn't show: in live it was only raised by client scripts. For a local chat line
the client puts a chat bubble over the sender, plays its talk animation and adds the line under the sender's name
(the chat handler raises DisplayChatBubble on the sender object), so the Guild Master now sends that player a local
chat line as itself, as property model chat does. Check: use the Guild Master while in a guild: bubble over it, it
talks, the line shows under its name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DisplayChatBubble from the Guild Master with "You are already in a guild!", besides the system chat line. Check:
use the Guild Master while in a guild: the bubble shows above it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Instead of opening the create box, a player with a guild gets "You are already in a guild!" in system chat, the
client's own MSG_GUILD_ALREADY_IN_GUILD text (which the client shows only once the box is submitted). Check: use the
Guild Master (LOT 3001) with and without a guild.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Members' clients read the chat text up to a terminating zero, as the client itself sends it; /g sent the text
without one, so the client read past it and showed extra characters ("test0>"). Check: guild chat shows exactly
what was typed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each WorldServer_<zone>_<clone>_<instance>_<time>.log (and older WorldServer_<zone>_<instance>_<time>.log) file set
made its own server entry; they're now grouped under WorldServer_<zone>, and the file picker still shows the full
names. Check: the Server picker lists one entry per zone and its file list holds every clone and instance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client adds every event string of the login response as an enabled feature (AddFeatureGate from the login
response handler), so event_1..event_8 = guilds turns guilds on in a stock client; no client file needs changing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/Guilds.md gains what DLU does with the client's guild system: the chat server as the authority, the world <-> chat
packets, the rank rules, the invite and name rules, the slash commands, the dashboard page and what is not done (a
member's name change reaches guildmates only with the next GUILD_DATA; reputation is 0). docs/Commands.md lists the guild
commands, docs/Dashboard.md the Guilds page, the guild names in the Review Queue and guild chat under chat_private.
Check: nothing in game (documentation only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Guilds page (Moderation, permission guilds_manage, GM 5 by default): every guild with its name status, member count,
leader and age, a pending-only switch, and per guild its members (with ranks) and history (guild_events). Staff can
approve a guild name that waits for review, reject it (the guild becomes "Guild <id>"), rename a guild (same name rules
as the game, unique without regard to case), remove a member (a leader's guild goes to the next member, the last
member's guild is deleted) and disband a guild. Every change is audited and added to the guild's history, and the chat
server is asked (GUILD_CHANGED through master) to tell the online members. Pending guild names also show in the Review
Queue. Guild chat ("guild" in the chat log) counts as private chat like whispers and team chat (chat_private), with its
own channel filter and Prometheus counter.
Check on the dashboard: /guilds lists a guild made in game; approve/reject/rename/remove/disband update the in-game guild
window and name billboard of online members; the Review Queue shows a guild whose name waits for review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The world side of guilds (docs/Guilds.md):
- TMP_GUILD_CREATE, which the client's guild create box sends to its world, goes to the chat server as GUILD_CREATE.
- The character component reads the character's guild from the database when it loads (instead of the old unused "gn"
and "gid" charxml attributes) and takes GUILD_GET_STATUS from the chat server; a change is serialized once (the
client redraws the name billboard every time it reads a guild name). A name waiting for moderation isn't shown.
- Slash commands (the client has none for guilds): /g and /guild (guild chat; the client's guild tab sends /g, which
goes through the chat filter and mute like zone chat, then to the chat server as channel 10), /guildcreate (opens the
create box with DisplayGuildCreateBox), /gkick, /grank <name> <officer|veteran|recruit>, /gleader and
/gdisband confirm.
- The Guild Master script (LOT 3001, L_GUILD_CREATE.lua: using it opens the create box). Live never placed it; GMs
can spawn it.
Check in game (two accounts, a client with a FeatureGating row "guilds", 1, 0, 0 in its cdclient.fdb): /guildcreate,
make a guild (the name shows under yours, the guild button appears on the status bar); invite the second character
from the guild window, accept, both lists show both; guild chat tab; /grank, /gkick, /gleader; leave from the window;
log the second character out and in (guildmate logged off/in lines).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connects GuildManager to the chat server (ChatGuilds): the client's GUILD_INVITE, GUILD_INVITE_RESPONSE, GUILD_LEAVE
and GUILD_GET_ALL (routed through its world), the worlds' GUILD_CREATE, GUILD_KICK, GUILD_SET_RANK and GUILD_DISBAND,
guild chat (GENERAL_CHAT_MESSAGE in channel 10, sent to the online members as channel 10 private chat, which the
client's guild tab shows, and logged as "guild"), guildmates told when a member logs in, out or changes worlds, and a
member's pending invite dropped when they log off. Client packets go through the member's world as WorldRoutePacket;
GUILD_GET_STATUS goes to the world itself.
Guild names: the chat filter's deny list refuses a name (dChatFilter::HasDenyList, since without a deny list the deny
check refuses everything); a name the allow list doesn't cover waits for moderation, as pet names do.
The dashboard's new player action GUILD_CHANGED goes from master to the chat server only (answered 1 when chat is
connected), which catches online members up. chatconfig.ini: guild_max_members (100) and guild_invite_timeout (600
seconds).
Check in game: nothing yet on its own (the world side makes guilds reachable). Server log: the chat server starts and
logs no unhandled guild packets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chat server is the guild authority (docs/Guilds.md). GuildManager keeps guilds in the database (IGuilds) and tells
clients through their worlds, with every outside dependency behind hooks so the rules are tested without a server:
- Create: name rules (3-30 of letters, digits, space ' - ., no space at either end or twice), the chat filter's deny
list refuses a name (BAD_NAME), names not on the allow list make the guild but wait for moderation (other players see
no guild name until then), a taken name (case ignored) is EXISTS, someone in a guild can't make one.
- Invite / answer: leaders and officers invite; the client's answers for not online, already in a guild, invite pending
and could not invite; one invite per player, answerable for 10 minutes and gone when the player logs off; a full guild
(guild_max_members) takes nobody. The new member is a recruit; the inviter hears the answer and gets the list again,
the other members get GUILD_ADD_PLAYER.
- Leave, and DLU's kick, rank and disband (the client has no controls for them): the leader kicks anyone, officers kick
veterans and recruits; a leader who leaves hands the guild to the highest-ranked, longest-serving member, and the last
one out ends it. Rank changes send everyone the list again (the client's GuildSetPlayerRank does nothing).
- GUILD_DATA for GUILD_GET_ALL, login/logout/world-change updates to guildmates, a guild that lost its leader (character
deleted) gets one again, and GuildChanged catches online players up with what the dashboard did.
- Every change is a guild_events row. Each character's world gets GUILD_GET_STATUS with the guild and the name others
may see.
Not connected to the chat server's packets yet.
Check: GuildManagerTests (18 tests) run every rule against an in-memory IGuilds. Nothing to check in game yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guild packets of the 1.10.64 client (docs/Guilds.md) as Serialize/Deserialize structs, laid out the way the client's
packet handlers read them: GUILD_CREATE_RESPONSE, GUILD_INVITE, GUILD_INVITE_INITIAL_RESPONSE, _FINAL_RESPONSE,
_CONFIRM, GUILD_ADD_PLAYER, GUILD_REMOVE_PLAYER, GUILD_LOGIN_LOGOUT and GUILD_DATA (ClientPackets), the world packet
TMP_GUILD_CREATE the create box sends, the chat packets the client sends through its world (GUILD_INVITE,
GUILD_INVITE_RESPONSE, GUILD_LEAVE, GUILD_GET_ALL), DLU's world <-> chat packets (GUILD_CREATE, GUILD_KICK,
GUILD_GET_STATUS as the chat server's guild update to a world, and GUILD_SET_RANK and GUILD_DISBAND appended to
MessageType::Chat after CREATE_TEAM) and the game message DisplayGuildCreateBox (626). Fixed-size names always keep a
NUL, since the client reads them as C strings. Enums eGuildCreateResponse, eGuildInviteResponse,
eGuildInviteFinalResponse, eGuildRank and eGuildLeaveReason. Nothing sends them yet.
Check: GuildPacketsTests compares every packet with the bytes at the client's offsets. Nothing to check in game.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four tables (MySQL migration 97, SQLite 80) and the IGuilds interface with MySQL, SQLite and TestSQL implementations,
for the guild system (docs/Guilds.md). A guild has a name (unique without regard to case), a name status like
pet_names (1 waiting for moderation, 2 approved), its founder and when it was made. A character is in at most one guild
(guild_members, with its rank: 1 leader, 2 officer, 3 veteran, 4 recruit, the client's names, and when it joined) and
has at most one pending invite (guild_invites). guild_events is each guild's history and outlives the guild. Deleting a
character removes its membership and the invites to and from it. Nothing uses the tables yet.
Check: both migrations run on a fresh and an existing database; DatabaseParityTests Guilds passes against MariaDB
(DLU_TEST_MYSQL_HOST) and SQLite gives the same results. Nothing to check in game.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/Guilds.md: what the client's guild system needs, from Ghidra and the client's Scaleform UI. The feature gate
(FeatureGating "guilds", missing from the shipped cdclient), the UI calls and messages, what the character component
serializes, every guild packet's layout and what the client does with it (including the ones it has no handler for),
GUILD_DATA's member records, DisplayGuildCreateBox, the guild chat channel (the chat box sends /g), rank names and the
limits the client enforces. No live capture has guild traffic.
Check in game: nothing (documentation only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checked for the "module textures in car and rocket icons" request: none
of the 399 render assets of the client's ModuleComponent LOTs (1.10.64;
the same in the other clients on disk) has an NiSourceTexture or
NiTexturingProperty. The modules' look is their vertex colors and
materials, which the icons draw already, so there is nothing to add.
Check: nothing to check in game; compare a car's and a rocket's icon with
the client's module icons (textures/ui/rebuilding/) if in doubt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A make's recorded time (process_ms, stats.json's ms.total) includes the
icon drawn at its end. Drawing only the icons again (the icon editor's
Draw all icons of this type again) left the stats and the rows with the
old icons' time. Now the icon-only job writes stats.json with the new
icon's time (ms.icon, ms.total changed by the difference,
UgcJobs::WithIconTime) and the main thread changes the row's process_ms
and process_cpu_ms by the same difference (the icon is drawn on one
thread, so its time is taken as its CPU time). The dashboard's Took,
CPU and totals follow.
Check: open a player model on the UGC page, Draw all icons of this type
again: afterwards its Took changes by the icon's difference and its
stats show the new icon time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UGC Search page and the UGC page searched the same things (owner,
account, property, the name and description a player gave a model, LOT,
UGC / blueprint id) with the same SQL. The UGC page's lists are now the
one search:
- models' List view has a Where column: placed on a property (with the
name given there and a link to the model in the property's 3D view),
in a mail or in the creator's inventories, or not found
(/api/ugc?where=1, UgcLinks::Whereabouts as before)
- owners link to their character and, with accounts_view, account
- while searching, the kind buttons say how many models and how many
cars and rockets match (/api/ugc "matches")
- /ugc_search?q= redirects to /ugc?view=list&q=; the menu's UGC Search
entry is now UGC, which opens the UGC page (it was only under Server
Admin, for ugc_manage)
- References lose their Find button (Where it is has the same) and name
the inventory a build is in
The ugc-search page and script are removed; /api/ugc_links/search stays
for API users.
Check: the menu's UGC entry; an old /ugc_search?q= link opens the UGC
list with the search filled in; owner:, account:, property:, name:,
lot: and id: searches, and the counts on the kind buttons; the Where
column's links (property, 3D, character, account).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UGC page's assemblies (cars and rockets) list had its own columns.
It now has the models' columns and sorts, minus Saved: ID and Owner (the
newest build and its creator, and how many other owners), State, Made,
Took, CPU and RAM (the latest make of any build, and the cost of the make
the UGC server did for the combination; builds that shared the made icon
cost nothing), Size (the module count), File (the build type and its
modules) and Where (how many builds and owners use it, opening
References). The gallery sorts the same way.
Check: UGC page, Cars and rockets, List: every column sorts both ways on
the server, and the gallery's sort list has the same sorts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The card had the UGC page's counts per state for both kinds, totals since
the server started and the last makes, which the UGC page's lists cover.
It now says whether the UGC server is up (throttled, paused), how many
items wait and failed (linking to the failed ones), its busy workers,
CPU, memory and stored files.
/api/diagnostics/ugc read the database on a worker thread; the counts are
now read on the web thread and the worker only fetches the UGC server's
status.
Check: Diagnostics with the UGC server enabled: the card is short, reads
right while it's up, throttled and stopped, and the failed link opens the
failed ones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cars and rockets built before builds were recorded were saved with
subkey 0 and have no ugc_modular_build row, so the client has no
blueprint id to ask for their icon with. The runtime fix only reaches
characters that load again.
Migrations mysql 98 and sqlite 81 (98/81 because the guilds work takes
97/80) run ModularBuildIdMigration after the SQL: every saved item with
modules (x@ma) and no subkey gets what a new build gets, as the
character-load fix does: a persistent id from object_id_tracker (with
the character bit) as its subkey and a ugc_modular_build row with its
modules and the character as owner. The XML is written with
UpdateCharacterXml. Tried on a copy of a server's database with a
couple of thousand such items: every one got an id and a build row, in
seconds.
Check: after the migration, old cars and rockets show their icons in the
backpack (the UGC server makes them like any build) and still work
(equip, race, launch).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New section 7 in CaptureUnknowns.md: how DLU's constructions of the 200 most
common LOTs were compared with the live captures, what was fixed, and each
remaining difference with its evidence (quickbuild default destroyable,
enemy immunities, script network vars, construction effects and buffs, pet
state, moving platform path info, jetpack, character club/prop-mod/leave
fields, model UGC ids, parent position flag, item extra info, component
order notes).
Check in game: nothing (documentation only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live wrote the item component's UGC info on every construction of an
object with an item component (99 of 99 constructions of LOT 14535, a
possessable with item, skill and controllable physics and no Objects row:
ug_id 0, moderation status NoStatus, no description). DLU wrote a 0 bit.
The item component is read after the skill and base-combat-AI components
(ComponentOrderVector::Initialize 0x0101f8e0; live constructions of LOT 14535
have Skill then Item). DLU wrote it before the inventory, which did not show while
it was a single 0 bit next to the skill's 0 bit, but would misalign every
component after it now that it writes more. It now comes after
BASE_COMBAT_AI. The ReplicaComponentOrderTests oracle moves the item the same
way (intended change).
Test: ReplicaConstructionTest.ItemConstructionWritesEmptyUgcInfo,
ReplicaComponentOrderTest.EveryListedComponent (oracle updated).
Check in game: with a second player watching, use a possessable / mount /
vehicle item (e.g. a racing car in the race lobby); it appears, moves and
is left normally for both. Rocket launches and property models look as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client reads an object's factions only from the DestructibleComponent
factionList (LWODestroyableComponent::LoadDataFromTemplate 0x00c9f900: one
atol per comma-separated token, -1 kept; the faction column is not read),
and live replicated that list: [-1] on 12,829 constructions (vendors,
quickbuilds, bouncers). DLU added the faction column and dropped -1,
so those objects were sent with no factions, and the four rows with faction 6
but factionList "-1" were sent as [6].
-1 has no Factions row, so it adds no friends or enemies.
Test: ReplicaConstructionTest.TemplateFactionMinusOneIsReplicated.
Check in game: vendors, quickbuilds, bouncers and NPCs still can't be
attacked; enemies still fight players and pets; smashables still smash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live's simple-physics constructions (40,786 in the captures):
- Motion type Fixed (5) for objects without a motionType (38,970). DLU read
the level's motionType and, when there was none, sent 0, a value the
client's motion-type enum does not have.
- Keyframed (4) for every moving platform (1,059 of 1,059) and for models
without behaviors; Dynamic (1) for models with behaviors (655 of 666 model
constructions follow this; DLU only switched it when a behavior was added
or removed).
- No velocity for fixed objects (all 38,970); keyframed and dynamic objects
have one. DLU wrote a velocity in every construction.
A level-set motionType still wins.
Test: ReplicaConstructionTest.SimplePhysicsConstructionLikeLive (values of a
captured smashable, LOT 12266); the existing SimplePhysicsTest bytes are
unchanged.
Check in game: moving platforms (AG, GF, NS, NT), spinners and other
simple-physics objects move and collide as before; smashables and static
objects stay put; on a property, placed models with and without behaviors
sit and run as before, including after picking them up and placing again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live wrote the controllable-physics cheat block (gravity scale, speed
multiplier) in a construction only when one of them differed from 1: 216 of
20,617 constructions had it (players at run speed 1.05 / 1.5 / 2.0 / 0.5,
enemies with gravity 0), never 1 and 1. DLU wrote it in every construction.
Serializations are unchanged.
Test: ReplicaConstructionTest.ControllablePhysicsConstructionWritesCheatsOnlyWhenChanged.
Check in game: with a second player watching, use a speed boost (or /setspeed)
and zone in while boosted; the other player sees you move at the right
speed. Enemies with gravity 0 (e.g. Sentinel Turret 6254) behave as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Equipped items in the inventory component construction, as live wrote
them (49,534 equipped items in the captures):
- is_bound is the item's bound state (false for 2,990 items that bind
neither way); DLU always wrote true. NPC items are bound when the item
binds on pickup or on equip (2,640 of 2,640).
- inventory_type is the item's inventory, left out for ITEMS: TempEquip for
temporary equips and proxies (3,899), Model for models (232). DLU never
wrote it. DLU keeps player proxies in ITEM_SETS; they are sent as
TEMP_ITEMS, like live.
- An NPC item's slot is its slot in the inventory it would be in: models
count from 0 in MODELS and proxies from 0 in TEMP_ITEMS, so a proxy no
longer takes the next ITEMS slot (1,475 of 1,491 NPC constructions match;
DLU's numbering matched 1,352).
- equipped_model_transforms is an empty list on every construction (11,214
of 11,214); DLU wrote none.
Test: InventoryConstructionTest.NpcItemsLikeLive (NPCs 7426 and 13790 from
the captures), InventoryConstructionTest.PlayerItemsLikeLive.
Check in game: NPCs with gear (Numb Chuck in FV, the Ninjago ninjas, faction
vendors) show all their gear; your own and other players' equipped items,
proxies (ninja hoods, capes) and temporary equips (Maelstrom vacuum, quest
items) show normally after zoning and after equipping/unequipping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live wrote all three optional blocks of the character component in every
player construction (9,726 LOT 1 constructions: gm_pvp_info with is_gm
false and gm_level 0 for civilians, current_activity Some(None) on 9,620,
social_info always Some with guild 0 and an empty name). DLU wrote each one
only after something set its dirty flag, so civilians got none of them.
Serializations are unchanged (still only when dirty).
Writing the social block every time exposed two bugs in it: the guild ID
was never initialized (now 0), and the guild name was written with
sizeof(wchar_t) (4 on Linux) bits per character, reading past the end of
the UTF-16 string; it is now written as 16-bit characters.
Test: ReplicaConstructionTest.CharacterConstructionAlwaysWritesGmActivityAndSocialBlocks.
Check in game: log in as a civilian and as a GM, with a second player
watching; both see each other normally (name tags, GM tag/level for the
GM, no PvP flag), and starting a quickbuild or pet taming still shows the
activity to the other player.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent the milliseconds since the object was created on the server in
every construction header (nonzero in 142,258 of 145,677 captured
constructions; the same object constructed again later carries a larger
value by the time elapsed; the local player's own construction on load is
0). DLU always wrote 0. The client stores it in the object's config
(LWOGameObject::time_since_created_on_server).
Entities now remember when they were created (steady clock) and write
their age.
Test: ReplicaConstructionTest.TimeSinceCreatedOnServerIsTheObjectsAge.
Check in game: zone in and walk around several worlds; objects, NPCs,
enemies and other players appear and animate as before (no objects
popping, stuck or misplaced animations on platforms/spinners).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
The pairs a waypoint carries (waypoint commands on movement and rail
paths, LDF config on spawner paths) are read by one helper,
ZoneFile::ReadLdfConfig, so the legacy path format can read them too.
Nothing read changes: every zone file on disk reads the same.
Check in game: nothing new; spawners and moving NPCs behave as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>