Commit Graph

2248 Commits

Author SHA1 Message Date
Aaron Kimbrell
cba2a36dc4 docs: when UpdatePlayerStatistic is sent
The per-statistic triggers and amounts DLU now follows, with what the
captures showed and what is inferred (DistanceDriven's interval).

Check in game: nothing (documentation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
eaba3d8459 feat(inventory): equip and unequip like live
Live, on every equip and unequip (captures: 22,422 ChangeObjectWorldState,
4,269 equip effects, 245 UncastSkill):

- ChangeObjectWorldState on the item to everyone: ATTACHED when equipped,
  INVENTORY when unequipped, proxies (sub-items) included.
- The equip-<slot> / unequip-<slot> effect on the wearer to everyone
  (effect -1, priority 1.07), the slot picked from the item's equip location
  as the client's ItemComponent does (0x00cdafb0): special_l left,
  special_r right, hair head, clavicle, chest, legs; none for others (e.g. a
  rocket's Extra_1); the item's equipEffects with "equip"/"unequip" when it
  has one; no equip effect for noEquipAnimation items, no unequip effect for
  proxies.
- Order when an item replaces another: the new item's state and effect,
  then the old item's, then the old item's proxies taken away
  (RemoveItemFromInventory, UnEquipInventory with ignoreCooldown,
  ChangeObjectWorldState INVENTORY, to the player), then UncastSkill for
  the old item's equip skills (castOnType 1, not its proxies'), then its
  skill removed and the new one added, then the new item's proxies.
- An equipped item taken out of the inventory is followed by
  UnEquipInventory (ignoreCooldown) to the player.

DLU sent none of these (only the rocket's ChangeObjectWorldState, which
now comes from equipping it, before RocketEquipped as live).

Check in game (with a second player watching): equip and unequip a hat, a
sword, a shield, a shirt, pants and a backpack; both players see the equip
animation/effect. Put a ninja hood on, then another hat over it: the hood's
back piece disappears and its passive effects end (no lingering aura or
stat). Launch a rocket: it is still shown during the launch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
b845faee72 fix(messages): ChangeObjectWorldState and UnEquipInventory as the client reads them; add UncastSkill
ChangeObjectWorldState's state is optional on the wire (a flag, then the
state when it isn't INWORLD): live sent 80 80 00 00 00 for ATTACHED. DLU
wrote the bare u32, so the client read the rocket's ATTACHED as INWORLD.

UnEquipInventory gets its optional replacementObjectID (the client always
sends the flag; the server-sent copies in the captures have it too), so it
can be sent server -> client.

New UncastSkill (1206, server -> client: the client ends its running
instance of the skill, LWOSkillComponent::msgUncastSkill 0x00bd86d0).

Tests: all three against live capture bytes.

Check in game: launch a rocket; the rocket is shown in the player's hands
during the launch animation (for the launcher and players nearby).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
aed16a8c80 fix(stats): RocketsUsed and QuickBuildsCompleted where live sent them
RocketsUsed: live counted the rocket when the client asked to go
(FireEventServerSide "ZonePlayer"), right before TransferToZone (captures:
94 of 117 directly before it). DLU counted it when the launch animation
started, so a launch that never went counted too.

QuickBuildsCompleted: live sent it after RebuildNotifyState(Completed) and
the completion effect (507), before EnableRebuild (218 of 227 samples); DLU
counted it before the notify state.

Check in game: launch a rocket to another world; Rockets Used goes up by 1
as the screen fades out. Finish a quick build: Quick Builds Completed goes
up by 1 as the build finishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
fd10e19a06 feat(stats): heals, repairs and restores count what they applied
Live sent TotalArmorRepaired / TotalImaginationRestored with what a repair
or restore applied, 0 included (1,050 and 2,148 zeros in the captures: a
power-up picked up at full), and never a TotalDamageHealed or
TotalDamageTaken of 0. DLU counted every change of the value instead, so
setting stats up (loading, respawning, level changes) counted as healing
and a heal at full health counted as 0 damage taken.

Now Heal, Repair and Imagine count what they applied; health lost and
imagination spent still count wherever they change.

Check in game: pick up an armor or imagination power-up when full; the
passport's Armor Repaired / Imagination Restored stay the same. Take damage
and heal; Damage Taken and Damage Healed go up by what the bar shows.
Respawn: Damage Healed doesn't jump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
1f29e9604a feat(stats): coins, bricks and smashes counted by the server as live did
The passport totals for coins, bricks and enemies came from the client's
ModifyPlayerZoneStatistic, which live's client sent for its per-zone counts;
live's server counted the totals itself and told the client
(UpdatePlayerStatistic). Now:

- CurrencyCollected (the amount gained) right after every SetCurrency that
  raised the coins (pickups, missions, achievements, selling, activities);
  none for losses. Captures: 15,815 pickups + mission/achievement/vendor/
  activity gains, all with the stat; no loss had one.
- BricksCollected (the count) after every add to the bricks inventory the
  client is told about (captures: 999 of 1,010, pickups and relocations).
- The kill counts go to the killer right after Die, before the loot:
  EnemiesSmashed when the DestructibleComponent's isnpc is set, else
  SmashablesSmashed for a smashable (captures: 2,766 NPC kills and 538
  smashables; faction or AI don't decide it, e.g. the Banana Cluster has AI
  and counted as a smashable). Neither while racing. Before, every kill was
  a SmashablesSmashed.
- ModifyPlayerZoneStatistic from the client only updates the zone counts,
  so nothing is counted twice.

Check in game: pick up coins, sell an item, finish a mission, pick up
bricks, smash a stromling and a crate; each passport number goes up by the
right amount at once and stays the same after relogging (no double counts).
The per-zone statistics still go up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
752f079100 feat(stats): send UpdatePlayerStatistic to the player as live did
Live sent UpdatePlayerStatistic (1481) server -> client for every statistic
the server counted (59,933 messages in the captures, none client -> server);
DLU only counted them, so the passport stayed stale until the next login.
CharacterComponent::UpdatePlayerStatistic now tells the player's client what
it counts (not what a client reported), with the amount left out when it is
1, byte for byte as the live samples.

MetersTraveled: whole meters now count (each position update's distance was
cut to a whole number before, so short steps never counted) and go to the
client once 25 have gathered (live's values were nearly all 25-33 while
moving) and the rest, 0 included, when the player is taken down on leaving
the world (live sent one more after TRANSFER_TO_WORLD). DistanceDriven goes
out every 10 seconds of racing (inferred: live sent it every few hundred
packets, about 1300 units at race speed).

Check in game: open the passport's statistics page, walk around, collect a
power-up, complete a mission; the numbers go up without relogging. Walk
25+ meters; Meters Traveled matches what the passport shows after
relogging. Change worlds and relog: the meters were kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -05:00
Aaron Kimbrell
5b1be32e06 feat(ghosting): scene ghosting on by default
ghosting_scenes defaults to 1 in the worldconfig.ini template, the settings
catalog and when the key is missing. Zones without a terrain scene map keep
distance ghosting; ghosting_scenes=0 turns it off everywhere.

Check in game: with a fresh worldconfig.ini the world server log says
"Scene ghosting is on" in Avant Gardens; objects appear by scene (the whole
scene you are in and its neighbours) instead of within 100-150 units. In a
zone without a scene map (e.g. a property) the log says it is off and
ghosting is by distance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:42:02 -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
cde8ea4979 docs: goTo, reloadInPlace and dash:refreshed for page scripts
Check: docs/Dashboard.md, Live updates: the page-scripts paragraph names
goTo(url) and reloadInPlace() and says when dash:refreshed is used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:12:14 -05:00
Aaron Kimbrell
2629b41e6c feat(dashboard): the top bar shows the server name while the menu is hidden
With the menu hidden on a wide screen the page had no name or link
home left; the top bar now shows "DarkflameServer" (linking home) then,
as it already does on narrow screens.

Check: hide the menu with the top bar's menu button on a wide window:
"DarkflameServer" appears beside it and opens Home; showing the menu
again hides it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:12:14 -05:00
Aaron Kimbrell
1c1bf38431 fix(dashboard): account history reloads without a script error
The account page throttled its account_notes watcher around the check
of which account changed, but a throttled call gets no event, so every
change threw "Cannot read properties of undefined (reading 'id')" and
the history never reloaded. The check now runs first and only the
reload is throttled.

Check: lock or unlock an account (or add a note from another tab): the
history list on its page updates, and the browser console shows no
error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:12:14 -05:00
Aaron Kimbrell
c70b163701 feat(dashboard): account and character pages update without reloading
Actions on an account (ban, mute, lock, GM level, email) and on a
character (edit, restoring a version, saving its XML) show the result
in place (reloadInPlace) instead of reloading the page. Deleting an
account goes back to Accounts without leaving the deleted account in
the history. After an in-place update (dash:refreshed) the pages show
the server's values again the way they do on load: last logins and the
mute end in the browser's time, and the GM level picker (unless a new
level is being picked).

Check: lock and unlock an account: the badge changes with no reload.
Change an account's GM level from another tab: the first tab's level
picker follows. Edit a character's items: the page updates when the
dialog closes. A character's last login updates when they log in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:12:13 -05:00
Aaron Kimbrell
172706ee51 feat(dashboard): bug report, play key and system log pages update without reloading
reloadInPlace() and goTo(url) (common.js) show the page again or open
another one through nav.js, or reload/load normally without it.
Resolving a bug report updates the page in place; deleting a bug
report or play key goes back to its list without leaving the deleted
page in the history. The System Log's server and file pickers and its
Refresh button swap the page in (Refresh keeps the scroll position).
The AI helper's "open the account" goes through nav.js too.

Check: resolve a bug report: the Resolved badge and text appear with no
reload. Delete a play key: back on Play Keys, and Back doesn't return
to the deleted key. System Log: picking a server or file and Refresh
change the log without a white flash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:12:13 -05:00
Aaron Kimbrell
b66bae6a9c feat(dashboard): pages about one row update in place instead of reloading
Nav.refresh() fetches the current page and compares the server's HTML
now with the HTML the page started from; only what differs is patched
into the live page (text, attributes, class changes, rows added or
taken away). Typed input stays (the browser keeps what the user
changed while the default value follows the server), and parts built
by the page's scripts are left alone. Where a patch would lose
script-built or script-bound parts, or the page's layout changed, the
page is swapped in again with its scripts, keeping the scroll
position, open tabs, open folding parts and typed input; while the
user is typing it offers a refresh instead.

Live.refreshPage (account, character, bug report and play key pages)
uses it instead of reloading the page, and the "This changed while you
were editing" banner's Refresh does too.

Check: open a play key; in another tab change its uses or active
switch: the first tab's values and badge change without a reload,
and notes typed there stay. Same on a bug report page with a half
typed resolution when it's resolved elsewhere. On a character page,
an in-game save updates it without losing the open tab or scroll.
Test: NavRulesJs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:12:13 -05:00
Aaron Kimbrell
f58255ab8a feat(dashboard): change pages without reloading the dashboard
nav.js (loaded on every signed-in page) follows links and GET forms to
other dashboard pages by fetching the page and swapping in its <main>,
title, page styles and page scripts; the sidebar, top bar and live
WebSocket stay. History entries, back/forward (with scroll positions,
also kept for a reload), deep links, #hash filters and breadcrumbs keep
working. What the old page set up is taken down first: its
document/window listeners, setInterval timers, Live watchers and
topics, DataTables, dialogs and what it appended to <body>. Page
scripts run again in order; DOMContentLoaded/load handlers they add run
once they have all run. A thin bar shows while loading; a failed fetch
shows an inline error with Try again / Open it normally.

Falls back to a normal load for anything that isn't a signed-in
dashboard page, when the account or permissions changed, and for pages
with module scripts or an import map (World 3D, property view and 3D,
UGC server) or data-nav="reload", and when leaving those. Unsaved-change
prompts (beforeunload) are asked before swapping.

Check: click around the sidebar and into accounts/characters: no white
flash, the footer's live indicator stays "live", back/forward return to
the same scroll position, breadcrumbs follow the path (Accounts > an
account > a character). Activity Log links with #search= still filter.
System Log's server picker works. Leaving Settings with a change asks
first. World 3D and a property's 3D view still load (normally). Pages
that poll (Server Health, Instance Load) stop polling once left
(browser network tab). Test: NavRulesJs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:12:13 -05:00
Aaron Kimbrell
f13d15b4dd feat(dashboard): the sidebar keeps its open groups and can be hidden
The menu's groups stay open or closed from page to page
(localStorage "dash.sidebar", this browser only), applied by an inline
script right after the menu so the first paint already shows them. The
group of the current page opens and stays open until it is closed. A
new top bar button hides the menu on wide screens; that choice is
applied in <head>, also before the first paint.

Check: open and close a few menu groups, switch pages and reload; the
groups stay as left, with no flicker. On a wide window the menu button
left of the user menu hides the menu, and it stays hidden across pages
and reloads. Narrow screens: the Menu button still slides the menu out.
Test: SidebarStateJs (ctest, needs node).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:12:13 -05:00
Aaron Kimbrell
da90a36be6 docs: permission grants
Dashboard.md: what a grant and a deny are, how they combine with GM levels, what can't be granted, who may manage
them (grants_manage, nothing you don't hold, the rank rules), where they are managed, live changes, audit log entries
and the API. Commands.md: how grants and denies apply to slash commands.

Check: the new sections read correctly and the link from Commands.md to Dashboard.md#permission-grants works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:16:50 -05:00
Aaron Kimbrell
641fd72969 feat: online players get grant changes at once
Adding or removing a grant sends the existing PLAYER_ACTION refresh through master to every world: REFRESH_ACCOUNT
for an account's grants, REFRESH_CHARACTER for a character's. A world with that account's sessions (or the loaded
character) drops the grants it kept, so the next command reads them again: no relog. The dashboard toasts whether the
player was online. The dashboard's own open pages already get the new rights on their next request, and their
WebSockets are checked again.

Check: with a character in game, grant it /spawn on the dashboard (toast: "Applied in game at once") and use /spawn
without relogging; remove the grant and /spawn is refused again at once. With the player offline the toast says it
applies when they next play.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:16:50 -05:00
Aaron Kimbrell
e00d22152e feat(game): slash commands accept permission grants
A command may be used when the character's GM level allows it or a grant does: a grant of the command, of every
command up to a GM level, or of the dashboard permission the command follows (so accounts_kick covers /kick). A deny
of any of those takes it away even when the level allows it, except from GM 9 accounts (also while they play at a
lower level). Grants never take anyone below a command's floor above GM 1 (/execute), and commands the client handles
keep their fixed level. Expired grants count for nothing. The grants are the account's and the logged-in
character's, read from the database the first time a command is used and kept on the User. /help lists the commands
a player may use this way. The self and rank rules for commands that act on another player count grants of self_* and
manage_equal_rank too. A denied command says it was taken away.

Check: grant a GM 0 account /spawn (or "every command up to GM 8") on the dashboard, relog, and /spawn works and
shows in /help; deny /spawn from a GM 8 character: "it was taken away"; grant accounts_kick and /kick works (the rank
rules still apply to whom). dGameTests SlashCommandGrantsTest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:16:49 -05:00
Aaron Kimbrell
868f34123f feat(dashboard): grants on the Permissions page and on account and character pages
A Grants tab on the Permissions page (with grants_manage: every grant in force, the history, and a form that asks
whose it is, searching accounts or characters by name), and a Permission grants card on account and character pages
(the account's or character's grants and history; the form with grants_manage). The form picks the kind (dashboard
permission, in-game command, every permission of a category, every command up to a GM level) and searches what to
grant among only what the signed-in user may grant; grant or deny, an optional expiry and a note. In-force grants have
a Remove button when the user may remove them. Players see their own grants, read-only. The Permissions page (and its
menu entry) now opens with permissions_manage or grants_manage; the GM level tabs still need permissions_manage.

Check: as GM 9, add a grant and a deny from the Permissions page and from an account and a character page, with and
without an expiry; remove one; the lists and history update (also in a second tab). As a GM 8 given grants_manage:
only the Grants tab shows, and only permissions and commands GM 8 has are offered. As a player: your own account page
lists your grants without a form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:16:49 -05:00
Aaron Kimbrell
821b7c8767 feat(dashboard): permission grants count in every dashboard permission check
What someone may do on the dashboard is now their GM level's permissions plus the grants on their account, minus its
denies (PermissionGrants.h). A deny beats a grant; denies never apply to GM 9, and settings and permissions_manage stay
GM 9 only. The account's grants are read with every request (like its GM level), so a change applies at once, and
they are passed through every check: RouteUtils::Can, CanViewCharacter, the rank rules (self_* and manage_equal_rank),
routes guarded by a permission, the templates' `can`, the API documentation, API access, API key scopes (a key never
does more than its owner may now) and WebSocket subscriptions.

