Commit Graph

2022 Commits

Author SHA1 Message Date
Aaron Kimbrell
0f9af0ccf2 test: run the property rent and reputation parity tests on their own
ctest runs each parity test in its own process, so they add the properties they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:52 -05:00
Aaron Kimbrell
d95eab1212 feat: property reputation from visitors, resistant to farming
Property worlds now give their property reputation for the time other people spend on it, which
fills the property lists and the news screen's Today's Top Properties. Visitors are counted per
account; the owner's account, accounts linked to it and staff don't count. A visit earns nothing
for the first property_reputation_min_visit seconds (WorldConfig's propertyReputationDelay), then
each active minute (the visitor moved) earns reputationPerMinute times a multiplier, for a capped
number of minutes per visit. Repeat visitors earn less the more recent days they already gave
reputation, and each visitor and each property have a daily cap. Every parameter is a setting;
what each account gave each property per day is kept in property_reputation_visits. The rules are
pure functions with unit tests.

Fixes #636
Fixes #637

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:52 -05:00
Aaron Kimbrell
680615ba9f feat: optional property rent
Off unless property_rent_enabled is on. Each property world's rent comes from its PropertyTemplate
row (minimumPrice every rentDuration x durationType; Block Yard is free), unless the new Property
Rent dashboard page sets another price or period (property_rent_manage). Rent is taken from the
owner's coins shortly after their character loads, with a mail receipt; unpaid rent is mailed and,
after property_rent_grace_days, makes the property private until it is paid, like live. The
property management component refuses public or best friends privacy while rent is overdue and a
property world that loads overdue makes itself private. Property game messages are unchanged.

Fixes #943

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:52 -05:00
Aaron Kimbrell
b2403b09ea feat: contraband list with flagging and optional removal
Staff list contraband items on a new dashboard page (item search, reason, flag or flag and remove;
contraband_manage to edit, reports_view to see). World servers check every inventory when a
character loads and every item a player receives: each find is an economy flag of the new kind
Contraband, shown with the other flags and in the character's related data. Items marked for
removal are taken away, with a character snapshot kept first so they can be given back, an audit
entry and a mail or chat message telling the player why. Staff are skipped unless
contraband_ignore_staff is off. Worlds reload the list when it changes (RELOAD_CONTRABAND, added
at the end of ePlayerAction).

Fixes #1563

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:52 -05:00
Aaron Kimbrell
341ce89a6a feat: refuse stale character saves with a save generation
Every character gets a save generation in charxml. A world bumps it when it loads the character for
play, and every save from that world only goes through while the stored generation is still the
one it loaded or last saved (and moves it on). Dashboard edits, restores and maintenance writes bump
it too. A world that lost the character to another world (a disconnect noticed late, a zone
transfer, an instance migration) or to a dashboard edit can no longer overwrite the newer data: the
save is refused, logged and audited as stale_save_refused, the world stops saving that character
and a player still connected to it is disconnected with the save failure reason so they reload.

Fixes #639

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:51 -05:00
Aaron Kimbrell
17689cd663 feat: build a zone's 3D data on worker threads
Opening a zone the first time built its terrain, scene objects and manifests
and ran ImageMagick on the web thread, stalling the dashboard for seconds.

- Workers: the shared pool plus Workers::Reply (answer at once when built,
  else from a worker via Web::Defer).
- terrain_chunks/terrain_layers/scene/paths/scenery/flairs (world3d, property
  and showcase routes) and terrain textures go through it; results are built
  once in OnceCaches, the .raw is read once per zone for chunks, layers and
  flairs, deflated bodies are cached thread-safely.
- ImageMagick conversions are deduplicated and written under a temporary name.
- Workers don't query the CDClient, read settings or call mongoose: ZoneTable,
  render components, flairs, object names, LOT kinds and terrain texture names
  are read at startup; client_location is read once; base64 is plain C++.
- Logger writes one line at a time (mutex; localtime's buffer is shared).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:51 -05:00
Aaron Kimbrell
20f70ac0de refactor: inventory and item game messages as structs
Converts AddItemToInventoryClientSync, SetInventorySize,
RemoveItemFromInventory, ConsumeClientItem, UseItemResult,
UseItemRequirementsResponse, ResponseMoveItemBetweenInventoryTypes,
NotifyNotEnoughInvSpace, UpdateInventoryUi, MarkInventoryItemAsActive and
MoveInventoryBatch (11 old Send functions) and the received EquipInventory,
UnEquipInventory, RemoveItemFromInventory, MoveItemInInventory,
MoveItemBetweenInventoryTypes, RequestMoveItemBetweenInventoryTypes,
PushEquippedItemsState, PopEquippedItemsState, ClientItemConsumed,
UseNonEquipmentItem, SetConsumableItem, UpdateInventoryGroup and
UpdateInventoryGroupContents (13 handlers) to NetGameMsgs in
InventoryMessages.{h,cpp}. The received messages are registered in
GameMessageHandler's map and handled by InventoryComponent (new On* methods
holding the old handler logic verbatim); the old functions and switch cases
are deleted. AddItemToInventoryClientSync::SetItem fills the fields that
come from the Item.

No wire change and no change in recipients. Kept as DLU has always sent them
and documented: NotifyNotEnoughInvSpace goes out with the message ID of
VehicleNotifyFinishedRace, and RemoveItemFromInventory always sets the flag
of the fields DLU fills. The old SendMoveInventoryBatch was never called
and wrote one flag bit fewer than the client reads (it had no moveSubkey);
the struct follows the client's layout (1.10.64,
LWOInventoryComponent_Common::msgMoveInventoryBatch at 00ce1310) and has no
oracle. UnEquipInventory still ignores the trailing replacementObjectID the
client sends, as before.

