The world read only a scene's objects and skipped the rest with
file.ignore. Level now reads a .lvl the way SceneLoader::LoadLvlFile
does (1.10.64) and keeps what it reads.
Chunked files: the file info chunk gives where the environment, object
and particle chunks start (the chunks were walked in order before); the
environment chunk gives where the lighting, skydome and editor settings
start, and the client reads all three only when there is lighting.
Files from before chunks: the no-editor-settings byte only from version
32, the revision from 37, a file newer than 48 is not read, a file
older than 3 has no objects; then lighting, skydome, editor settings
and objects in turn.
Kept, by version:
- lighting: blend time (45 on, default 10), ambient, specular, upper
hemisphere, position, min and max draw distances (31 on; before 39
fog near and far only), cull settings (40 on), fog colour (31 on),
directional colour (36 on), and before 42 the zone's spawn point
(before 33 the fixed one the client uses);
- the skydome and its five other files (34 on; they were skipped from
33);
- the editor's colours (37 on, the first 32);
- each object's node type (38 on), glom ID (32 on) and render
techniques (7 on: a 64 byte name, then per technique a 64 byte name,
u32, u8 and 16 floats); before, a count of techniques was read and
the techniques themselves were not, so an object with any misread
the rest of the file.
Object config is split at line breaks as before. A file that ends early
keeps the objects read before its end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReadLvlObjectData (1.10.64, 0x0103ba20) loads every scene object except
those of LOT 1 (the player). The world spawned them; now it skips them.
No 1.10.64 scene has one; one older client scene file has two.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zone held a std::function that queried the SceneTable, so tests could swap it out. CDSceneTableTable now loads the table with the others, and Zone looks scenes up in it; tests fill its entries like the zone table's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LuzFile::ReadLUZFile, LuzReader::ReadScenes and ZoneLoader::ReadZoneFile
(1.10.64):
- a version older than 20 is read as 20;
- before version 30 a scene is a bare u32 SceneTable ID: in ID order,
each one with a SceneTable row gets the next index (-1 past 255) as
its ID and the row's sceneName as its file; the others are left out;
- from 30 to 32 the file has no scene IDs: a scene's ID is its index in
the file (they all had ID 0, colliding);
- a later scene with the same ID and layer replaces an earlier one, a
scene ID with no layer 0 scene is left out, and before version 41
only the layer 0 scenes are loaded.
No 1.10.64 zone file has a duplicate scene, a scene without a layer 0
scene or another layer before version 41, so what live zones load is
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LuzFile::ReadLUZFile copies the path chunk by its size and ReadLUZPaths
reads it with LevelPath::FromBuffer (1.10.64). Reading as they do:
- the paths are read from their chunk, never past its end;
- a chunk version of 2 or more, 10000 paths or more, or a path the
client refuses (version 19 or more, 10000 waypoints or more, more
than 99 name/value pairs on a waypoint) leaves the zone with no
paths, logged; the rest of the zone file still reads;
- before version 3 a path's type is a name ("platform" or a movement
path) and every waypoint has a rotation, the moving platform values
and a name/value list, whatever its type;
- property headers are there from version 3, spawner and camera
headers from version 4;
- a camera waypoint's FOV before version 11 is divided by 1.25;
- a property path's reputation multiplier outside 0-1000 becomes 0.
Every path chunk of every client's zone files on disk (858) reads to
its end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ZoneBoundary's destinationZone and destinationSceneID and the scene's NiColor, so main's scene tools and tests read
the zone this branch's reader loads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zone::GetScene and SimplePhysicsComponent's Get/SetAngularVelocity handlers were declared and used but their
definitions were lost where the merge kept this branch's side of the file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dashboard's copy of the terrain reader in dCommon is gone; main's in dZoneManager reads for everyone, and keeps
what the copy had over it: colour and texture maps from before version 32 become RGBA, cut to the resolution the
client keeps. The scene map (ZoneScenes) and the dCommon tests compile against that reader.
A scene ID takes its layer as the zone file numbers it (0 General, 1 Audio) as well as the enum, so main's scene tools
and tests build; /getscene prints the layer as a number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's model behaviors (every rotation, DoDamage, OnTimer, OnStartup, MoveBackToStart, moves along the model's
local axes), raw terrain reading with scene lookups and scene tools, property model and brick by brick fixes and the
chat server's player count fix, merged onto this branch's structures:
- game messages stay structs; the angular velocity messages join them and the handler sets a message's target
- strips own their speed again and the block names, moves and rotations stay in the shared action table
(PropertyBehaviorActions, which the dashboard's behavior player reads; it now lists rotations too)
- the zone keeps reading its file through ZoneFile and takes the boundaries from it, then loads the raw terrain as main
does; the old dNavigation terrain reader is gone
- the show all response stays a struct and logs a player count mismatch as main does
- the property management component tells models who sent a chat message
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: the server mock keeps a copy of the last bitstream sent
The bitstreams sent are usually on the sender's stack, so the pointer it kept was dangling once Send returned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: PlaceModelResponse writes the model's rotation
The client's PlaceModelResponse::Deserialize (0x00dc0170) reads the rotation as an optional w, x, y, z quaternion, but the server wrote the 4-byte response after the rotation flag, so with any rotation other than identity the client ran out of data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: naming a model no longer renames the property
The model info window sends UpdatePropertyOrModelForFilterCheck with isProperty false (client 0x00cc42e0); only the property editor sends it true (0x00c2e710). A model's name and description aren't stored yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: keep a model item's blueprint across saves
A brick built model item's config has blueprintid (lowercase), which the saved item XML didn't keep (only blueprintID), so a picked up model lost its blueprint on the next load and could not be placed again. It is now saved as bp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: write model behaviors only for property and inventory models
The client makes the mutable model component, which reads the behaviors and editing info, only when the object's config has propertyObjectID or inInventory (ObjectLoader2::LoadModelBehaviorsComponent); every other model gets the plain one, which reads only the model block. Writing the behaviors for every model made the client read them as the next component's data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: the model vault uses the models' inventory groups, as the client
LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) and msgUpdateInventoryGroupContents (0x00ce1f50) look the inventory type up in m_VaultToPlayerInventory first, which maps VAULT_MODELS to MODELS. The server refused group updates for the model vault.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat: raw terrain parsing for scene data
Replace old dNavigation/dTerrain raw parser with new Raw module in
dZoneManager. Parse heightmaps, color maps, and scene maps from .raw
files to determine which scene a position belongs to. Build scene
adjacency graph from terrain data and scene transitions.
Adds NiColor type, SceneColor lookup table, eSceneType enum, terrain
mesh generation with OBJ export, and debug slash commands for scene
visualization.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: deduplicate terrain grid lookup logic, remove unnecessary cast
Add IsValidForSceneLookup(), GetSceneIDAtGrid(), and GridToWorldPos()
to Raw::Chunk. Use them in GenerateTerrainMesh, GetSceneIDFromPosition,
SpawnScenePoints, and SpawnAllScenePoints instead of duplicated inline
math. Remove unnecessary static_cast in LoadSceneTransitionInfo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add version < 32 support to raw terrain parser
The parser was only correct for v32 files. For v<32:
- colorMapResolution is width-1, not width
- Color map reads width*width*4 bytes (BGRA per-pixel), not colorMapRes^2*4
- No DDS lightmap
- No texture settings byte or blend DDS
- Scene map: v31 reads (colorMapRes+1)^2 cells with border skip,
v<31 skips 1 byte
- No mesh data
Verified against all 309 .raw files across multiple client versions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: log and reject legacy raw terrain format, clean up ReadRaw API
Log a clear message and throw if the zone uses a legacy raw format
(LUZ version < 30) since no known maps use it. Remove ReadLegacyRaw
and the mapVersion parameter from ReadRaw.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: revert v33 scene field renames to unknown1/unknown2
These fields are skipped by the client in every known build (ptr += 12
and ptr += 4). No client ever reads them into a struct. Keep the
original unknown1/unknown2 names from main.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: load the zone without scene lookups when its raw terrain is missing or unreadable
Log at debug when it parses; build the terrain mesh only for the OBJ export; read the zone name and description only for version 31 and later, as the client does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: look up the scene at a position as the client does, in constant time
Chunk bounds span (width - 1) * scale; the nearest scene map cell is used and the far edge is kept one cell inside. Chunks are indexed by grid cell when they form a grid. Skipping reads instead of seeking, so a cut-off file can't seek past the end. Terrain mesh indices follow the vertex order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: scene point commands take a step and use brick LOTs from the BrickIDTable
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: rename NiColor::ToXYZ to Luminance
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: raw terrain reading, scene lookup, scene graph and scene commands
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: close the OBJ file before removing it so the test passes on Windows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: keep the zone boundary lines and read the luz scene count as the client does
Each boundary line's destination is read as a map ID and an instance ID into a LWOZONEID. The scene count is a byte before version 37 and a u32 from it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: name the fourth flair colour byte padding
The client never sets or reads it, so it holds leftover memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: keep the scene map of version 31 and older raws
These files have a (resolution + 1)^2 scene map, kept in sceneMapLegacy. Version 31 uses its first resolution rows and columns; older versions are all scene 0, as in the client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor: simplify the raw heightmap size check
The dimensions are capped at kMaxResolution, so the product can't overflow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: remove the scene point spawn commands and send /getadjacentscenes as one message
The spawn commands were only for testing. /getscene and /getadjacentscenes stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
A client that loads the zone again while its player is in the world (the
Happy Flower editor reloads the zone in place) sent LEVEL_LOAD_COMPLETE a
second time; LoadPlayer returned at once because the player existed, so
the client waited forever. Now the server resends the character
(CreateCharacter, ServerStates), forgets what it had sent that client,
constructs every object again and sends the done-loading sequence at the
player's current place. A first load is unchanged.
The Happy Flower editor messages (GetEditorLevel, the HF_* messages) are
logged when they arrive, until the server handles them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* add rotation behavior logic (not working correctly for multiple axes)
* Fix FetchContent_Declare speed
* remove manual quaternion math
* feat: working rotations
* remove flag
* feat: make directional movements based on model rotation and not world axes
* poggers rotation on the strip
* Add OnStartup and OnTimer
* go home works
* Add DoDamage behavior and dont try and start the same node multiple times
* fix: pause bbb models
* interrupt rotations and save interrupt count across resets
Totals and disk free, a size chart over a day, week or month, the
fastest-growing tables and every table with sorting. The time chart takes
a timeLabel for ranges longer than a day.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The database_usage task (hourly) measures on the background connection,
checks the free space on the database's disk (database_disk_path, else the
SQLite file or a local MySQL data directory) and alerts once when it drops
under database_disk_warn_percent. Log pruning keeps database_usage_days.
GET /api/database/usage?range=day|week|month. The settings are on the
Settings page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IDatabaseUsage: MySQL/MariaDB from information_schema.TABLES (fresh
statistics on MySQL 8), SQLite from its page counts and the dbstat table
(now compiled in) with counted rows. Samples go to database_usage and
database_usage_tables (migrations 105 / 88).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOMissionComponent::MissionWindowOK (0x00d501f0) turns a mission in only
when CanReceiveAllRewards (msgCanReceiveAllRewards 0x00d17e20,
sub_00d17750) passes for the reward the player picked: the reward items
(the repeat ones once completed, each at least 1) must fit after the items
the tasks take (a gather task's target unless it keeps them, one of a
delivery task's target) with the first completion's bag rewards added.
The fit is LWOInventoryComponent_Common::CheckCanAddItems (0x00d53de0):
turned in items leave first (an item of 1 settles the whole count), a
unique item fails while one is held outside the buyback, then each item
tops up stacks of its LOT (stack size 0 is no limit, LookupItemInfo
0x00c1c190) and takes free slots.
MissionDialogueOK now refuses a turn-in that does not pass, which the
client never sends. The client's max currency part is not checked: the
server writes no cm, so the client has no limit either.
InventoryComponent::GetDefaultInventorySize is split out of GetInventory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client's cooldown (LWOMissionComponent sub_00cd7770) counts whole
minutes since the last completion and has none when Missions.cooldownTime
is 0 or below. Accepting used seconds and treated only -1 as no cooldown,
so a cooldown of 0 still refused in the second of completion and other
negative values refused for a while. CanAccept now uses the same check as
the offers (MissionComponent::IsCooldownRunning).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client checks Missions.prereqMissionID in
LWOMissionComponent::getMissionState (0x00cd7400): text without any of
!&^;,|(): is one mission ID (met when 0 or ever completed); anything else
goes through its expression reader (sub_00a32980), left to right with no
precedence, & , ; and, | or, ^ xor, ! ~ not, ( ) groups, and/or skipping
the rest once the result is known. Each operand (sub_00c90a80): "id" is
met once the mission was ever completed; "id:state" when the client's
state mask of that mission equals state.
DLU read the text right to left ignoring groups, so
"(509|229)&(812|813|814|815)" (mission 781) was met by 509 alone; a plain
ID needed the mission's state to be 8, so a repeatable being repeated
blocked the missions after it (1870 after 1869); and id:state compared the
mission's own state. GetClientMissionState moves to MissionPrerequisites
so operands can ask for the mask.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client keeps a viewed flag on each mission in progress, sets it with
SetMissionViewed (LWOMissionComponent::msgSetMissionViewed 0x00bec7a0, only
for missions in progress) and reads it as v on the mission's <cur> entry
(LWOMissionComponent::LoadFromSaveData 0x00d171c0, a number or "true", false
when missing). The server now handles SetMissionViewed, saves v="1" for a
viewed mission in progress and loads it back; saves without v load as not
viewed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A player who had been to a world before arrived at that world's saved
respawn point instead of the zone's spawn. The saved point is where a death
in that world respawns, not where arriving starts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A client asking to resync a simple mover that has no path got a
PlatformResync anyway, and the client's simple mover then reads waypoints
from its null path and crashes (LWOPlatformSimpleMover::Resync 0x00bf9ec0,
Path::GetWaypointAtIndex). The two LOTs special-cased for this before are
examples of the same thing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::msgUseNonEquipmentItem (0x00cdeba0) asks
GetStunned first and uses nothing when canUseItem is false
(LWOControllablePhysComponent::msgGetStunned 0x00be0df0: stunned from
using items, or dead). The server now counts the use-item stun of every
SetStunned it sends for an entity as msgSetStunned (0x00bf1d10) does
(push/pop, never below 0, characters only, unless immune and not
bIgnoreImmunity), and checks it with IsDead before using an item. The
server never checked either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live charxmls carried <items nn="1"> (225 of 226 captured).
LWOInventoryComponent_Common::LoadFromSaveData (0x00d1ec40) reads it and
then does not add the inventory component's template items to the loaded
character. No effect on players today (the player LOT's inventory
component has no items); the server never wrote it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Client::LoadConfigData (0x00cde200) reads txfring
from the character data; without it the client runs its inventory-full
tips (msgPlayerReady 0x00bde5e0) on every world load. Of 226 captured
live character data packets, 222 had the key, all true; the server never
wrote it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) stores
the ADD's locked flag in the group; locked groups don't count toward the
50 user groups (msgGetInventoryGroupCount 0x00bde4b0 counts only unlocked
ones) and msgUpdateInventoryGroupContents (0x00ce1f50) applies the
1000-LOT cap only to unlocked groups. LoadFromSaveData (0x00d1ec40) reads
it from the grp element's u attribute. The server dropped the flag and
always saved u="0"; saves without u load as unlocked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::msgUpdateInventoryGroupContents (0x00ce1f50)
ignores an ADD to an unlocked group that already holds 1000 LOTs. The
server had no cap, so a modified client could grow the charxml without
limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::msgUpdateInventoryGroupContents (0x00ce1f50)
takes ADD, REMOVE and CLEAR; CLEAR removes every LOT from the group. The
server logged CLEAR as an invalid action and kept the LOTs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) and
msgUpdateInventoryGroupContents (0x00ce1f50) look the inventory type up in
m_VaultToPlayerInventory first, which Initialize (0x00d55a60) fills with
VAULT_MODELS -> MODELS. The server refused group updates for the model
vault.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) resizes
the group name to 64 characters on ADD and MODIFY. The server kept names
of any length in the charxml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) ignores
an ADD whose group id is already in the inventory. The server added a
second group with the same id, saved to the charxml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::msgMoveItemInInventory (0x00d20590) looks the
item up in the inventory the message names and moves it only to the
inventory its LOT belongs in (GetInventoryTypeForLOT 0x00c3c4f0), or for
the vaults (VAULT_ITEMS, VAULT_MODELS) the LOT's vault type
(GetValueTypeForLOT 0x00c3c4a0). An item in the target slot swaps into the
moved item's old slot and inventory, and must belong there. The server
moved an item into any inventory destInvType named and left the swapped
item in the destination. Live clients never set destInvType (0 of 61 in
the captures).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::DoRemoveItemFromInventory (0x00d1fb50)
passes over equipped items when the loot source is LOOT_SOURCE_DELETION.
The server deleted the item while it was still worn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::msgRemoveItemFromInventory (0x00d203b0)
counts the item (GetInvItemCount) and does nothing when it holds fewer
than iStackCount; a request for an item with no count takes the whole
stack. The server cut the request down to the stack and deleted it, and
deleted nothing for a count of 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOInventoryComponent_Common::CanRemoveFromInventory (0x00ce0d20) refuses
the possessed object (GetPossessedObject) and the mount item
(GetMountInventoryID) before the operator bypass and the deletion
restrictions. The server let a player delete the mount they were riding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOActivityComponent::Startup (0x00d006b0) sets actIDovrd, when the level
leaves it out, to whether the level has an activityID, so
LWOQuickBuildComponent::LoadDataFromTemplate (0x00cbb2e0) keeps the level's
activityID; the template's is read only for an explicit actIDovrd=0. Every
quickbuild with actIDovrd=0 in the levels has the template's activityID as
its level one. Comment only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live's 28th sale to a full buyback removed the oldest item, then sent
MoveItemInInventory (inventory 11, VENDOR_BUYBACK) for each of the 26 items
left, to slots 0 to 25 in order, before the sold item was added after them.
The server left the hole, so the sold item took the dropped item's slot
and the buyback page was no longer in sale order. The buyback inventory is
not saved and nothing changes hands; only slots move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOSkillComponent::msgStartSkill (0x00d45cb0) starts no cooldown for cast
types 1 and 2, and inserts the cooldown (std::map insert), so a cooldown
group already running keeps its time left. The server started one for
every cast type and extended a running group to the longer cooldown.
Live client casts used cast types 0, 3 and 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOSkillComponent::MsgSyncSkill (0x00d454b0) checks whether the caster is
dead before running a synced behavior; if so it only removes the behavior
handle from the skill's pending ones. The server ran the sync anyway, so a
player's delayed hit still landed after they died.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOSkillComponent::msgStartSkill (0x00d45cb0) logs and ignores a StartSkill
whose skill handle is still waiting on behavior syncs. The server ran it a
second time: the hits, the imagination cost and the mission progress twice.
Live captures hold 22 byte-identical StartSkill resends with the same
handle a few packets apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item::UseNonEquip declared a second `success` inside the package branch, so
the UseItemResult it sent (and its log line) was always false, even when the
package opened and its contents were given. The client
(LWOInventoryComponent_Client::sub_00c1bb20 (0x00c1bb20)) plays the item's
audioEventUse on true and a failure sound on false; live sent true for 183
of the 187 UseItemResult in the captures.
Test: UseItemResultTests.AnOpenedPackageReportsSuccess.
Check in game: opening a package (e.g. a starter pack or a reward bag) plays
its open sound rather than the failure sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client's RequestLinkedMission handler (LWOMissionOfferComponent
0x00cd6eb0) takes the giver's list without the mission the window was
for, and a new mission (AVAILABLE, never completed) at its head is the
one offered. DLU offered every qualifying mission again, so a mission
just accepted was offered once more. The live captures match the client:
no offer after an accept, and after a turn in the next mission of the
chain, once per RequestLinkedMission.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOMissionOfferComponent builds a player's missions for a giver
(0x00c605f0, one entry 0x00bebf30) in priority order: 0 a mission to turn
in here, 5 one in progress, 10 a new one, 15 a repeat. A ready mission is
only listed where MissionNPCComponent.acceptsMission is set, a new one
only where offersMission is set and its cooldown has run out, a mission in
progress by every giver that has it. A random mission (0x00c5ffd0) lists
one of its pool that the giver has: one to turn in first, then one in
progress, else a random one. Missions are kept by mission ID; equal
priorities keep that order.
DLU sent an OfferMission for every mission that qualified, including one
ready to turn in at a giver that does not accept it. In the live captures
every use got one mission's OfferMission pair. A use now offers the head
of the list, and a pick from the multi interaction menu the picked one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LWOMissionComponent::getMissionState (0x00cd7400) returns a mask: COMPLETE
once the mission was completed, ORed with the state of the mission in
progress; a mission not in progress gets AVAILABLE when it was never
completed or is repeatable and its prerequisites are met, else 0. DLU's
handler answered UNKNOWN (-1, every bit set) for a mission that cannot be
taken and COMPLETE for a completed repeatable one, and ignored
cooldownInfoRequested. With cooldown info requested (0x00cd7770) the
client only checks the cooldown of a repeat in progress (state 9 or 10):
fewer whole minutes since the last completion than Missions.cooldownTime.
MissionComponent::GetMissionState, used across the server, is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object's EnemyPrecondition (level config) decides who it is an enemy of:
whoever asks and doesn't meet the preconditions, whatever the factions
(LWODestroyableComponent::msgIsEnemy 0x00c9e930; ',' ';' ':' join groups
that must all pass, '|' alternatives). Forbidden Valley Siege's objects use
it today; any level object can.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>