New permission grants_manage (GM 9 by default) and the API to manage grants: GET /api/grants/catalog, GET /api/grants,
POST /api/grants, POST /api/grants/:id/remove. Nobody grants or takes away what they don't hold themselves (a
permission, every permission of a group, a command they may use, every command up to their own GM level), and only on
accounts the rank rules let them manage (their own with self_moderation). Commands with a fixed level or a floor
above GM 1 (/execute) can't be granted. Every change goes in the audit log (grant_permission, deny_permission,
remove_grant). Also: the Showcase gate and the traffic subscription now check their permission by name.

Check: grant a GM 2 account accounts_ban (it can ban, and the Ban button shows); deny a GM 8 account accounts_view (the
accounts list is refused); give an expiry a minute ahead and see it stop; try to grant a permission your account
doesn't have (refused); dWebTests PermissionGrantsTests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:16:38 -05:00
Aaron Kimbrell
c575eba4e7 feat(db): permission_grants, permissions and commands given to one account or character
A new table, permission_grants (MySQL migration 96, SQLite 79), and the IPermissionGrants interface with MySQL, SQLite
and TestSQL implementations. A row grants (or with deny, takes away) a dashboard permission, a slash command, a
permission category or every command up to a GM level, for one account or one character, with an optional expiry, who
granted it and when, and a note. Rows are never deleted: removing one sets revoked_at/revoked_by, so the table is also
the history. Nothing reads it yet.

