The client reads <pet a> as its active pet's database ID with
GetInt64Attribute (LWOPetControlComponent::LoadFromSaveData 0x00c18120);
DLU never wrote it. Live wrote a="0" on every character (226 live
charxmls, 45 with pets): the charxml is only read when the player loads,
before any pet is out, and a pet summoned afterwards registers itself
(RegisterPetDBID). So a is written as 0.
The pet taming type p@t stays 0 as DLU already wrote it: it is an int
(IntAttribute), the client sets it to 0 for every pet it is given
(AddPetToPlayer in LWOPetControlComponent::HandleMessage 0x00d0fe00,
which ignores the message's elemental type) and every live pet had t="0".
Commented so it isn't "fixed" later.
Check in game: with a tamed pet, summon it, change zones and log out and
back in: the pet menu and pet naming work as before and no pet is shown
as out until you summon one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client reads the cooldowns still running from the charxml on load
(LWOSkillComponent::LoadFromSaveData 0x00c2a9c0): <skil sc="..."> split
on ';' and ':' (the delimiter string at 0x015b1714 is L";:") into a
cooldown group and the seconds left, each put in its cooldown map under
{hasCooldownGroup = 1, group}, the key it checks when the player casts a
skill of that group (0x00c11400). Live wrote <skil/> with nothing running
and e.g. <skil sc="17:15.6958;"/> otherwise (226 live charxmls; groups
8, 17 and 78 seen, the times below the groups' SkillBehavior cooldowns).
The claude-client-re-docs page described sc as "id;time" pairs; the
live saves and the delimiters show "group:time;".
DLU wrote no <skil>, so relogging or changing zones cleared every
cooldown (e.g. the 90 s imagination and faction skills).
- A player's successful cast (CastPlayerSkill) starts its
SkillBehavior.cooldowngroup's cooldown (groups 0 and up, cooldown > 0),
counted down in Update.
- Saved as live wrote it; loaded back when the player's SkillComponent
is created, so a cooldown survives several zone changes. Old saves
without <skil> load with none running.
Check in game: use a skill with a long cooldown (e.g. a faction kit's
special skill, a consumable with a cooldown), change zones or log out and
back in right away: its cooldown is still shown and counts down from
where it was (minus the loading time on the server).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOCharacterComponent sends ModifyGhostingDistance (1485: an optional
fDistanceScalar, default 1) on load. All 239 live packets were
the single byte 00 (default scale), so there is nothing for the server to
change; it was logged as an unknown game message. It is now read and
ignored.
Check in game: nothing changes; the world server log no longer has
"Received Unknown GM ... 1485" on each load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client's rail activator sends RequestRailActivatorState (1479, no
payload) when it is added to the world (LWORailActivatorComponent::
SendMessage 0x00c00da0) and live answered with
NotifyRailActivatorStateChange (1478, bActive) to that client; the client
stores rail_activator_active and updates the rail's pick type (usable or
not). Live: 152 requests, 413 answers, every one bActive = true. DLU
dropped the request and never answered.
RailActivatorComponent keeps the level key rail_activator_active
(default true; every rail in the live levels sets it to 1) and the
request is answered with it.
Check in game: in Ninjago Monastery and Nimbus Station, rails (spinjitzu
posts, the Nexus Tower rails) show as usable and work as before when
first loading the world and after zone changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
World packet 120 (UgcDownloadFailed: resType u32, blueprint ID, status
u32, character ID; lu_packets, 31 bytes after the 0x53 as the client
sends it) was logged as an unknown world packet. The client sends it
from LWOResMgr2Interface::FinishResourceRequest (0x0105e5c0) for every
blueprint file (player model, car or rocket build) whose request did not
end with HTTP 200, including files it did not download at all (status
0). Live: 1525 packets, 1520 with status 0 (all four file types, around
load and FetchModelMetadataRequest), 5 with 404. Live sent nothing back
and the sessions carried on.
The logout the client does on failed UGC downloads
(NET_DISCONNECT_FAILED_DOWNLOAD_UGC, MainThread_LogoutDueToConnectionFailures
0x0102b9c0) is its own decision after repeated connection failures to the
UGC HTTP server; there is no server message that prevents or triggers it.
So the server only records it: a log line for an HTTP failure (for
finding missing files on the UGC server), a debug line for status 0.
MessageType::World gains UGC_DOWNLOAD_FAILED = 120 (appended; the magic
enum range goes to 120).
Check in game: nothing changes for the player. With a property that has
models, load it: the world server log has no "Unknown world packet 120"
lines; if a model file is missing on the UGC server there is one "failed
to download blueprint" line naming it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The property editor sends ResyncEquipment (1238, no payload)
after the player picks up or sets down a model they carry: 59 live
packets, every one right after PlaceModelResponse. Live answered with no
game message, only replica serializations (packet 0x1b), i.e. the
player's equipment sent again. DLU dropped it.
The handler marks the inventory's equipped items dirty and serializes the
player, so everyone gets the equipment again. That live re-sent the
inventory part of the replica is inferred: the captures show a
serialization but it was not decoded.
Check in game: on your property, in build mode, pick up a model from the
world and place it again a few times, then leave build mode: your hat,
shirt, weapon etc. are all shown on you (and to a second player
watching), none invisible.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a client gets EchoStartSkill (or SyncSkill) from a caster it sees
as dead, aimed at another object, it logs "msgEchoStartSkill
msgCasterDead" and sends CasterDead (120: optional i64Caster, optional
uiSkillHandle) through that target
(LWOSkillComponent::msgEchoStartSkill 0x00d5dc90). 303 live packets, the
target nearly always the attacked player, the caster a spawned enemy;
the next messages were mostly Die and SetStunned for the player. DLU
dropped it.
The server now ends that skill on the caster: its behaviors with that
handle are dropped (end entries run, pending timers and syncs discarded,
its projectiles removed), so a hit an enemy scheduled before dying does
not land later. It only does so when the server also sees the caster as
dead, so a client can't cancel a living enemy's attack. What live did
with the message is inferred from the name and when it was sent.
Check in game: kill an enemy in the middle of a slow or charged attack
(e.g. a Maelstrom horseman or a spider queen add): its attack does not
hit after it has died, and other enemies' attacks still hit normally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client sends SetLastCustomBuild (890, a u32-length wide string;
other references) with its rocket build, e.g. "1:14454;1:4714;1:4715;", when it
assembles the rocket the player carries: after EquipInventory at a
launchpad and on load when landing (202 live packets). It stores it as
lastUsedRocketInfo (LWOCharacterComponent::SendMessage 0x00d34330) and
reads it back from char@lcbp on load (0x00ca4910). Live saved lcbp in the
same format on every character.
DLU dropped the message and only set lcbp when the server equipped the
rocket. The handler now keeps the client's build as the rocket config
(saved as lcbp). It does not change whether the player lands on the next
load: DLU uses an empty config to skip the landing after non-rocket
transfers, and that still happens when the server clears it.
Check in game: build a new rocket in the rocket builder, launch from a
launchpad and land: the rocket shown on landing and the builder's default
next time are the new build. Teleport to another world without a rocket
(e.g. from a property): no landing animation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client sends SetTooltipFlag (469: bFlag, then the tooltip,
2 live packets, both tooltip 24) when a tooltip has been shown, and
follows it with SetFlag for the same ID. DLU dropped it and never wrote
char@ttip, so the client's tooltip bits reset on every load.
- SetTooltipFlag message struct and handler: the bit is set or cleared on
the CharacterComponent exactly as the client does it
(LWOCharacterComponent::SendMessage 0x00d34330): tooltips above 127 are
ignored, set = 1 << tooltip, clear = mask ~1 << tooltip (which also
clears the lower bits), shifts of 64 or more give 0.
- Saved as char@ttip, a 64-bit value (the client reads it with
GetLongLongValue in LoadFromSaveData 0x00ca4910). Live wrote it on
every character (226 live charxmls: "0" or "16777216" = tooltip 24).
Old saves without it load with 0.
Check in game: on a new character, trigger a first-time tooltip (e.g.
the one about an item or a new area), change zones and log out and back
in: the character loads normally and the same tooltip does not come back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client sends SetMissionTypeState (851) for the journal's mission
types and subtypes (Missions.defined_type / defined_subtype) at load and
after mission updates: 650 live packets, every one with the optional
state left at NEW, the subtype written before the type. DLU
dropped it and never wrote the states, so the client lost them on every
load.
- SetMissionTypeState message struct and handler: the state is stored on
the player's MissionComponent (byte-sized as the client keeps it,
LWOMissionComponent::msgSetMissionTypeState 0x00c90d00).
- Saved as live wrote it (226 live charxmls): after <cur>,
<ts><type v="Build"><st sub="" val="1"/></type>...</ts>, <ts/> when
empty. Read back per <type>; old saves without <ts> load with none.
The client's reader (0x00d171c0) reads v from <ts> rather than from
each <type>, so it files every state under one type; the server keeps
the types apart.
- Mission::SetMissionTypeState (on accept) records NEW for the mission's
type instead of doing nothing.
Check in game: accept missions of a few kinds (a location mission, a
battle achievement), open the passport/journal, then log out and back in
and change zones: the journal tabs keep their "new" markers as before,
nothing errors, and the character still loads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>