Commit Graph

2352 Commits

Author SHA1 Message Date
Aaron Kimbrell
342269cf61 fix(scripts): the Forbidden Valley cavalry of the horsemen path gets its script
L_ACT_HORSEMEN_1 (one path spawner of Maelstrom Cavalry, LOT 7816, in Forbidden Valley) had no script. It does what
L_FV_MAELSTROM_CAVALRY does (tell the group's turret it spawned, report a Brick Fury kill to the horsemen trigger),
so it uses that port. Check: the cavalry on that path wakes the ninja turret and Brick Fury kills count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:29:59 -05:00
Aaron Kimbrell
22f6a76f46 feat(scripts): turret enemies are immovable as L_TURRET makes them
L_TURRET (LOTs 12286 and 13093, e.g. the Crux Prime auto turrets) had no script, so these turrets could be stunned,
knocked back and pulled. They now push the same immunities the live script does. Check: stun and knock back a
Crux Prime auto turret: nothing happens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:27:48 -05:00
Aaron Kimbrell
f35862b596 feat(scripts): the Assembly Engineer turret works as TURRET.lua
The turret (LOT 9303-9305, 12618) had no script: it fought as soon as it was spawned and lasted the generic 60
seconds. As TURRET.lua: its combat AI is off until it is built, it can't be stunned, interrupted, knocked back or
pulled, and it dies 30 seconds after it was placed once nobody is building it. Kill credit going to the builder is
not done. Check: summon the turret with Engineer gear: it doesn't shoot until built, then fights and goes away
after about 30 seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:25:37 -05:00
Aaron Kimbrell
e0169a0a63 docs: issue tracker: issue 1113
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:22:59 -05:00
Aaron Kimbrell
9ee1431aab feat(master): say which folder setting is wrong at startup (issue 1113)
Master stops with a message naming client_location, its value and the folder it resolves to when that folder
doesn't exist, instead of failing later on missing client files; a missing dump_folder gets a warning. Check: set
client_location to a folder that doesn't exist: master says so and stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:22:59 -05:00
Aaron Kimbrell
495d62e559 docs: issue tracker: issue 928
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:22:01 -05:00
Aaron Kimbrell
6335c87f56 docs: CMake from Kitware's repository without apt-key (issue 928)
The WSL steps used software-properties-common, lsb-release and the deprecated apt-key. They now follow Kitware's
current instructions: the key as a keyring referenced with signed-by, the release codename from /etc/os-release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:21:55 -05:00
Aaron Kimbrell
9e5a4b2ba0 docs: issue tracker: issue 1256
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:19:57 -05:00
Aaron Kimbrell
c120be0bcc fix(scripts): concert instruments place the player as live did (issue 1256)
RepositionPlayer added the offsets in world X/Z, set the height to 0 and built the keyboard turn around the position
vector. As l_ns_concert_instrument_qb.lua: the offset is in the instrument's own frame, the instrument's height is
kept, the player faces the instrument's way, and at the keyboard turns -0.8 further. Check: build and play all four
instruments on the Nimbus Station stage: the player stands at each one facing it, not below the stage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:19:57 -05:00
Aaron Kimbrell
dceec20ac2 docs: issue tracker: issue 957
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:18:53 -05:00
Aaron Kimbrell
57925d1e11 feat(moderation): a name decision shows as a popup to the player, as live did (issue 957)
When staff approve or reject a character name while the player is online, the player gets a private announcement
popup ("Name approved" / "Name not approved") as well as the chat line. Players who are offline still aren't told
at their next login. Check: approve and reject a pending name while its player is online.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:18:53 -05:00
Aaron Kimbrell
826dd9ee59 fix(scripts): Gnarled Forest tiki torches lock while in use, cooldown from the animation
The port turned the in-use flag off on a second use instead of refusing it, so a torch could be used again at
once; it now stays in use until the interact animation is done, as L_GF_TORCH does, and the cooldown is that
animation's length (4 seconds when it has none) instead of a flat 4. Check: use a tiki torch twice quickly: the
second use does nothing until the torch is done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:18:04 -05:00
Aaron Kimbrell
8bf01a6c43 fix(scripts): the water gun blows Forbidden Valley candles out
L_ACT_CANDLE also blows a candle out on the "waterspray" skill event; the port only did it on a hit. The 169
candles in Forbidden Valley now go out (and count for missions 850, 1431, 1529, 1566 and 1603) when sprayed. Check:
spray a candle with the water gun: it goes out, smokes, relights after 5 seconds, and the candle missions count it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:15:32 -05:00
Aaron Kimbrell
f7d21b22c2 docs: issue tracker: issue 611
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:12:32 -05:00
Aaron Kimbrell
154ff1f16f fix(minimap): venture vision shows again after logging in (issue 611)
Venture vision from an equipped item was sent while the character loaded, before the client's UI existed, so after a
login the minimap stayed empty until the item was equipped again (a world transfer keeps the UI, so it worked
there). The player's active venture vision effects are sent again once the player has loaded. Check: wear the
Venture Vision helmet, log out and back in: the minimap shows the icons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:12:26 -05:00
Aaron Kimbrell
c75205a0e2 fix(enemies): spiderlings can't be stunned, interrupted, knocked back or pulled
L_BASE_ENEMY_SPIDERLING was excluded, so the 7 spiderlings in Avant Gardens and 3 in Venture Explorer had none of the
immunities its onStartup pushes: stun immunity to attack, interrupt, move, turn, use item, equip and interact, and
status immunity to pull-to-point and knockback. BaseEnemySpiderling does the same. Check: stun and knock back a
spiderling: nothing happens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:11:18 -05:00
Aaron Kimbrell
33402425e4 feat(dashboard): pending guild names count on the Review Queue and Guilds badges
The dashboard snapshot counts guilds whose name waits for moderation; the moderation counts (WebSocket and
/api/moderation/counts) include it, the Review Queue badge adds it for staff with guilds_manage, and the Guilds menu
entry has its own badge. Check: create a guild with a name off the allow list: both badges go up; approve it: they
go down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:46:34 -05:00
Aaron Kimbrell
24d96f13df docs: issue tracker commit hashes as they are on the branch now
The table named commits by their hashes from before earlier rebases; each is now the current hash of the commit
with the same subject.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:41:32 -05:00
Aaron Kimbrell
3a14dc4c9f chore: stop tracking Python bytecode
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:39:37 -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
eb06bc86e0 perf(terrain): read a chunk's heights and mesh lists in one read each
The heights of a terrain chunk (millions of floats in the big zones)
and the mesh index lists were read one value at a time; they are now
read in one read each, as the client does (ReadBytesTimes4 /
RAWReadU16 with a count). The values are the same.

Check in game: nothing (terrain reads the same, only faster).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:39:37 -05:00
Aaron Kimbrell
21f1f3f8e1 fix(zone): zones older than version 30 have no terrain scene map
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>
2026-09-29 08:39:37 -05:00
Aaron Kimbrell
93c7b1948f fix(terrain): read terrain files before version 32 as the client does
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>
2026-09-29 08:39:37 -05:00
Aaron Kimbrell
71b7f19ec8 fix(level): scene objects of LOT 1 are not loaded, as in the client
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>
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
00b62edfa0 fix(guilds): the Guild Master speaks in local chat so its bubble shows
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>
2026-09-29 08:32:02 -05:00
Aaron Kimbrell
553c024bee feat(guilds): the Guild Master says it in a chat bubble over its head
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>
2026-09-29 08:25:53 -05:00
Aaron Kimbrell
d48f43b579 feat(guilds): the Guild Master tells a player already in a guild so
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>
2026-09-29 08:24:35 -05:00
Aaron Kimbrell
cf6c5280ed fix(guilds): guild chat keeps its terminating zero
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>
2026-09-29 07:52:50 -05:00
Aaron Kimbrell
7066860067 fix(dashboard): System Log groups world server logs by zone, not by clone and instance
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>
2026-09-29 07:30:58 -05:00
Aaron Kimbrell
4789711f82 docs(guilds): the login response's events turn guilds on
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>
2026-09-29 06:56:05 -05:00
Aaron Kimbrell
4bdaeb8d3c docs: how DLU does guilds; guild commands and the Guilds page
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>
2026-09-29 06:48:40 -05:00
Aaron Kimbrell
431eddd5bd feat(dashboard): guilds page, guild name review and guild chat in the chat log
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>
2026-09-29 06:48:40 -05:00
Aaron Kimbrell
f6f937955e feat(world): guilds in the world: create box, character component, guild commands
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>
2026-09-29 06:48:40 -05:00
Aaron Kimbrell
d1eec7dacb feat(chat): the chat server handles guilds
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>
2026-09-29 06:48:40 -05:00
Aaron Kimbrell
e49c22374b feat(chat): GuildManager, the guild rules of the chat server
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>
2026-09-29 06:48:40 -05:00
Aaron Kimbrell
130f3566cc wire: guild packets and DisplayGuildCreateBox as structs
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>
2026-09-29 06:48:40 -05:00
Aaron Kimbrell
88ebdf685e feat(db): guilds, guild_members, guild_invites and guild_events
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>
2026-09-29 06:48:40 -05:00
Aaron Kimbrell
c99e2a719f docs: guilds in the 1.10.64 client (RE)
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>
2026-09-29 06:48:40 -05:00