Check: both migrations run on a fresh and an existing database; DatabaseParityTests PermissionGrants passes against
MariaDB (DLU_TEST_MYSQL_HOST) and SQLite gives the same results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:16:37 -05:00
Aaron Kimbrell
dc1c5fb36b feat(dashboard): redesign the Chat Filter page
Three sections with one-line explanations instead of paragraphs:
- Test a message: normal or best friends' free chat, the verdict, each
  word with why, and Block/Allow/Remove per word.
- Staff lists: one table for Blocked and Allowed with search, a list
  filter and 50 per page. Block, Allow, move and Remove open a
  confirmation showing where the word stands and the recent chat the
  change affects (the old "What would blocking it stop?" checks).
- Word files: each file's size in one line; chatplus_en_us.txt words
  searchable, 200 per page with Previous/Next; click a word to test it.
All existing APIs are unchanged and still used. docs/Dashboard.md
rewritten for the page.

Check: /chat_filter: test a message in both chats, add, move and remove
a word (confirm and cancel), search and page both lists, copy file
words into Allowed, Re-apply in running worlds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:59:50 -05:00
Aaron Kimbrell
29edb18298 feat(dashboard): test a message against the chat filter
GET /api/chat_filter/test?message=&chat=normal|free says whether the
filter would stop a message from a player below GM 2, word by word and
why: blocked or allowed on the staff lists, in chatplus_en_us.txt, an
approved character name, not allowed, in blocklist.dcf, or no
blocklist.dcf (free chat then stops everything). It follows
dChatFilter::IsSentenceOkay: words split at spaces, normalized the same
way. ModerationTools::ExplainMessage, unit tested.

