CaptureProperty::Build reads, per world of a capture, the property it was:
each model (constructed with a model component, or a brick built model) with
its LOT, object, spawner, blueprint and behavior count and where it stood per
time span, from its constructions, serializations and destruction; the
DownloadPropertyData of that map and the GetModelsOnProperty counts; and the
events: placed (a PlaceModelResponse with the model made at that position,
before or after it), moved, removed (with the DeleteModelFromClient reason),
editing, and behavior messages.
CaptureTool property <bundle> --cdserver prints it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A compressed construction config is a u32 uncompressed size, a u32 compressed
size and the zlib bytes: the reader took the first size for the second and lost
its place, so live model constructions never read. It now inflates and reads
the entries.
A model's item component isn't made (Entity::Initialize): its model component
writes the item info. The layout left the registry's item component in, so a
premade model's components read one block twice and didn't match. Live writes
the same bits from its item component, so both now read exactly.
Tested with a live placed model's construction and the server's own model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every live capture decoded: 83 of 60,659 constructions and 5 of 609,919
serializations now don't read exactly (was about 1 in 10 constructions, and
the decode aborted on text that isn't UTF-8).
- trigger component (header trigger bit): a bit and the trigger ID, last
- BBB component on objects listing component 107 (characters)
- local space info, buff immunities, phantom physics distance, skills in
progress, a choice build's setting
- moving platform: path when dirty, then subcomponents while a 1 bit comes
before one (mover and simple mover)
- pet: names under the dirty bit, on updates too; no item or model component
- models: the plain model block, or the mutable one (behaviors, editing
info) when the config has propertyObjectID or inInventory; the UGC block
is the item's
- zone file variants: markedAsPhantom (phantom physics) and renderDisabled
(no render data)
- compressed LDF: u32 uncompressed and compressed sizes, inflated and shown
as entries
- narrow text that isn't UTF-8 is shown byte by byte, and the capture tool
prints invalid text with replacements instead of aborting
Tests use live byte samples for each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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, as live wrote it. DLU
wrote the behaviors for every model, which the client then read as the next
component's data (the render effects). Tested with the capture decoder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client reads the moderation and names bit whenever the pet component is
dirty, on updates as well as on construction (LWOPetComponent::Deserialize),
and live wrote a 0 there on updates. DLU wrote it only on construction, so
on an update the client took the next component's first bit (a combat AI's
dirty bit) for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client reads a moving platform's subcomponents while a 1 bit comes
before one (LWOMovingPlatformComponent::Deserialize), as live wrote them.
DLU wrote the mover without the closing 0 bit, so the client took the next
component's first bit for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client never loads a level object whose config has carver_only set (its
resource manager skips the load; LWOBasePhysComponent::LoadConfigData
0x00c495c9 reads the flag next to navmesh_carver), and live never sent one:
none of the 331 carver_only placements (mostly Crux Prime's trigger boxes)
shows up among the live constructions, where their zones' other objects do.
DLU spawned them as objects.
They are skipped now. The ones that carve the navmesh (53, in six zones,
among them the Avant Gardens Sentinel camp walls) still stop the server's movers: their shape
goes into the world's movement blockers without an object, owned by the
physics world (dpWorld::AddOwnedMovementBlocker) and freed when it shuts
down. None of them has a script, trigger or group.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Walls whose asset shape the server doesn't know are left out of the movement
blockers (a stand in cube would be a guess), which left out every navmesh
carver in Robot City (hedges, hedge corners, potted bushes, greebles, robot
statues) and the Ninjago Monastery benches. Their sizes now come from the
client's own collision shapes (res/physics): the bounds of all of a shape's
parts, with the offset of the box from the object's position. The hedge
corner is an L, so its box also covers the inside of the corner.
The same bounds match two shapes the server already had (misc_phys_10x1x5:
10 x 5 x 1; Trigger_Rectangle_Box: 8 x 8 x 4).
CreatePhysicsEntity's shape lookup becomes CreateAssetShape, which needs no
component, for walls without an object.
Still unknown: the Ninjago Monastery cave's primitive model carver (a 77.6
cube whose origin, centre or base, isn't known).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pets are walked by the server's MovementAI like enemies, but only enemies'
paths were cut at movement blockers, so pets walked through the Nimbus Station
pet ranch's "PR - Pet Blocker" walls. Their collision group (3) is the one
the pet blocker's group (18) touches in the client's collision filter, so a
pet's path now stops at them (and at navmesh carvers), whether wandering or
following its owner; a pet left behind still warps to its owner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live's RemoveItemFromInventory for a quickbuild's item cost (the FV Stone
Warrior pedestal's 5 Maelstrom Infected Bricks, taken when the build starts)
had the loot source Quickbuild and the quickbuild's object ID as the loot
source object. DLU sent loot source None with no source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The other item sources live marked in AddItemToInventoryClientSync's extra
info, with the keys and values live sent:
- mission and achievement rewards: _Metric_Mission_ID_Int and source LOT 1,
the player (225 of 225 live rewards)
- activity rewards: _Metric_Activity_ID_Int and the activity object's LOT
- vendor purchases: the vendor's LOT and _Metric_Currency_Delta_Int, the coins
paid as a negative number (left out when the item costs no coins)
- mail attachments: _Metric_Mail_ID_Int64
- traded items: _Metric_Transaction_ID_Int64, the trade's ID
Loot::GiveLoot gains overloads that pass them on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live's AddItemToInventoryClientSync for a picked up drop had
_Metric_Souce_LOT_Int in its extra info on all 2,344 live pickups: the LOT
of the DropClientLoot's source object, 1 when a player was the source
(activity rewards and chests, which drop from the player). DLU sent none.
The drop remembers its source object's LOT when it is registered for the
player, and the pickup sends it. A source object already gone when it drops sends
no key. Package contents keep sending none, as live's did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live servers put _Metric_ keys in AddItemToInventoryClientSync's extra info,
after the item's own config, saying where the items came from: the source
object's LOT (_Metric_Souce_LOT_Int, misspelt as live had it), the mission,
activity, coins paid, mail or trade. LootMetrics holds them and writes them
with the key names and name value types live used (the mail ID as type 8,
the trade ID as type 9). AddItem, ReceiveItem (its options), the new item
constructor and Item::SetCount pass them through to the message; the item
does not keep them.
RemoveItem and Item::SetCount can also say what took items away
(ItemRemovalSource), filling RemoveItemFromInventory's loot source and
source object instead of none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every LU MessageType has a struct or is listed as never used by this server; every game message struct reads;
samples of each family and of sent game messages read back; replica constructions, updates and destructions written
by the server's own serializers read back. Generator finds constructors defined in the .cpp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReplicaDecoder mirrors Entity::WriteBaseReplicaData and each component's Serialize, keeps what each network ID is
per world instance, and shows the rest as bits when no layout fits. CaptureTool links the game: game messages are
decoded (and compared in replays), and decode --cdserver reads replica packets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GameMessageDecoder reads every NetGameMsg struct (generated member lists in GameMessageFields.inc) instead of 10
hand-written ones; DisplayTooltip gets the Deserialize it lacked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live filled a NewMail notice with the mail it was about: the mail ID, the
player, the attachment (LOT -1 without one) and a count of 1, and at load sent
one notice per unread mail. DLU sent one notice with every field 0 and the
unread total as the count. The answer to NotificationRequest names the player
and no mail, with LOT -1 and the unread count, as live's did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Launching to a property started an extra clone 0 instance of the zone besides the property's own, because
the prep only carried the zone. PrepZone now carries the clone when there is one (written only then, so a
plain prep is unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture check now also reads every recorded client game message with the
struct the server reads it with (GameMessageHandler::CreateReceived) and writes
it again: it must read the whole message and give back the same bits. Game
message fields are decoded with the server's structs in the fixture tests.
A synthetic fixture, built in the test from the server's own structs (login,
position update, a client game message), goes through the export steps
(portable, anonymised, saved, read again) and passes the same checks; recorded
fixtures stay in tests/fixtures-local and are never committed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Staff arm a packet capture on the dashboard; master passes MESSAGE_CAPTURE_CONTROL ARM to
every world, auth and chat and arms its own. Each server's PacketCapture tap (dServer receive,
and a send hook in RakPeer::Send so replica constructions are seen too) records into one
preallocated chunk per server and ships sealed chunks through master on the main loop when
capture_flush_bytes or capture_flush_interval_ms is reached; past capture_buffer_max_mb the
oldest chunks are dropped and the dashboard records a gap. Nothing is armed: one flag check.
- targets: an account (from its login; packets before the login are kept per connection
and added once auth or the world knows whose they are), a character (from when it is
picked), or everything; up to 8 at once (a bit each in the record mask)
- worlds and auth record their clients' packets and the master link messages of a captured
player (session keys by name, zone transfers by request, player added/removed, migration);
chat finds the player in each packet; master records server traffic for everything
- secrets are never recorded: structs that carry them (login request, login response user
key, world validation session key, session key messages between servers) are read,
blanked and written again before recording; auth keeps only the handshake and login
- PacketDecoder: a registry by service and message id names every packet and decodes the
registered structs; CaptureBundle is the file format (DLUBNDL1, metadata, records);
CaptureTools orders records on one timeline, pulls movement out, makes bundles portable
or anonymous and diffs replays
- the dashboard keeps packet captures in message_capture_sessions (capture_kind 1) and
their packets in a file under capture_dir, one write per batch; arming is audited
- MESSAGE_CAPTURE_CONTROL/DATA only gain appended enum values and trailing fields
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frames and phase scopes in the world, auth, chat and UGC loops (packets per type, entities, physics, ghosting, replica, spawners, log flush, saves, web requests by route). Scopes at LoadPlayer, CreateEntity, each component's construction, InventoryComponent::LoadXml, script timers and a world's zone load; game database queries in the query helpers; CDClient statements timed through sqlite3_trace_v2 as CDClient <table> (CppSQLite3DB gets a handle accessor). Task 96.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client's TacArcBehavior::Cast (0x00fb2d10) sorts the targets in the arc nearest first, or by weight when
distance_weight or angle_weight is set (sortWithWeights, 0x00f58cd0: distance_weight * (max range - distance) /
max range + angle_weight * (180 - angle) / 180, heaviest first). With use_attack_priority, SortByAttackPriority
(0x00f72900) then buckets them by GetAttackPriority, lowest first, keeping that order inside each bucket; only the
DestroyableComponent answers it, and an object without one counts as 1. DoHit keeps the first max targets.
Nothing else ranks targets: enemies are taken over nearer smashables only because their attack_priority (1) is
lower than most smashables' (10). use_attack_priority is off when a behavior does not set it (TacArcBehavior::
Initialize, 0x00f9b980), as before.
The server sorted by distance only and ignored the flag, so a one-target swing hit the nearest crate instead of the
enemy behind it. OrderTargets now does the client's ordering, with equal targets in ascending id order (the order
the client's id set hands them over in); the chosen targets are still written in ascending id order (issue 1045).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DestructibleComponent.attack_priority is loaded onto the DestroyableComponent as a signed value. The client's
LWODestroyableComponent (LoadDataFromTemplate, 0x00c9f900) starts it at 1 and keeps 1 when the column is empty, and
answers GetAttackPriority with it; the server now does the same so TacArcs can order their targets by it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every server now splits its packet counts by peer: its own connections
(players on auth and worlds), its master link, or other servers (the
worlds on chat, every server on master, a world's chat link, which is
now counted too). HTTP requests carrying X-Darkflame-Server count as
another server's, the dashboard counts its own requests to the UGC
server, and each HTTP client address is counted. The report also gets
each RakNet connection's statistics (worlds name the player on it),
trimmed to the 32 busiest with the rest summed. Counting stays on the
main loop, except the dashboard's UGC fetches, which only touch the
locked recorder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MovementAIComponent::SetDestination cuts an enemy's path (chasing, tethering,
wandering) where it first walks into a wall its collision group can't cross,
so enemies no longer walk through the Sentinel camp walls, the Crux Prime and
property navmesh carvers, or the enemy blocking volumes. With nothing left to
walk the enemy stays put. Patrols along a level path, and movers without combat
AI, are left alone.
Adds a scenario test: an enemy chasing across an enemy only volume and across a
carver stops in front of it; the clear threat wall is a trigger, not a wall.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fixed simple physics object, or a phantom volume, that its data marks as a
wall for enemies (a navmesh carver, or solid with a group only enemies touch)
adds its shape to the world's movement blockers, and takes it out again when it
goes away. Only shapes the server knows are used; an unknown asset's stand in
cube would be a guess, so it is logged and left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Clear threat list Trigger Wall" names its physics test\POI_trigger_wall.hkx,
which the server only knew as env\POI_trigger_wall.hkx, so it got the 2x2x2
stand in cube and enemies walking past never touched it. Physics assets are now
also matched by file name, case blind, and the wall is its measured size
(1 x 12.98 x 20.45, base at its origin).
"Trigger Rectangle Box" (Trigger_Rectangle_Box.hkx, 8 x 8 x 4) gets its real
size too; it was a stand in cube as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Make again gets a Made by picker, /api/ugc/options lists the processor
choices and default, the settings page has the toolbox_* settings, and
/reprocessproperty takes native or toolbox-blender.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client reads RebuildComponent.activityID over a level's activityID
unless the level sets actIDovrd, but live followed the level's: the AM
Center draw bridge (LOT 12047) has activityID 12047 without actIDovrd and
dropped no loot in 4 of 4 live completions, where the template's activity 6
would have dropped quickbuild rewards. DLU already does what live did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A quickbuild's HasItem preconditions took their items only when the build
completed and never gave them back. Live took them when the build started
and gave them back when it was cancelled: the FV Stone Warrior pedestal
(LOT 8551, precondition 99: 5 of LOT 6194) took the items at Building 5
times and added them back with the loot source Quickbuild on each of the 3
cancels in the live captures.
Preconditions now report their item costs instead of removing items while
checking. A quickbuild takes them at the start, gives them back on cancel
or a reset during the build, keeps them on completion, and gives them back
when the builder leaves the world mid-build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent AddItemToInventoryClientSync with loot_type_source Pickup for the
items of opened packages (token bags, surprise packs, the imaginite bag;
all 98 items added after a live UseNonEquipmentItem). DLU used Consumption.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent quickbuild completion loot (about 190 samples) and the dragon,
BONS, spider queen and Frakjaw chest and wishing well rewards with the
player who earned them as the DropClientLoot source and owner,
use_position true and the spawn position at the object. DLU used the object
as the source, so use_position was false.
DropActivityLoot now uses the player as the source by default. The growing
flowers and the VE mission console have no live samples and keep the
object as the source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ActivityRewards coins were always read from CurrencyTable npcminlevel 1.
Live used the reward row's ChallengeRating as the npcminlevel, and level 1
when the currency index has no row for that level:
- FV foot races (ChallengeRating 4, index 1) gave 36 and 48 coins, which
only level 4 (30-50) fits; level 1 is 3-5.
- Frakjaw's chest (activity 58, ChallengeRating 6) gave 250 each to a team
of 2: its currency indices 123-126 only have a level 6 row (500), so DLU
gave nothing.
- Quickbuilds, wishing wells and chests have ChallengeRating 1; survival
and the shooting galleries have ratings with no row and keep level 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DLU sent a DropClientLoot for 0 coins with every drop whose coin range was
0, such as item-only smashables and scripted drops. None of the 3163 live
currency drops was for 0 coins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live sent a drop's currency DropClientLoot before its item drops in 1498 of
1502 live drop groups that had both. DLU sent the coins last.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The third ray backend, so every machine has a library for it: Embree on x86
CPUs (embree), HIPRT on AMD and NVIDIA GPUs (hiprt), Embree through SYCL on
Intel Arc and Xe GPUs (embree-gpu).
- CMake option DLU_EMBREE_SYCL (off). dUgcServer/EmbreeSycl is a project of
its own built by a SYCL compiler (DLU_SYCL_CXX; icpx through ONEAPI_ROOT or
the path, or the open source DPC++'s clang++ through DPCPP_ROOT) as an
external project: Embree 4.4 with EMBREE_SYCL_SUPPORT, linked statically and
bound inside (-Bsymbolic, only its C functions exported, so it never meets
the servers' own Embree), and the GPU kernels (nearest hit skipping a ray's
triangle, any hit), into libdlu_embree_sycl next to the servers
- UgcRaysEmbreeGpu loads it the first time embree-gpu is asked for; one GPU for
the process (embree_gpu_device picks it), the workers take turns, the
occlusion rays in batches as for hiprt
- without the build, the library or a supported Intel GPU it falls back to
embree and says why (the UGC server's log at start, --make-model on stderr)
- the option names, the settings page, the dashboard's picker, /reprocessproperty
Verified here: the default build and ctest; the SYCL build with the open source
DPC++ 7.1.0 (compiles, links against oneAPI's libsycl.so.9, exports only its C
functions); on this machine (no Intel GPU) it loads, finds no GPU and falls
back to embree. Not verified: tracing on an Intel GPU (none here).
Check: on a machine with an Intel Arc or Xe GPU and oneAPI, configure with
-DDLU_EMBREE_SYCL=ON and run UgcServer --make-model x.lxfml out embree-gpu;
the UGC tests then compare it with Embree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Embree 4 (always built) replaces the UGC server's own bounding volume
hierarchies (the nearest-hit one and the occlusion rays' any-hit one), with no
fallback to them. ray_backend is embree (default) or hiprt (optional build;
Embree when it can't be used).
Settings, stored options and stats that say builtin still read: it is embree
(UgcRays::Parse, UgcProcessOptions::Parse). The dashboard's picker,
/reprocessproperty and --make-model offer embree and hiprt.
Tests: the backends are compared with Embree (hiprt when built), Embree against
rays whose hits are known, and the clutter's occlusion against what the old
hierarchy worked out (296 vertices summing to 114.5, 26 open, 183 dark); the
pinned model hashes are unchanged with Embree.
Check: ray_backend=builtin in an ini still starts and uses embree; the
settings page offers Embree and HIPRT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The path tracer that imitated LU Toolbox's Remove Hidden Faces took 17 to 100
times as long as the renders from 42 directions around the model, too heavy to
use. It goes with its settings (hsr_method, hsr_samples, hsr_bounces,
hsr_sample_spacing, hsr_min_points), its side by side tracing for GPUs and its
tests. The renders (UgcRender::VisibleFromAround) remove hidden faces as before
that method existed; their size is hsr_resolution (1024), and the memory
estimate counts their buffers again.
The hidden-face method is no longer a processing option: the dashboard's picker,
/reprocessproperty and --make-model take only the ray backend (the occlusion's)
and denoising. Options stored before (made_options, process_options,
ugc_process_runs, --make-model arguments) that name toolbox or fast still
parse; the word is skipped.
Check: the settings page has no hsr_method or path settings and has
hsr_resolution; /reprocessproperty embree toolbox still works (toolbox
ignored); models made with the defaults keep their hashes (UGC tests).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Staff can make models again with other processing options than the UGC
settings' (ray backend, hidden-face method, denoising) and every make records
what made it, for comparing the options.
- migration mysql 99 / sqlite 82 (ugc_process_options): ugc.process_options
(picked for the next make, cleared once made), ugc.made_options (what made
the current files) and ugc_process_runs (every successful make: options,
wall and CPU time, hidden faces', occlusion's and icon's time, bricks,
triangles before and after)
- IUgc: ResetUgcModelProcessing and ResetPropertyUgcModelProcessing take the
options; PendingModel carries them; RecordUgcModelRun, GetUgcRunSummaries;
list entries have madeOptions and processOptions
- the UGC server applies a job's options over its settings and records the run
- /api/ugc/reprocess takes options ("embree fast oidn"); /api/ugc/options
lists the choices, the settings' defaults and averages per combination
- /reprocessproperty [builtin|embree|hiprt] [toolbox|fast] [off|oidn], any
order, all optional
Check: run the migration on MySQL and SQLite; /reprocessproperty embree fast
on a property, then the models' made_options and ugc_process_runs rows;
/api/ugc/options.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Item::RemoveFromInventory ended with `delete this`, so every caller that still used the item afterwards (SetCount(0)
returning into its caller, loops that remove several items, proxies purged while their parent is handled) touched
freed memory. The inventory now takes the removed item and frees it at the inventory component's next update, or
with the inventory.
Check in game: sell, drop, delete, trade, mail and use up stacks (including the last of a stack); unequip and
remove an item set piece and a proxy-bearing item (rocket, modular car); donate items; nothing crashes and the
inventory shows the right counts after relogging.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client's TacArcBehavior::DoHit (0x00fb10c0) writes the closest max-targets ids from a set, so ascending and
each once, then the action data per id in that same order; DoUnserializeBS (0x00fb26a0) reads them the same way and
skips empty ids. The server handled the targets in list order and skipped ids whose object it could not find
without reading their action data, so every later target read the wrong bits (several pirates under a Doom Slicer).
Handle now reads the action for every listed id in ascending order, and the server's own casts write ids and actions
in ascending order after picking the closest targets.
TacArcBehavior::Cast (0x00fb2d10): a picked target that passes the filter gets the action with no TacArc data (the
server's own cast now calculates that action instead of handling it); otherwise the target is dropped, and an arc
measured from the target's position writes nothing.
Check in game: Doom Slicer and multi-target katanas on groups of pirates/admirals damage each of them; apes still
take damage during their stun; enemies with arc attacks (apes, Maelstrom horsemen) still hit players.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client's AreaOfEffectBehavior::Cast (0x004ec590) writes every target id, then runs and writes the action once
per unique target in ascending id order. The server ran it for every listed target in list order, so a target
listed twice (the caster with a magnet, Everlasting items' refill) was handled twice, and later targets' data was
read against the wrong target. Handle and the server's own Calculate now go through the unique ids in ascending
order. Check: Thumpin' Bass / Flowin' MC refill once; Shinobi charge with a magnet gives imagination once; area
attacks on several enemies still hit each of them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When staff approve or reject a character name while the player is online, the player gets a private announcement
popup ("Name approved" / "Name not approved") as well as the chat line. Players who are offline still aren't told
at their next login. Check: approve and reject a pending name while its player is online.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Venture vision from an equipped item was sent while the character loaded, before the client's UI existed, so after a
login the minimap stayed empty until the item was equipped again (a world transfer keeps the UI, so it worked
there). The player's active venture vision effects are sent again once the player has loaded. Check: wear the
Venture Vision helmet, log out and back in: the minimap shows the icons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Members' clients read the chat text up to a terminating zero, as the client itself sends it; /g sent the text
without one, so the client read past it and showed extra characters ("test0>"). Check: guild chat shows exactly
what was typed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>