Verified byte for byte against a frozen verbatim copy of the old functions
over an input grid (AddItemToInventoryClientSync with real Items, extra info
and bind flags), to one client and broadcast; received messages compared
with the old handlers' read sequences and every truncated payload rejected;
hand computed golden bytes; round trips; a deliberate width mutation made
the tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:51 -05:00
Aaron Kimbrell
dae1925d1a refactor: effects, audio, animation and UI text game messages as structs
Converts PlayAnimation, PlayNDAudioEmitter, PlayEmbeddedEffectOnAllClientsNearObject,
PlayFXEffect, StopFXEffect, BroadcastTextToChatbox, Play2DAmbientSound,
Stop2DAmbientSound, UIMessageServerToSingleClient, UIMessageServerToAllClients,
StartCelebrationEffect, DisplayMessageBox, DisplayChatBubble, ChangeIdleFlags,
SetNameBillboardState, ShowBillboardInteractIcon, PlayCinematic, EndCinematic,
SlashCommandTextFeedback, PlayEmote and SetEmoteLockState (21 old Send
functions, 23 with overloads) and the received MessageBoxRespond,
ChoiceBoxRespond, CinematicUpdate and PlayEmote to NetGameMsgs in
EffectsMessages.{h,cpp}. The high traffic messages get a constructor for their
required fields. UI messages own their AMF arguments. Every caller is switched,
the received messages are registered in GameMessageHandler's map, and the old
functions and switch cases are deleted. Handlers are copied verbatim.

No wire change and no change in recipients: functions that always broadcast
whatever address they were given are sent with Send(UNASSIGNED_SYSTEM_ADDRESS),
functions that only did SEND_PACKET use SendToClient. Quirks are kept and
documented on the structs (PlayAnimation's UTF-8 sized name, the always written
null terminator in BroadcastTextToChatbox, StartCelebrationEffect always
writing celebrationID, SetNameBillboardState having no payload).

Verified byte for byte against a frozen verbatim copy of the old functions over
an input grid, to one client and broadcast; received messages compared with the
old handlers' read sequences and every truncated payload rejected; hand
computed golden bytes; round trips; a deliberate width mutation made the tests
fail. The byte-equality helper gains a Broadcast mode for functions that
ignored their address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:51 -05:00
Aaron Kimbrell
5a9a3e380b refactor: master packets as structs
Every MASTER service packet is now an LUBitStream struct (docs/PacketArchitecture.md,
PR 14) and the master server's switch is a dispatch map (PacketDispatcher), as are the
master handlers of the world, chat and dashboard servers.

- dNet/MasterPackets.h: RequestZoneTransfer, RequestZoneTransferResponse, ServerInfo,
  RequestSessionKey, SetSessionKey, SessionKeyResponse, NewSessionAlert, PlayerAdded /
  PlayerRemoved, CreatePrivateZone, RequestPrivateZone (passwords still cut to 50
  characters when read), WorldReady, WorldReadyInfo (WORLD_READY to the dashboard),
  PrepZone, Shutdown, ShutdownResponse, WorldShutDown (SHUTDOWN_RESPONSE to the
  dashboard), ShutdownUniverse, AffirmTransferRequest/Response, RequestServerList,
  ServerListResponse, DashboardShutdown, ConfigReload, InstanceShutdown. The Send*
  functions are gone; MasterPackets::SendToMaster(msg) and SendTo(sysAddr, msg) send a
  struct.
- The dashboard and instance migration structs (PlayerAction, DataChanged, Dashboard
  messages, MessageCapture, InstanceMigration) are LUBitStreams of the MASTER service now
  and moved to dNet/master/, included by MasterPackets.h. Their payloads are unchanged;
  master forwards them by re-serializing the struct instead of copying raw bytes.
- InstanceManager, ZoneInstanceManager, MigrationCoordinator, dServer (server info, zone
  transfer response), auth (SET_SESSION_KEY), the world (session keys, player added and
  removed, world ready, shutdown response, affirmations, prep zone, shutdown universe) and
  the dashboard (server list, instance shutdown, config reload, announcements, player
  actions, message capture) send and read structs.
- The login stamps are a `stamps` field of RequestZoneTransfer and
  RequestZoneTransferResponse (read leniently as before: a message without them reads as
  empty); master adds its stamps in the REQUEST_ZONE_TRANSFER handler and when it answers,
  as it did.
- InstanceManager::GetInstanceBySysAddr takes a const address.

Verified: tests/dGameTests/dNetTests/Legacy/MasterPacketsLegacy.h is a verbatim copy of
the old writers and readers; MasterPacketsTests requires identical bytes for a grid of
inputs, checks the old readers read what the structs write, round trips and truncation,
hand written golden packets, that zone transfers without stamps still read, that the dashboard/migration structs write what
"header + Serialize" wrote, and that the dispatcher drops truncated packets. No wire
bytes changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:50 -05:00
Aaron Kimbrell
796fd1b941 refactor: chat packets as structs
Every packet of the chat service is now an LUBitStream struct in dNet/ChatPackets.h
(docs/PacketArchitecture.md, PR 13), and nothing in the chat server or the chat side of
the world server reads or writes a packet by hand any more.