Check: dWebTests ChatFilterWordsTest.Explain*; the API with a word from
each list, in normal and free chat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:59:50 -05:00
Aaron Kimbrell
9a5a9a274f feat(dashboard): Mail page with every mail, translated text and deleted mail
New Mail page (/mail, characters_mail, under Moderation): every in-game
mail newest first and live, with sender and receiver linked to their
character and account, the attachment with its icon and name (waiting
or claimed), and the state (unread, read, deleted by the player with
the time). Filters: state, character (sent or received), account,
text. Open shows the body, the attachment's item ID, subkey and data.

Locale keys in mail (%[MissionEmail_12_subjectText], the game's sender
name) are shown as the client shows them, from locale.xml; the stored
text stays under "Stored text". LocaleText::Expand, unit tested.

The character Mailbox uses the same view. Staff see mail the player
deleted (marked) and a link to the character's mail on the Mail page;
the owner sees only what is still in the mailbox, without account IDs.
/api/characters/:id/mail keeps its old fields and adds the new ones.

Check: /mail with each filter, Open on game, staff and player mail,
a mission mail's subject translated; a character's Mailbox as staff
and as the owner after deleting a mail in game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:59:50 -05:00
Aaron Kimbrell
180585f9c6 feat(mail): mail history query for the dashboard
IMail::GetMailHistory and CountMailHistory return mail rows as stored,
deleted mail included, newest first, with the sender's and receiver's
accounts. Filters: character (sent or received), account, text in the
subject, body or a name (LIKE wildcards matched literally), state
(unread, read, attachment waiting, claimed, deleted) and whether to
include deleted mail. MySQL and SQLite share the SQL (MailSql.h).

Check: parity test ParitySeeded.Mail (GetMailHistory).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:59:49 -05:00
Aaron Kimbrell
acb0e03a74 feat(mail): keep mail a player deletes, marked deleted, for staff
Deleting mail in game now sets mail.deleted_at (migrations mysql 95,
sqlite 78) instead of removing the row. Every read for the game skips
deleted mail: the mailbox, a single mail (claim, read, delete), the
unread count, the economy scan of waiting attachments and the UGC
lookup of mailed models. Deleting a character still removes its mail.

Check: delete a mail in game; it leaves the mailbox, the unread count
drops, and the row is still in the mail table with deleted_at set.
Parity test: ParitySeeded.Mail (DeleteMail).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:59:49 -05:00
Aaron Kimbrell
e42fd45c1f feat(dashboard): glitter sparkles and flecks in the UGC 3D views
The UGC page's model view now draws the glitter sparkle shapes (look
SPARKLE) flashing as the client's Distortion Directional shader makes
them: two layers of sparkles sliding at the client's rates (a tile in
24 s at three quarters the scale, a tile in 48 s), shown only where both
have one. Flecks are drawn as flat flakes of the fleck size and varied
brightness (addGlitter in scenery-core.js), no longer drifting (the game
never moves them). The LXFML views (the UGC page's second view, the
property and zone views) draw both on the glitter colors from
window.LDD_GLITTER, which now carries every glitter setting and is no
longer cached, so changing a glitter setting on the settings page
previews on any model's LXFML view without making it again.

Check in the dashboard (not the game): open a glitter model on the UGC
page; both views show still flecks and sparkles flashing on the glitter
bricks only; change glitter_sparkle_amount / glitter_fleck_size in the
settings and reopen the model: the LXFML view follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:40:11 -05:00
Aaron Kimbrell
aab161038e fix(ugc): glitter flecks look like LEGO glitter
The fleck texture was 50 soft round blobs of one size, bright in the
middle and fading out, which read as a smudgy dot pattern. LEGO's glitter
bricks have many small flat flakes (about half a millimetre) of which
most look faint and a few catch the light. The flecks are now flat
flakes of glitter_fleck_size (0.05 model units, i.e. 0.5 mm; 0.7 to 1.3
of it) with a one-pixel edge, each as bright as its facet catches the
light (0.3 to 1 of glitter_fleck_opacity, 80%, weighted towards dim),
80 a tile by default. The texture grows (128 to 512) to keep a fleck 3
pixels wide. New settings glitter_fleck_size and glitter_fleck_opacity;
glitter_density defaults to 80. Only glitter output changes.

Check in game: reprocess a glitter model; close up, the flecks are small
crisp flakes of varied brightness, not blurry dots; from a few metres
the brick still reads as its color with a fine glitter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:40:10 -05:00
Aaron Kimbrell
fb7850fb9f fix(ugc): glitter sparkles that move on placed models
Why the glitter never moved: player models (LOT 14) are wrapped in
weeblewobble.kfm (RenderComponentWrapper 9845), so the client makes them
an LWOSkinnedRenderComponent, whose Run (0x00d6d3d0) updates the scene
graph (and so any NiTextureTransformController) only while animation is
enabled, and LWOModelBehaviorComponent::EnableAnimation (0x00be2740)
turns it off for modelType 2, which every placed property model is. The
root flags 0x102 added earlier are only read by the base render
component. Nothing in a placed model's .nif can move.