- World <-> chat: LoginSessionNotify, UnexpectedDisconnect, GMLevelUpdate, GMMute,
  Announcement (GM announce), CreateTeam, TeamUpdate (TEAM_GET_STATUS to worlds),
  AchievementNotify, ShowAllRequest, FindPlayerRequest.
- Client -> world -> chat (friends, ignore list, teams, general and private chat):
  GetFriendsList, AddFriendRequest, AddFriendResponse, RemoveFriend, GetIgnoreList,
  AddIgnore, RemoveIgnore, GeneralChatMessage, PrivateChatMessage, TeamInvite,
  TeamInviteResponse, TeamLeave, TeamKick, TeamSetLeader, TeamSetLoot, TeamGetStatus.
  The 77 bytes the handlers skipped are named fields now (the sender block the client
  fills in); the 4 unused bytes after the player ID are kept as `unknown`.
- What the client receives. Chat service (ChatPackets::Client): GeneralChatMessage
  (replaces SendChatMessage; SendSystemMessage stays as a helper built on it),
  PrivateChatMessage. Client service (ClientPackets, as structs go in the file of the
  ServiceType in their header): SendCannedText (replaces SendMessageFail), GetFriendsListResponse, AddFriendRequest,
  AddFriendResponse, RemoveFriendResponse, UpdateFriendNotify, WhoResponse,
  ShowAllResponse, Get/Add/RemoveIgnoreResponse, TeamInvite, TeamInviteInitialResponse,
  and the team game messages chat writes (TeamInviteConfirm, TeamGetStatusResponse,
  TeamSetLeader, TeamAddPlayer, TeamRemovePlayer, TeamSetOffWorldFlag) as TeamGameMsg
  structs, since chat doesn't link dGame's NetGameMsg.
- WORLD_ROUTE_PACKET is ChatPackets::WorldRoutePacket (its own file, dNet/WorldRoutePacket.h,
  since it carries packets of other services): the target and the inner packet.
  ChatPacketHandler::SendRouted replaces the per-function route headers and
  SendRoutedMsg.

Dispatch: dNet/PacketDispatcher.h is a dispatch map from packet ID to (struct, handler
function); packets that fail to Deserialize are logged and dropped. The chat server's
switch and the world's HandlePacketChat switch are now maps. The handlers take the
structs; their logic is unchanged. World code sends to chat with ChatServerLink::Send
(dGame) instead of writing to Game::chatServer by hand. eChatChannel,
eChatMessageResponseCode and eAddIgnoreResponse moved to dCommon/dEnums so the structs
can use them.