What does move: shader classes set globals in their own per-frame Run.
Distortion Directional (Ocean) (mapShaders 79, Run 0x010b90c0) slides
its texture layers by fixed shares of a tile a second, as the game's own
pond ripples (S79__pond_ripplesShape). Glitter bricks now get a sparkle
group, S79_GlitterSparkle_Model: their triangles lifted 0.005 off the
brick, vertex colors white tinted by the brick, UVs placed per brick,
alpha tested (ShaderCommon's alpha test phase, GREATEREQUAL 127), with a
stored texture of flat sparkles at alpha 230: one layer's sparkle alone
averages under the test, two meeting pass, so sparkles flash and go out
as the layers cross. The flecks stay (LEGO-AnimUV, now without the
controllers and flags that never ran). The icon and the dashboard's 3D
view leave the sparkles out.

New settings: shader_glitter_sparkle (79, 0 off), glitter_sparkle_size,
glitter_sparkle_amount, glitter_sparkle_tint, glitter_sparkle_brightness;
glitter_speed is now how fast sparkles flash (the sparkle tile). Only
glitter output changes; non-glitter models are byte-identical.

Check in game: reprocess a property with glitter models, then look at
them from a few angles and distances, on each graphics quality:
- sparkles flash on and off all over the glitter bricks, continuously
- no flickering fight between the sparkles and the brick surface
- transparent glitter bricks still see-through, flecks still visible
- nothing drawn where there is no glitter brick; icons unchanged
  apart from the flecks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:40:10 -05:00
Aaron Kimbrell
63bfaf545e fix(ugc): each glitter brick gets its own fleck pattern
Every glitter brick had the same flecks in the same places: the UVs were
the vertex positions projected on an axis plane, so bricks a whole tile
apart (and every brick of the same shape at the same spot in its own
model) looked identical. Each brick now has a number of its own
(UgcGlitter::BrickSeed, from the model's id and the brick's index, kept
per vertex in Mesh::brickSeeds) that turns the projection by an angle
and moves it by an offset under a tile, differently for each axis
plane. A model made again gets the same patterns; every LOD of a brick
the same one. The icon now draws the flecks on the UVs the .nif has
(Mesh::uvs read back by FromNif). New setting glitter_random (1; 0 puts
the same pattern on every brick as before). Non-glitter models are
byte-identical (hash tests unchanged).

Check in game: reprocess a model with several glitter bricks of the
same shape; the fleck patterns differ from brick to brick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:40:09 -05:00
Aaron Kimbrell
0a1e33d3d2 refactor(dashboard): reduce the CDClient browser to a raw table viewer
Drops the analysis views (objects, loot odds, missions, skills, behavior
trees, activities, zones), the name search and the 3D preview, with
their API routes and CDClientRules.h. What stays: the table list,
paging, sort, any-column search, column filters, and linked values that
open the target table filtered to that ID. Old links such as
/cdclient#/object/<LOT> (UGC page, world view) now open the Objects
table filtered to that LOT.

Check in the dashboard: CDClient Browser lists every table; open one,
sort by a column, add a filter, search, page; click a LOT or loot
matrix value; /cdclient#/object/1727 opens Objects id = 1727.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:21:18 -05:00
Aaron Kimbrell
266f8b81c5 fix(ugc): send the served mesh checksums when a player loads a property
A client asks for a served model's HKX and is answered with the model's
LXFML, so it builds its own collision. That build also writes the
client's own .nif over the served one it downloaded and caches that
file's MD5 as the NIF's manifest info (client 1.10.64:
LWOBBBInterface::GenerateModelFromLxfml 0x00b6c220, MainThread_Process-
ModelResponse 0x00b5a1e0; valid entries never expire, 0x010186d0). On the
next load of the property the client used its cached checksum and file
without asking, and drew its own build instead of the served mesh.

Now, before the property's objects are constructed for a player, the
world sends them the served NIF checksum of every made model placed there
(UgcManifest::OnPropertyLoading). A client whose cached checksum is its
own build's downloads the served mesh again; one that has it already
downloads nothing. Nothing is sent unless ugc_manifest and
ugc_manifest_models are 1.

Check in game (ugc_manifest=1, ugc_manifest_models=1): visit a property
with made models, leave, come back in the same session (and after a
client restart): the models show the served mesh every time, and still
have collision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:05:42 -05:00
Aaron Kimbrell
bd1d76193f fix(ugc): switch a client to a served mesh only once its own build is done
When a model finishes on the UGC server while players are on the property,
each client was sent the served NIF checksum, NotifyClientUGCModelReady and
the model constructed again at once. A client that was sent the model's
LXFML shortly before (the property load, a request for a model not made
yet, the owner's brick by brick save) is still building it then: the build
writes its own .nif over BrickModels/UserMade/<id>.nif and caches that
file's MD5 as the NIF's manifest info when it finishes (client 1.10.64:
LWOBBBInterface::GenerateModelFromLxfml 0x00b6c220, MainThread_Process-
ModelResponse 0x00b5a1e0), undoing the switch, so it kept its own build.
That is the usual case: another player's client asks for a model the owner
just placed, gets its LXFML, and that request makes the UGC server make the
model at once.

Now such a client is switched 30 seconds after the last LXFML sent to it
(UgcManifest::ServedMeshSwitches, BUILD_SETTLE); another LXFML sent
meanwhile starts the wait again. Other clients are switched at once, as
before. The served checksum is looked up when the switch happens. HKX
requests are still answered with the LXFML, so collision stays the
client's own.

Check in game (ugc_manifest=1, ugc_manifest_models=1, two accounts on one
property): the owner saves a new model; within about 30 s of the UGC
server making it, the other player's copy blinks out and back with the
served mesh (vertex AO, look of the dashboard's viewer), and still has
collision (walk into it). The owner's copy switches the same way. The
world log shows "Switching ... to the served mesh of model ...".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:05:42 -05:00
Aaron Kimbrell
45473f4f72 chore(dashboard): drop the tabs on Server Health, Instance Load and Diagnostics
The three pages had in-page tabs linking to each other, the same links
as the Logs & Health sidebar group. Removed the tabs (health_tabs
template); each page is still in the sidebar. Diagnostics gets an
<h2> heading like the other two, since the tab was its only title. The
log pages had no tabs.

Check: Server Health, Instance Load and Diagnostics open from the
sidebar with no tab row; Diagnostics shows its heading; the range and
server controls still sit on the right.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:44:44 -05:00
Aaron Kimbrell
74056c9ade docs: game messages 716, 1726, 1481 and ignored client messages
Findings from the 1.10.64 client and the live captures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:44:44 -05:00
Aaron Kimbrell
a61a01c4fb test(messages): UpdatePlayerStatistic against live capture packets
Live sent UpdatePlayerStatistic (1481) server to client; the struct
already reads and writes those packets. Documents what the client does
with it. Needs the FromLiveCapture test helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:44:44 -05:00
Aaron Kimbrell
7621a4fcdc feat(messages): RemoveBuffsAppliedByObject struct
Game message 1726 (optional object ID, default 0), as the 1.10.64 client
reads it. Live sent it to every player when an object left the world;
nothing sends it yet. Tested against a live capture packet. Needs the
FromLiveCapture test helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:44:44 -05:00
Aaron Kimbrell
7d28405559 test(messages): DownloadPropertyData against a live capture packet
Adds FromLiveCapture, which reads a whole captured game message packet
into a struct and requires the struct to write the same bytes back, and
checks DownloadPropertyData (716) with an unowned property packet from
the 2011/2012 live captures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:44:44 -05:00
Aaron Kimbrell
a52e777097 fix(messages): client names for game messages 792, 793, 915 and 916
The 1.10.64 client's message table (GameMessage::<Name>::Initialize) names
792 DeletePropertyResponse, 793 CreateModelFromClient, 915
PropertyModerationAction and 916 PropertyModerationActionResponse.
Only the enum names change; the IDs stay pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:44:44 -05:00
Aaron Kimbrell
b0dab03807 chore(dashboard): remove the Maintenance page
The page held one-off repairs from Nexus Dashboard (approve known pet
names, find/delete orphaned pet names, remove every buff, fix property
clone IDs, list mission rewards without a commendation price). Removed
the page, its sidebar entry, its /api/maintenance/* routes, the
`maintenance` permission and the two database calls only it used
(GetAllPetNames, FixPropertyCloneIds). The scheduled tasks (lift expired
bans, fill in pet owners, approve known pet names) and property model
import/removal stay.

Check: the Admin sidebar group has no Maintenance link; /maintenance is
a 404; the Tasks page still lists "Approve known pet names" and "Fill in
pet owners"; property import still works; the Permissions page no
longer lists "Data maintenance".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:44:44 -05:00
Aaron Kimbrell
7440e5744d fix(rails): rider flags default to the RailActivatorComponent row
StartRailMovement's bDamageImmune, bNoAggro and bShowNameBillboard came
only from the level keys rail_activator_damage_immune, rail_no_aggro and
rail_show_name_billboard, which 23 of the 80 level rails do not have, so
those rails sent false. The client reads the same flags from the
RailActivatorComponent row (DamageImmune, NoAggro, ShowNameBillboard; all
11 rows are 1) in LWOPlayerForcedMovementComponent::LoadRailData
(0x00c85dc0), and the message's values replace the row's unless bUseDB
(msgStartRailMovement 0x00ccd9c0). The server now takes the row's value
and lets a level key replace it when the key is there. Wire format is
unchanged; only the flag values sent for the 23 rails change (to true,
matching the 5 live samples).

In game: ride the 23 rails without the keys, all in the Ninjago earth gauntlet
(earthgauntlet levels: dart spinners, 8 spinners, millstones, boss) near enemies: you are not
damaged or targeted while riding, and name billboards behave as on the
other rails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:34:55 -05:00
Aaron Kimbrell
54cd9abd7a feat(loot): smashables use the level's smashable_loot_matrix
A level object's smashable_loot_matrix replaces the DestructibleComponent
LootMatrixIndex when smashable_loot_matrix_set is true or absent and the
matrix is not -1, as the client resolves it
(LWODestroyableComponent::LoadConfigData 0x00c44cb0). DLU only read the
key in BaseInteractDropLootServer, so tagged smashables dropped their
template's loot. Resolution in DestroyableComponent::GetLevelLootMatrix,
with tests. No wire change.

In game: smash level smashables that carry smashable_loot_matrix_set=1
(11 level objects in the 1.10.64 levels) and ones with a matrix but no _set key; they drop the
level's loot instead of the template's. Ordinary smashables
(smashable_loot_matrix_set=0) drop what they did before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:34:35 -05:00
Aaron Kimbrell
721925add0 fix(factions): override_faction 0 keeps the template's factions
The client (LWODestroyableComponent::LoadConfigData 0x00c44cb0) uses a
level object's set_faction only when override_faction is absent or true;
with override_faction=0 LoadDataFromTemplate (0x00c9f900) puts the
DestructibleComponent factionList back. 2948 level objects have
override_faction=0, and DLU applied their set_faction anyway. The
resolution is now DestroyableComponent::GetLevelFactions, with tests.
No wire change.

In game: enemies and smashables placed in levels with a set_faction but
override_faction=0 (most smashables, many enemies) are targeted and
aggro as before for the common case; check a few enemies in AG/GF/FV
still fight you and friendly objects are not attacked, and that
smashables still smash and give credit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:34:22 -05:00
Aaron Kimbrell
107437e72f fix(inventory): a save in build mode keeps the equipment from before it
Entering build mode pushes the equipped items (PushEquippedItems); the
thinking hat and a model picked up off the property are then equipped
until PopEquippedItems puts the old equipment back. A character save in
between (the world's periodic save, a disconnect, a zone change) wrote
what was equipped at that moment, so a carried brick built model (LOT
6662) was saved in MODELS with eq="true" and equipped again on every
load. While the equipment is pushed, the save now writes the pushed
equipment as equipped instead. Nothing is dropped from the save.

In game: on a property, pick up a brick built model and carry it, wait
for a save (or log out) while carrying it, log back in: the model is in
the Models bag, not equipped, and your normal gear is worn. Characters
already saved with an equipped model keep it until it is put away or
the save is cleaned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:33:58 -05:00
Aaron Kimbrell
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
7c32779e89 fix(ugc): transparent bricks drawn see-through in mixed models; dUgcServer in folders by role
- The client puts a shape in its sorted, blended pass only when its
  NiMaterialProperty alpha is under 0.99999 (ShaderCommon::GetAlphaFlags
  0x0109f5a0; the NiAlphaProperty blend flag isn't read); at 1.0 it's drawn
  solid with blending off. Transparent (and transparent glitter) shapes now
  get a material with alpha 0.9999, as the S01_Alpha shapes of the game's own
  brick models (res/BrickModels/ndmade) do; opaque shapes keep 1.0. Models
  with a transparent brick change; the others are byte for byte the same.
- dUgcServer's files move into Bricks/, Model/, Render/, Formats/ and
  Processing/ (the CMakeLists says what each holds); includes are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:26 -05:00
Aaron Kimbrell
2277cbd7e2 fix(ugc): stopping the UGC server cancels the models being made
Stopping waited for every worker to finish its model, and with the path
traced hidden-face removal a big model takes minutes, so a restart hung.
UgcThrottle::Cancel(true) (set by UgcProcessor::Stop) makes Checkpoint,
which the long loops already call, throw UgcThrottle::Cancelled; the worker
abandons the job without writing or recording anything, so its row stays
waiting and is made again after the restart. The throttle's sleeps wake to
check it. Previews answer 503.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:25 -05:00
Aaron Kimbrell
4679edd2b2 feat(ugc): processing totals on the UGC page; no shared icon preset for player models
- IUgc::GetUgcProcessTotals sums what the UGC server made, for models and
  for car and rocket builds: count, time spent (and how many are timed),
  CPU time, average and slowest, average and most memory (estimate),
  bricks, triangles and triangles saved (models whose count before hidden
  face removal is known). /api/ugc returns them as totals and the UGC page
  shows a card for each kind. Parity tested.
- Player models no longer have a shared icon preset: every one is a
  different size and shape, so its icon is fitted to it from the settings.
  The UGC server ignores a kind:model preset, the dashboard refuses to save
  one, and the icon editor hides the type buttons for models. One model's
  own icon values still work. Car and rocket build types keep theirs.

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