Verified: tests/dGameTests/dNetTests/Legacy/ChatPacketsLegacy.h is a verbatim copy of
the old senders and readers; ChatPacketsTests (25 tests) sends a grid of inputs through
both and requires identical bits, bytes and destination, checks the old readers read
what the structs write, round trips every struct, checks truncated packets are refused,
and has hand written golden packets for general chat, canned text, GM mute and a routed
team game message. Breaking one field width (UpdateFriendNotify) and the TeamAddPlayer
zone flag made the tests fail. No wire bytes changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:50 -05:00
Aaron Kimbrell
0c2727ad3c refactor: building game messages and blueprint packets as structs
Converts build mode, arranging, modular build and brick by brick
messages to NetGameMsgs in BuildingMessages.{h,cpp}: StartArrangingWithItem,
FinishArrangingWithItem, ModularBuildEnd and SetBuildModeConfirmed
(sent), and StartBuildingWithItem, DoneArrangingWithItem,
ModularBuildFinish, ModularBuildMoveAndEquip, ModularBuildConvertModel,
SetBuildMode, BuildModeSet, UnUseBBBModel, BBBLoadItemRequest and
BBBSaveRequest (received, registered in GameMessageHandler's map with
their handlers' logic kept). The BLUEPRINT_SAVE_RESPONSE and
BLUEPRINT_LOAD_RESPONSE_ITEMID client packets become the LUBitStream
structs ClientPackets::BlueprintSaveResponse and BlueprintLoadItemResponse,
also used by WorldServer's level load. The never-called
SendBBBSaveResponse is gone.

SetBuildModeConfirmed keeps writing modeValue's and startPos's default
flags as always set, as DLU did. BuildModeSet and UnUseBBBModel now read
their whole client layout (DLU read only the first fields).

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions and inline packet
writes over an input grid, received messages compared with the old
handlers' read sequences with every truncated payload rejected, hand
computed golden bytes, round trips and a mutation check. Layouts
confirmed against the 1.10.64 client in Ghidra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:49 -05:00
Aaron Kimbrell
9458230f0b refactor: property game messages as structs
Converts the property messages to NetGameMsgs in PropertyMessages.{h,cpp}.
Sent: OpenPropertyVendor, OpenPropertyManagement, DownloadPropertyData
(replaces PropertyDataMessage, now with the client's PropertyData field
names), PropertyRentalResponse, PropertyEntranceBegin,
PropertySelectQuery (replaces PropertySelectQueryProperty with the
client's PropertyInfo), GetModelsOnProperty, PlaceModelResponse and
HandleUGCEquipPre/PostDeleteBasedOnEditMode. Received, registered in
GameMessageHandler's map: SetPropertyAccess,
UpdatePropertyOrModelForFilterCheck, QueryPropertyData,
PropertyEditorBegin/End, PropertyContentsFromClient,
ZonePropertyModelEquipped/Rotated, PlacePropertyModel,
UpdateModelFromClient, DeleteModelFromClient, ControlBehaviors,
PropertyEntranceSync, EnterProperty1, UpdatePropertyPerformanceCost,
ReportOffensiveModel/Property and GetHotPropertyData. The news screen's
NewsSendHotPropertiesInfoToClient moves here with the top properties
lookup; the dashboard's player reports now take the decoded text.

Handlers keep their logic and hand the work to PropertyManagementComponent,
PropertyVendorComponent, PropertyEntranceComponent and
MultiZoneEntranceComponent as before. Messages whose payload DLU ignored
(PropertyEditorBegin, PropertyContentsFromClient, ZonePropertyModel*)
now read it with the client's layout. The never-called
SendZonePropertyModelEquipped (which wrote no default flags) is gone.

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions, PropertyDataMessage
and PropertySelectQueryProperty over an input grid (to one client and
broadcast), received messages compared with the old handlers' read
sequences with every truncated payload rejected, hand computed golden
bytes, round trips and a mutation check. Layouts confirmed against the
1.10.64 client in Ghidra. Known wire bug kept as is: PlaceModelResponse
writes response where the client reads a rotation quaternion
(PlaceModelResponse::Deserialize 0x00dc0170).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:49 -05:00
Aaron Kimbrell
527f54488b refactor: pet game messages as structs
Converts the pet taming minigame, naming, command and bouncer messages to
NetGameMsgs in PetMessages.{h,cpp}: NotifyPetTamingMinigame,
NotifyTamingModelLoadedOnServer, NotifyPetTamingPuzzleSelected,
PetTamingTryBuildResult, PetResponse, AddPetToPlayer, RegisterPetID,
RegisterPetDBID, ShowPetActionButton, BouncerActiveStatus, SetPetName,
SetPetNameModerated and PetNameChanged (sent), and PetTamingTryBuild,
NotifyTamingBuildSuccess, RequestSetPetName, StartServerPetMinigameTimer,
ClientExitTamingMinigame, CommandPet and DespawnPet (received, registered
in GameMessageHandler's map; each Handle hands the message to the
player's taming or active PetComponent exactly as the old handler did).
PetComponent, BouncerComponent and the hydrant/catapult scripts build
the structs; the old Send*/Handle* functions and switch cases are gone.
The never-called SendClientExitTamingMinigame is folded into the
ClientExitTamingMinigame struct. MarkInventoryItemAsActive stays with
the inventory messages.

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions over an input grid
(to one client and broadcast), received messages compared with the old
handlers' read sequences with every truncated payload rejected, hand
computed golden bytes, round trips, and a mutation check. Layouts
confirmed against the 1.10.64 client's Serialize/Deserialize in Ghidra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:49 -05:00
Aaron Kimbrell
b489ee59f8 refactor: world packets as structs
WorldPackets now holds what a client sends a world server, one struct per
packet with Serialize/Deserialize: Validation, CharacterListRequest,
CharacterCreateRequest, CharacterLoginRequest, GameMessage,
CharacterDeleteRequest, CharacterRenameRequest, LevelLoadComplete,
PositionUpdate, MailPacket, RoutePacket, StringCheck, GeneralChatMessage,
HandleFunness, UIHelpTop5. WorldServer's switch became a dispatch map of
handlers (each overrides Handle in WorldServer.cpp, logic unchanged:
dashboard hooks, LoadPlayer, chat logging, migration checks, message
inspector capture all still run); a packet that does not deserialize is
logged and dropped. UserManager's create/delete/rename take the structs.

The answers are ClientPackets (the CLIENT service): LoadStaticZone,
CharacterListResponse, CharacterCreateResponse, CharacterRenameResponse,
DeleteCharacterResponse, TransferToWorld, ServerStates, CreateCharacter,
ChatModerationString, MakeGMResponse, HTTPMonitorInfoResponse and
DebugOutput, built at the call sites (world server, UserManager, slash
commands, dashboard actions, components, migration). The world -> chat
forward of a routed packet is ChatPackets::RoutedFromClient. All
WorldPackets::Send* functions, HTTPMonitorInfo and the ClientPackets parse
functions are gone. The architecture doc now states the file rule: a
packet lives in the file of the ServiceType in its header.

No wire change. Verified against frozen verbatim copies of the old code
(tests/dGameTests/dNetTests/Legacy/WorldPacketsLegacy.h): every response
is sent through the old function and the struct over grids of inputs
(all enum values, strings of every length class, IDs, >64 moderation
segments, big XML) and must match byte for byte; every request is read
by the old code and the struct and must give the same values; plus
golden bytes, round trips, truncation checks and a field width mutation
that made the tests fail. Only differences: malformed requests are
dropped instead of handled with partial values, and LevelLoadComplete now
reads the zone ID the client sends after it (lu_packets and captures show
the 1.10.64 client always sends it; DLU ignored it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:49 -05:00
Aaron Kimbrell
efa08ac1d0 feat(master): login stamps travel through master and back
The login's Stamps now go with auth's REQUEST_ZONE_TRANSFER to master and
come back in REQUEST_ZONE_TRANSFER_RESPONSE, so master stamps the steps it
performs itself, with its own time: got the request
(WORLD_PACKET_RECEIVED, zone), the world is still starting and the request
waits (IM_LOGIN_QUEUED, instance), answered with a world
(WORLD_SESSION_CONFIRM_TO_AUTH, instance). Auth sends what comes back in
the login response.

Server to server wire change (message IDs unchanged): both messages end
with a Stamps list (u32 16 * count + 4, then the stamps); it is empty
(4 bytes) when a world server asks for a transfer. Touched:
- MasterPackets::SendZoneTransferRequest / SendZoneTransferResponse: take
  and write the stamps (default empty)
- MasterServer.cpp REQUEST_ZONE_TRANSFER: reads them, stamps, keeps them
  in the PendingInstanceRequest (new stamps member, InstanceManager.h)
- InstanceManager::ReadyInstance / AffirmTransfer: stamp and send them back
- ZoneInstanceManager: HandleRequestZoneTransferResponse reads them; a
  RequestZoneTransfer overload takes stamps and a callback that gets them
- AuthPackets LoginRequest::Handle uses that overload

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:48 -05:00
Aaron Kimbrell
34f43c3fef feat(auth): stamp each login step as it happens
The login response's stamps were a fixed list: START and CLIENT_OS at the
start, an ERROR or two, and IM_LOGIN_START / WORLD_COMMUNICATION_FINISH
appended to every response whatever happened. Now a Stamps list (new
dNet/Stamps.{h,cpp}: eStamps, Stamp and Stamps with Serialize /
Deserialize in the login response's layout, so server messages can carry
it too) is created when the login request arrives, and each step adds its
own stamp, with the time, where it happens: the request read (value: the
client's OS), the account lookup (found or not), the password check,
failed checks, the closed server and play key checks, database writes,
asking master for a world, master's answer, handing the session key to
master, and sending the player on. The response serializes whatever was
stamped; the auth server logs the list like the client does.

What the client does with stamps (1.10.64): PacketHandler_MSG_CLIENT_
LOGIN_RESPONSE @ 00b32f90 reads them (LoginResponse::ReadStamps @ 005ed3c0,
count = (size - 4) / 16) and only logs each with its name from
StampLookup @ 017e6e88 (the 39 eStamps names) and its time relative to the
first and previous stamp. Documented in Stamps.h.

Behaviour change: on success, master gets the session key just before the
client gets the response instead of just after, so the handoff can be
stamped (and master knows the key before the client can reach a world).
The client-facing layout is unchanged; tests pin the stamp bytes, check
the steps of a failed login in order, and round trip the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:48 -05:00
Aaron Kimbrell
70f1c2b19e refactor: common and auth packets as structs
Adds CommonPackets (ServiceType::COMMON): ClientVersionConfirm (the
client's handshake, with the handler that logs it and answers),
ServerVersionConfirm, DisconnectNotify and GeneralNotify (layout from the
1.10.64 client's PacketHandler_MSG_SERVER_GENERAL_NOTIFY; DLU does not send
it yet). AuthPackets::LoginRequest reads the login and keeps the old login
logic in its Handle; ClientPackets::LoginResponse (with the Stamp list)
replaces the hand written response, and AuthPackets::SendLoginResponse
fills it from the settings as before. dServer::Disconnect writes a
DisconnectNotify. AuthServer's if-chain and WorldServer's COMMON case
become CommonPackets::Handle / AuthPackets::Handle, dispatch maps that
read the struct, drop and log it if it does not deserialize, then Handle.
HandleHandshake and SendHandshake are gone.

No wire change. Verified against a frozen verbatim copy of the old
functions (tests/dGameTests/dNetTests/Legacy): the old handshake and login
handlers and the new dispatchers are fed the same packets and must send
identical bytes to the same address (same RNG seed for the session key),
over grids of versions, service types, ports, response codes, error
messages, IPs and stamp lists; plus hand computed golden bytes, round
trips, truncation checks, and a deliberate field width mutation that made
the tests fail. The only behaviour difference: truncated handshake and
login packets are now dropped instead of handled with whatever was read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:48 -05:00
Aaron Kimbrell
d8a47b8a8e chore: list scenery_workers in the example dashboard config
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:48 -05:00
Aaron Kimbrell
23e803280f feat: convert 3D scenery models on worker threads with deferred replies
The dashboard's web server answers one request at a time, so converting a big
.nif (glom files up to tens of MB) held up every other request, flairs included.

- dWeb: Web::Defer hands a request to another thread; the reply is sent from the
  web thread on its next poll (DeferredQueue). A client that leaves first cancels
  it and the late reply is dropped. The synchronous route API is unchanged.
- Web::Shutdown closes connections while the state their close events touch is
  still alive; the destructor no longer runs handlers during static destruction
  (stopping the dashboard aborted in ~WSClient).
- WorkerPool: priority lanes, with one thread only for urgent work (flairs,
  small models, textures), and limited background work.
- Scenery: mesh and texture routes (and the showcase's) convert on the pool;
  thread-safe memory and disk caches, one conversion per model at a time with
  waiters sharing it; zones are converted ahead onto the disk cache while viewed.
- Setting scenery_workers (0: half the cores, 2 to 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:48 -05:00
Aaron Kimbrell
8ac9d991f6 perf(migrations): index map_events_daily by day
Its key starts with the zone, so the economy compaction read the whole table twice for every
day it merged into a month. With 900k old rows to merge on MariaDB the task went from 107 s to
14 s. The index holds only the day and the key, so the game's upsert that adds to a row's
events never updates it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:48 -05:00
Aaron Kimbrell
6b9310e513 perf(dashboard): economy reports on a large ledger
- Leave staff out of the currency and U-score reports by excluding the few staff characters,
  instead of joining every ledger row to its character and account.
- Top earners: total per character first and look up names for the top rows only.
- The places list and the activity report read map events for every kind in one query
  (GetMapZonesAllKinds) instead of one query per kind.

With about 1M currency rows and 650k map event rows (90 days) on MariaDB: currency 1.8 s to
0.8 s, top earners 2.4 s to 0.75 s, places 2.6 s to 0.5 s, activity 2.6 s to 1.7 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:47 -05:00
Aaron Kimbrell
8993360f66 fix(dashboard): MySQL backups with a tcp://, unix:// or pipe:// mysql_host
mysqldump was given --host=tcp --port=//host:port for the tcp:// form the servers accept, so
backups failed. Read mysql_host the way the servers connect with it: tcp://host:port (a trailing
/database is dropped), unix:// as --socket and pipe:// as a named pipe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:47 -05:00
Aaron Kimbrell
91c9f4a13d perf(dashboard): live event object picker without a correlated EXISTS
The picker's EXISTS on ComponentsRegistry (no index) took 2 to 4 s with the bundled SQLite for the
treasure kinds; the same filter as IN takes under 10 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:47 -05:00
Aaron Kimbrell
99aecc6b74 fix(dashboard): activity log table on large MySQL databases
- Read activity and map_id as signed: a row with map_id -1 made the page fail with a server error
  when sorted by map.
- A search counts and pages through the characters it matches (index on character_id) instead
  of joining every row of the log to a name; when the matches are a large part of the log the
  page still walks the log in order.
- Sorted by character name, only the row ids are sorted and the page's rows are read after.

On a log of about 800k rows this takes searches with few matches from about 1 s to 0.1 s and
name sorts from about 3 s to 0.5 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:47 -05:00
Aaron Kimbrell
a14445d6f3 fix(migrations): skip existing columns and indexes on databases migrated elsewhere
On a database NexusDashboard (or a server's own changes) already migrated, 29, 30 and 40 logged
an error for every column that was already there, and 40 and 57 added a second index on
columns MariaDB had already indexed for the inline REFERENCES foreign keys of 0_initial.sql.
Add those columns and indexes only when information_schema says they are missing.

40 also gives pet_names.owner_id values written before 23_store_character_id_as_objectid.sql
the persistent bit, as 23 did for every other character id column, so they match characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:47 -05:00
Aaron Kimbrell
746919b3d2 fix: skip raw terrain chunk data with 64 bit relative seeks
Each skip computed an absolute offset as a 32 bit tellg plus a size
product in 32 bit math (colorMapSize * colorMapSize * 4 and friends),
which wraps for large values and can seek backwards. The skips are now
relative to the current position with the size promoted to
std::streamoff first. Offsets are unchanged for every real terrain file,
so this only removes the overflow the TODO asked about.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:47 -05:00
Aaron Kimbrell
b5e9d3b94c fix: AG property guard camera and foot race starter group
Two script TODOs with the live scripts shipped in the client to follow:

- AgPropGuard: accepting mission 768 before touching the orb (flag 71
  unset) plays the "MissionCam" cinematic, as L_AG_PROP_GUARD.lua does.
  Also no longer dereferences a missing Character.
- BaseFootRaceManager: joins the "FootRaceStarter" group on startup like
  L_ACT_BASE_FOOT_RACE.lua, since other foot race objects look the
  starters up through that group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:46 -05:00
Aaron Kimbrell
76f9388d66 chore: drop an obsolete instance IP TODO
New instances already get their address from external_ip through the
master server's dServer, so the TODO asking to read it from config was
done. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:46 -05:00
Aaron Kimbrell
2add7e064e fix: trigger commands can target the zone
Trigger commands with target="zone" gathered no targets, so they never
ran. Forbidden Valley uses it for two ActivateSpawnerNetwork commands
(qb1 on interact, qb2 on activation) in nd_forbidden_valley.lutriggers,
and the winter large property uses it too. The zone is now the zone
control object, the same entity scripts treat as the zone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:46 -05:00
Aaron Kimbrell
86ee6de544 fix: slows and speed buffs change enemy movement speed
Speed buffs (BuffParameters "speed", for example the slow from the Great
Katana of Freezing and the Shurikens of Ice) and speed behaviours go
through ControllablePhysicsComponent's speed multiplier, which only
players' clients used. Enemies move with MovementAIComponent, which
ignored it, so slows had no effect on them.

- MovementAIComponent scales its waypoint speed by the entity's speed
  multiplier when one is set.
- RemoveSpeedboost set the multiplier of any entity without a
  LevelProgressionComponent (every enemy and pet) to 0 once its last
  boost ended, which was harmless while nothing read it for them. It now
  goes back to the normal base of 500 (multiplier 1).

Needs an in-game check with the freezing katana charge-up against an
enemy.

Fixes #1179

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:46 -05:00
Aaron Kimbrell
e9dd2c9473 fix: exploding assets only explode once
The live script (L_EXPLODING_ASSET.lua) does everything inside
"if not bIsHit": smash the attacker if they stand within 10 units, cast
the explosion skill, smash itself and progress missions. The server only
guarded the attacker smash, so another hit before the asset despawned
cast the skill and progressed missions again.

The TODO asking to kill everything in a radius is obsolete: live smashes
only the attacker directly, and everyone else in range takes the
explosion skill's damage, which the server already casts. Removed it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:46 -05:00
Aaron Kimbrell
b9d5ef9963 fix: Skullkin drill credits the player who breaks it
The drill only progressed missions 972, 1305 and 1308 for the player who
started it, so another player breaking the drill (the usual way on a
busy Crux Prime) got no credit for the drill objectives. The live
script (L_SKULLKIN_DRILL.lua, notifyDie) credits the killer, then also
the activator when that is someone else, which is what it does now. It
also no longer dereferences a null attacker. Removes the "Missions"
TODO.

May account for part of #1021 (Crux Prime daily smashable objectives not
tallying); needs an in-game check with two players.

Refs #1021

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:46 -05:00
Aaron Kimbrell
4ad7ddd9c6 feat: add missing force field, jetpack NPC and Skullkin volume scripts
Ported from the live server scripts that ship with the client:

- scripts\ai\LS\ForceFieldEffect.lua (LEGO Club and Starbase 3001 force
  fields): plays effect 3671 "cast" on whatever touches the phantom.
- scripts\ai\NP\L_NPC_NP_JETPACK_GUY.lua (jetpack ambient NPCs): plays
  the object's "launch" effect when used.
- scripts\02_server\Map\AM\L_SKELETON_SPAWNER_VOLUME.lua (Crux Prime
  Skullkin area): with 10 or more players in the volume the Skullkin
  engineer, miner and patroller networks maintain 2/6/6 instead of
  1/4/4, with a 60 second cooldown between changes and a catch-up check
  when it ends. Only players are counted, and the count never goes
  below zero.

WBL_Enemy_Grabbler.lua only sets wander variables for the old Lua
movement AI, so it joins the other alpha wander scripts in the excluded
list instead of logging as missing.

Refs #746

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:46 -05:00
Aaron Kimbrell
c485ab2d38 chore: remove finished TODOs from the Spider Queen script
The faction and immunity change, spawning the spider wave, knockbacks
and reading the animation time are all implemented right below their
TODOs, so the comments were stale. No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:45 -05:00
Aaron Kimbrell
b316686538 fix: Crux Prime dropship computer respawns and no longer crashes
The dropship computer's OnDie activated the "next" spawner network
(pipe number + 1) when a player smashed it and only fell back to the
first network otherwise. The computer has no next network, so once a
player smashed it (easiest with area attacks like the Doom Hammer) it
never came back. The live script (L_DROPSHIP_COMPUTER.lua, shipped in
the client's server scripts) always resets its own network and
activates the first one, which is what it does now.

OnUse also dereferenced GetMission(979) without a null check, crashing
the world when a player without that mission used the built computer.
It now matches the live script: only players with the mission active
get the Nexus Talon data card, once. SsModularBuildServer had the same
unchecked GetMission and now checks the mission state.

Fixes #596

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:45 -05:00
Aaron Kimbrell
040d7ec067 fix: ignore whitespace around config keys and values
"client_location = ../client " or a value with a trailing space is a
common setup mistake that surfaces much later as an unrelated error (a
missing client folder, a failed database login). Keys and values are now
trimmed of spaces, tabs and line endings when the ini is read, which
also covers the \r of files saved with Windows line endings. Spaces
inside a value are kept, and lines with an empty key are ignored.

Verified with a new unit test that loads an ini with padded keys and
values.

Refs #1113

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:45 -05:00
Aaron Kimbrell
d671d26635 fix: character xml load and save no longer crash on bad data
Loading a character whose xml has a malformed flag threw out of
std::stoul, and a character whose xml failed to parse (or has no obj
tag) dereferenced null on load and again on every save.

- Flags are parsed with TryParse and a malformed one is logged and
  skipped instead of aborting the load.
- The quick parse checks for the obj and items tags before using them.
- Saving refuses to run when the document has no obj tag, logging that
  the character was not saved, instead of crashing and taking every
  other player's unsaved progress with it.
- The remaining obj-child lookups go through a helper that returns null
  when the root is missing.

Component LoadFromXml/UpdateXml still assume a valid document; refusing
to load a player with invalid xml belongs in the world server's load
path, which is being converted separately.

Refs #1332

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:45 -05:00
Aaron Kimbrell
536c36227c fix: read set_faction the way the client does
Level files set factions with set_faction, and most of the string-typed
values have a trailing space ("13:6 ", "13:-1 ") or hold several
factions separated by spaces ("13:4 6"). The server split only on ';'
and TryParse rejects trailing characters, so about 1,500 placed objects
(smashable lanterns, fences, candles, treasure chest spawners and so on)
silently kept their component faction instead.

LWODestroyableComponent::LoadConfigData in the 1.10.64 client reads
set_faction as a string, splits it on both ';' and ' ', and replaces its
faction list with the result. The server now does the same: split on
both, skip empty pieces, and replace the factions (SetFaction for the
first, AddFaction for the rest). This also removes the TODO about
splitting on spaces, and the faction list from the destructible table
no longer uses std::stoi on a token that TryParse already parsed.

Values in the shipped maps were counted with a scan of the .lvl files.
Needs an in-game check that objects placed with set_faction (for example
the AG Survival buff stations and NT treasure chests) are still targeted
or ignored as expected; this may also affect #1301.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:45 -05:00
Aaron Kimbrell
363e02e963 fix: enemies and summons drop targets that have died
FindTarget kept returning the current target while it stayed inside the
tether radius, even after it died, so enemies stood over dead players
(most visible in long respawn areas like the Battle of Nimbus Station)
and pet or summon AI hovered over enemies playing their death animation.

A dead current target now loses its threat and a new target is picked,
and dead entities are skipped both as proximity candidates and as threat
entries. Players who respawn are picked up again through the usual
proximity and threat paths.

Verified by building WorldServer and the test suite; needs an in-game
check (die to an enemy, the enemy should return to idle or pick another
player).

Fixes #1428

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:44 -05:00
Aaron Kimbrell
8a2ccb1ae7 fix: bound AMF3 decoding and stop hashing client keys
Client-sent AMF (CONTROL_BEHAVIORS) was decoded without limits on the
associative part of arrays, on nesting depth, or on the total number of
values. Associative entries went into an unordered_map with the standard
unseeded string hash, so a client could pick colliding keys and make
insertion quadratic, and deeply nested arrays recursed until the stack
overflowed, crashing the world server.

- The associative map is now an ordered std::map (O(log n) whatever the
  keys, deterministic serialization order).
- Each array allows at most 10,000 associative entries, the same as the
  existing dense limit, which is now checked before anything is read.
- Arrays may nest at most 32 deep and one deserializer reads at most
  100,000 values. Every limit throws, which the only caller already
  catches and drops the message.
- Inserting a duplicate key keeps the last value and no longer returns a
  reference to a value that was destroyed when the key already held null.

Verified with new unit tests for each limit (including a 100,000 deep
nesting that previously overflowed the stack) and the existing live
packet test, which still decodes.

Fixes #2035

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:44 -05:00
Aaron Kimbrell
e213a7aeff feat: web dashboard and playground work
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch:
accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy
reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live
events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved
captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes
they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:43 -05:00
Aaron Kimbrell
854d02f509 docs: what live servers sent around world transfers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:37 -05:00
Aaron Kimbrell
6f58a1f23f feat: worlds move their players to another instance, GM commands
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:37 -05:00
Aaron Kimbrell
b0f81a1c8f feat: master coordinates moving an instance's players (replace, merge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:36 -05:00
Aaron Kimbrell
b1930ed4ed feat: instance migration messages and planning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:36 -05:00
Aaron Kimbrell
00b7edfcf0 docs: how the client switches world servers, and instance migrations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:36 -05:00
Aaron Kimbrell
1f1edf790f fix(behaviour): don't crash on MissionDialogueOK from an unknown responder
THIS CHANGES SERVER BEHAVIOUR (intentionally); no wire change.

The MissionDialogueOK handler (carried over verbatim from
GameMessages::HandleMissionDialogOK) dereferenced the responder without
a null check, so a client naming an object that doesn't exist crashed
the world server. It now logs and ignores the message; the script
callback is no longer called with a null player (that path always
ended in the crash).

Test: handling a MissionDialogueOK with an unknown responder crashes
without this change and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:36 -05:00
Aaron Kimbrell
d599b788d0 refactor: mission, flag and collectible game messages as structs
Converts OfferMission, NotifyMission, NotifyMissionTask, ResetMissions,
NotifyClientFlagChange and NotifyLevelRewards (sent with SendToClient:
the old functions never broadcast) and the received RespondToMission,
MissionDialogueOK, RequestLinkedMission, SetFlag and HasBeenCollected
to NetGameMsgs in MissionMessages.{h,cpp}. Callers are switched,
OfferMission's send-it-twice behaviour moves to MissionOfferComponent,
the received messages are registered in GameMessageHandler's map and
the old functions are deleted. Handlers are copied verbatim. The
byte-equality helper can now compare SendToClient messages.

No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions over an input grid
(including OfferMission's two packets), received messages compared with
the old handlers' read sequences and every truncated payload rejected,
hand computed golden bytes and round trips. Layouts confirmed against
the 1.10.64 client in Ghidra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:36 -05:00
Aaron Kimbrell
2681cc51ff refactor: racing and vehicle game messages as structs
Converts the 12 racing / vehicle / modular assembly messages DLU sends
and the 8 it receives to NetGameMsgs in RacingMessages.{h,cpp}, adds
eRacingClientNotificationType for NotifyRacingClient's event type,
switches the callers (RacingControlComponent, CarBoostBehavior,
PossessorComponent, slash commands), registers the received messages
in GameMessageHandler's map and deletes the old functions. Handlers are
copied verbatim. Also moves the byte-equality test helpers into a
shared GameMessageTestUtils.h.

No wire change. Verified byte for byte against a frozen verbatim copy
of the old functions over an input grid, to one client and broadcast;
received messages compared with the old handlers' read sequences
(including all 16 optional-field combinations of
VehicleNotifyHitImaginationServer) and every truncated payload
rejected; hand computed golden bytes; round trips; a deliberate field
mutation made the tests fail. Layouts confirmed against the 1.10.64
client in Ghidra. Malformed received messages are
dropped (see the foundations commit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:36 -05:00
Aaron Kimbrell
7fb75f0484 refactor: activity game messages as structs
Converts ActivityEnter, ActivityExit, ActivityStart, ActivityStop,
ActivityPause, StartActivityTime, RequestActivityEnter,
RequestActivityExit and ShowActivityCountdown to NetGameMsgs in
ActivityMessages.{h,cpp}, switches their callers (racing, shooting
gallery, survival, waves, AG course, NT combat challenge) and deletes
the old GameMessages::Send* / HandleRequestActivityExit functions.
REQUEST_ACTIVITY_EXIT is registered in GameMessageHandler's map.

No wire change. Verified by comparing every struct byte for byte with a
frozen verbatim copy of the old functions (tests/.../Legacy) over a
grid of inputs, both to one client and as a broadcast (a temporary test
proved the copy matched production before it was deleted), plus hand
computed golden bytes, round trips and a deliberate field mutation that
made the tests fail. Layouts confirmed against the 1.10.64 client in
Ghidra. Only difference: a broadcast no longer makes
the extra Send(UNASSIGNED, false) that RakNet already rejected.

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