# Retail projectile, PhysicsScript, and portal-effect pseudocode **Date:** 2026-07-13 **Build:** Sept 2013 EoR retail client, matching named PDB/decomp **Purpose:** Oracle for the acdream inbound missile and DAT-driven VFX port. This note translates the named retail functions into implementation-oriented pseudocode before any C# port. Addresses refer to `docs/research/named-retail/acclient_2013_pseudo_c.txt`. Retail remains the behavioral oracle; ACE, ACViewer, and Holtburger are interpretation and wire cross-checks only. ## 1. PhysicsDesc wire order Oracle: `PhysicsDesc::UnPack` at `0x0051DDD0`. ```text unpack PhysicsDesc: destroy prior owned buffers bitfield = read u32 state = read u32 if bitfield has Movement (0x00010000): length = read u32 movementBytes = read length bytes if length > 0: autonomousMovement = read u32 else if bitfield has AnimationFrame (0x00020000): animationFrame = read u32 if Position (0x00008000): position = unpack Position if MTable (0x00000002): motionTableId = read DID if STable (0x00000800): soundTableId = read DID if PeTable (0x00001000): physicsScriptTableId = read DID if CSetup (0x00000001): setupId = read DID if Parent (0x00000020): parentGuid, locationId = read u32, u32 if Children (0x00000040): count = read u32 repeat count: childGuid, childLocationId = read u32, u32 if ObjScale (0x00000080): objectScale = read f32 if Friction (0x00000100): friction = read f32 if Elasticity (0x00000200): elasticity = read f32 if Translucency (0x00040000): translucency = read f32 if Velocity (0x00000004): velocity = read vec3 f32 if Acceleration (0x00000008): acceleration = read vec3 f32 if Omega (0x00000010): omega = read vec3 f32 if DefaultScript (0x00002000): defaultScriptType = read u32 if DefaultScriptIntensity (0x00004000): defaultIntensity = read f32 timestamps = read exactly nine u16 values in this order: Position, Movement, State, Vector, Teleport, ServerControlledMove, ForcePosition, ObjDesc, Instance align cursor to four bytes ``` Absent optional values are semantically different from present zero values and must remain nullable in the parsed representation. ACE's `PhysicsDescriptionFlag`, and Holtburger's object-description reader, confirm the same order and nine-value timestamp tail. ## 2. Installing a PhysicsDesc Oracle: `CPhysicsObj::set_description` at `0x00514F40` and `CPhysicsObj::InitDefaults` at `0x005139D0`. ```text InitDefaults(setup): if setup.DefaultScript != 0: play that direct PhysicsScript DID if setup.DefaultMotionTable != 0: install it if setup.DefaultSoundTable != 0: replace sound table if setup.DefaultPhysicsScriptTable != 0: replace typed-effect table if object is static: set HasDefaultAnim / HasDefaultScript as appropriate register static animation when either applies set_description(desc): install motion table release current sound table if desc.SoundTable != 0: load it release current PhysicsScriptTable unconditionally if desc.PeTable != 0: load it install movement buffer or placement frame set_state(desc.State, applySideEffects=true) install scale, valid friction, elasticity, translucency set_velocity(desc.Velocity) install desc.Omega install typed default-script type and intensity copy all nine timestamps into the object's update-time array ``` The network PeTable therefore overrides the Setup table even when the network value is absent/zero: a live object does not fall back to the Setup table after `set_description`. The network acceleration vector is unpacked but is not installed here; retail recalculates normal acceleration from physics state. ## 3. PhysicsState values and state transitions The values cross-check exactly with ACE's `PhysicsState` enum: ```text Static=0x1, Ethereal=0x4, ReportCollisions=0x8, IgnoreCollisions=0x10, NoDraw=0x20, Missile=0x40, Pushable=0x80, AlignPath=0x100, PathClipped=0x200, Gravity=0x400, Lighting=0x800, ParticleEmitter=0x1000, Hidden=0x4000, ScriptedCollision=0x8000, HasPhysicsBsp=0x10000, Inelastic=0x20000, HasDefaultAnim=0x40000, HasDefaultScript=0x80000, Cloaked=0x100000, ReportAsEnvironment=0x200000, EdgeSlide=0x400000, Sledding=0x800000, Frozen=0x1000000 ``` Oracle: `CPhysicsObj::set_state` at `0x00514DD0` and `CPhysicsObj::set_hidden` at `0x00514C60`. ```text set_state(next): changed = current XOR next current = next if Lighting changed: initialize or destroy part-array lights if NoDraw changed: apply set_nodraw before Hidden handling if Hidden changed: set_hidden(next has Hidden) set_hidden(true): set Hidden resolve PS_Hidden at intensity 1 through current table; enqueue it set every attached child NoDraw and update its part array if ReportCollisions: end reporting and clear the bit set IgnoreCollisions hide root object in its cell/render world set_hidden(false): clear Hidden resolve PS_UnHide at intensity 1 through current table; enqueue it clear every attached child's NoDraw, unconditionally as retail does clear IgnoreCollisions if ReportCollisions is clear: set it and start reporting enter/unhide root part array and cell object ``` `CPhysicsObj::CPhysicsObj` at `0x005123F0` initializes state to `0x00400C08` (the assignment is at `0x00512508`). That value is `EdgeSlide | Lighting | Gravity | ReportCollisions`; it does **not** contain `Hidden` (`0x00004000`). Consequently, an ordinary initially-visible CreateObject does not call `set_hidden` and must not manufacture an UnHide script. UnHide occurs only for a real state-bit transition from Hidden to visible, while an explicit F755 UnHide remains a separate typed effect packet. Hidden is never object destruction: scripts and particles continue to exist while the mesh and interaction surface are suppressed. `SmartBox::DoSetState` at `0x004520D0` applies the state only when its u16 timestamp is newer using the retail wrap-safe comparison. The same comparator is used by State, Vector, Movement, Position, and related timestamp-gated channels (`SmartBox::DoVectorUpdate` at `0x004521C0` is the second executable cross-check): ```text is_newer(incoming:u16, stored:u16): distance = abs((int)incoming - (int)stored) if distance > 0x7FFF: return incoming < stored return stored < incoming ``` Equality is stale. Up through a distance of `0x7FFF`, the numerically higher stamp wins. At exactly `0x8000` and throughout the wrap half, the numerically lower stamp wins. This exact boundary must be ported; a generic half-range subtraction helper can choose the opposite result at `0x8000`. ### 3.1 Event freshness and object generations Additional oracles: - `SmartBox::HandleReceivedPosition` at `0x00453FD0` - `SmartBox::HandleObjDescEvent` at `0x00453340` and `SmartBox::UpdateVisualDesc` at `0x00451F40` - `SmartBox::HandleDeleteObject` at `0x00451EA0` ```text on CreateObject(desc): if no live generation: copy all nine desc timestamps wholesale else if desc.Instance is newer: begin a new generation and replace all nine else if current Instance is newer: reject the stale CreateObject else: keep the object and route each PhysicsDesc branch through its ordinary per-channel handler; do not authorize the whole snapshot on State / Vector / Movement / ObjDesc: require the exact live Instance require a strictly newer channel timestamp apply only after both gates pass on DeleteObject: reject unconditionally when guid is the local player require the exact live Instance before either object projection is removed on ParentEvent(parent, child): require the parent's exact live Instance strictly advance the child's Position timestamp apply parent + placement without creating a second position counter on PickupEvent(object): require the exact live Instance strictly advance the shared Position timestamp unparent and leave the world, retaining the object and its timestamps on Position(localPlayer): require the exact live Instance if ForcePosition is newer: store ForcePosition if incoming Teleport exactly equals stored Teleport: assign Position directly, even when equal or older preserve the current heading do not advance Teleport BlipPlayer via SetPositionSimple (preserve active motion, velocity, contact state, and PositionManager stick) if Contact AND OnWalkable are set and position is valid: send PositionEvent immediately from the post-blip canonical Position return tentatively advance Position, requiring strictly newer if stored Teleport is newer than incoming Teleport: restore the old Position timestamp and reject if incoming Teleport is newer: advance Teleport PositionPack supplies placement 0 and velocity (0,0,0) when their flags are absent unset the parent if the object has no active animations: apply the decoded placement id if object is remote: MoveOrTeleport receives the decoded velocity else if Teleport advanced: teleport player and explicitly zero velocity else: constrain/interpolate without installing packet velocity apply the position ``` `PositionPack::UnPack` at `0x00516740` is the source of the absent-as-zero defaults; retaining a prior placement or remote velocity when the wire flag is absent is not retail behavior. The immutable parsed type still preserves presence so diagnostics and conformance tests can distinguish absent from an explicit zero. `HandleReceivedPosition` consumes those defaults only after the timestamp gate admits the normal path. ForcePosition returns before placement or packet velocity is consumed and therefore preserves the player's live heading and velocity. `SmartBox::HandlePlayerTeleport` at `0x00452150` handles standalone F751: ```text on PlayerTeleport(sequence): if sequence is older than current Teleport timestamp: reject equality and newer sequences are accepted enter the waiting/portal state do not advance Teleport here ``` The accepted destination Position packet advances `TELEPORT_TS`. Parsing F751 must therefore never publish its sequence directly into outbound AP state. Retail queues a packet whose Instance belongs to a not-yet-created future generation. `ParentAttachmentState` now does so for ParentEvent, retaining the parent generation through atomic object replacement. Until `LiveEntityRuntime` owns the complete mixed pending queue, acdream still drops future Movement, Position, State, Vector, Pickup, Delete, and ObjDesc packets without touching the current generation (AD-32); critically, it never adopts the future Instance into the current object. ## 4. Direct and typed effect packets Oracles: - `SmartBox::HandlePlayScriptID` at `0x00452020` - `SmartBox::HandlePlayScriptType` at `0x00452070` ```text F754 direct packet: targetGuid:u32, physicsScriptDid:u32 F755 typed packet: targetGuid:u32, scriptType:u32, intensity:f32 on either packet: if target object is not materialized: QueueBlobForObject(targetGuid, whole packet) and return queued if F754: play direct PhysicsScript DID if F755: resolve type+intensity through target's current table, then play ``` The queue preserves packet order. F754 never goes through the table. F755 must preserve unknown raw type values and the incoming float bits until resolution. ACE's `GameMessageScript` confirms the 16-byte opcode+GUID+type+float F755 wire shape. Synthetic, non-DAT packet fixtures fixed for the parser port are: ```text F754: 54 F7 00 00 | 42 00 00 50 | 99 00 00 33 opcode GUID 50000042 script DID 33000099 F755: 55 F7 00 00 | 42 00 00 50 | A5 A5 A5 A5 | 34 12 C0 7F opcode GUID 50000042 unknown raw type NaN payload bits ``` Step 1 consumes these in parser tests so exact packet lengths and raw float bits are exercised. Step 3 constructs synthetic raw Animation and PhysicsScript fixtures through the compatibility-reader tests; no copyrighted installed-DAT bytes are committed. ## 5. PhysicsScriptTable selection Oracles: - `PhysicsScriptTable::GetScript` at `0x00521E40` - `PhysicsScriptTableData::GetScript` at `0x00521A20` ```text resolve(type, intensity): data = hash lookup by exact type if absent: invalid DID for entry in stored DAT order: if intensity <= entry.Mod: return entry.ScriptId return invalid DID ``` `Mod` is an upper threshold, not a random weight. Do not sort entries and do not fall back to the first. Equality matches. Matching x86 at `0x00521A3B` uses `test ah, 0x41; jnp return`: one tested status bit means intensity is less than the entry (C0) or equal to it (C3), while unordered/NaN sets both tested bits and continues. NaN therefore resolves to no script; an intensity above every stored threshold also resolves to no script. Consequently negative infinity selects the first finite threshold, while positive infinity and NaN select none; the resolver must preserve and compare the incoming float rather than blanket-rejecting all non-finite values. ## 6. ScriptManager queue Oracles: - `ScriptManager::AddScriptInternal` at `0x0051B310` - `ScriptManager::NextHook` at `0x0051B3F0` - `ScriptManager::UpdateScripts` at `0x0051B480` `PhysicsScript::UnPack` at `0x005218B0` first sorts its decoded hook records by ascending `StartTime` with `PhysicsScriptData::Sort` at `0x00521600`. This sort applies to PhysicsScript hook time, not to PhysicsScriptTable `Mod` thresholds (which must remain in stored DAT order). ```text add(script): node = new ScriptData node.start = queue empty ? Timer.current : tail.start + tail.script.length append node, including duplicate script/owner pairs if it became the head: hookIndex = -1 nextHookTime = node.start + firstHook.start update: while a current node exists and currentTime >= nextHookTime: hook = NextHook() if hook exists: execute hook against owner object else: pop and release completed head initialize next node or mark queue empty ``` Each owner has one serial FIFO, not a deduplicated set or a concurrent list. Different owners have independent queues. A long frame executes all hooks that became due, in order. ## 7. CallPES and default-script hooks Oracle: `CPhysicsObj::CallPES` at `0x00511AF0`. ```text CallPES(scriptDid, maximumPause): if maximumPause < 0.0002 seconds: if object is in a cell: enqueue script immediately else: delay = uniformRandom(0, maximumPause) insert a timed CALL_PES object hook at the head of the object-hook list when due, enqueue the PhysicsScript on this owner's FIFO ``` The timed object-hook list is traversed head-to-next. Multiple delayed CallPES hooks are therefore observed newest-first until their individually randomized times become due; this is separate from the PhysicsScript owner's serial FIFO. `DefaultScriptHook` resolves the owner's PhysicsDesc default type/intensity. `DefaultScriptPartHook` locates the child attached at the requested parent part and resolves the child's own table/default, not the parent's. ## 8. Particle emitter semantics Oracles: - `CreateBlockingParticleHook::Execute` at `0x00526EF0` - `ParticleManager::CreateBlockingParticleEmitter` at `0x0051B8A0` CreateBlockingParticle inherits the complete CreateParticle payload: ```text emitterInfoDid, partIndex, offsetFrame(position+orientation), logicalEmitterId ``` ```text normal CreateParticle: nonzero logical id replaces the existing live emitter with that id zero id creates a new anonymous emitter blocking CreateParticle: if nonzero logical id names a live emitter: do nothing and return 0 otherwise create exactly as normal ``` "Blocking" does not pause the animation sequencer. The current Chorizite.DatReaderWriter model omits this inherited payload and can misalign the following hook, so acdream needs one centralized raw-hook compatibility reader for PhysicsScript and Animation files. Ordinary hook types remain delegated to the package decoder. The complete offset Frame must be decoded and retained for schema fidelity, but retail particle emission consumes only its origin. `Particle::Init` at `0x0051C930` adds that origin to the particle start point and transforms the translation plus emitter vectors through the current owner part/root frame; it does not read the hook-offset quaternion. `ParticleEmitter::UpdateParticles` at `0x0051D180` then chooses either the saved start frame or the current live part/root frame according to the DAT parenting mode. Emitter attachment is therefore evaluated as: ```text ownerFrame = partIndex == -1 ? rootWorld : animatedPartLocal * rootWorld anchorPosition = transformPoint(hookOffset.origin, ownerFrame) ``` The hook-offset quaternion is not applied to particle emission unless a different retail hook consumer proves otherwise. For non-parent-local particles, only future emissions follow a moving anchor; already-born particles remain in world space. Parent-local particles continue to follow their owner. ## 9. Per-frame physics and projectile flags Oracles: - `CPhysicsObj::UpdatePositionInternal` at `0x00512C30` - `CPhysicsObj::UpdateObjectInternal` at `0x005156B0` - `CPhysicsObj::update_object` at `0x00515D10` - `CPhysicsObj::UpdatePhysicsInternal` at `0x00510700` - `CPhysicsObj::calc_acceleration` at `0x00510950` - `CPhysicsObj::get_object_info` at `0x00511CC0` - `Frame::set_vector_heading` at `0x00535DB0` ```text update_object: if parented, out of world, or Frozen: clear transient Active and return without an object tick elapsed = Timer.current - update_time PhysicsTimer.current = update_time if elapsed <= 0.0002 seconds: update_time = Timer.current return if elapsed > 2 seconds: update_time = Timer.current return while elapsed > MAX_QUANTUM (1/5 second): PhysicsTimer.current += MAX_QUANTUM UpdateObjectInternal(MAX_QUANTUM) elapsed -= MAX_QUANTUM if elapsed > MIN_QUANTUM (1/30 second): PhysicsTimer.current += elapsed UpdateObjectInternal(elapsed) update_time = PhysicsTimer.current UpdateObjectInternal(dt): UpdatePositionInternal(dt, candidateFrame) if Setup has one or more physics spheres: align path when applicable sweep/transition from committed frame to candidate frame commit transition result and update children else: commit candidate frame directly; do not create a dummy-sphere sweep update detection/target/movement/position managers ParticleManager.UpdateParticles() ScriptManager.UpdateScripts() UpdatePositionInternal(dt, candidateFrame): unless Hidden: advance part array / animation and produce its frame offset apply PositionManager offset even when Hidden combine object root frame with animation offset unless Hidden: UpdatePhysicsInternal(dt, candidateFrame) process timed object hooks even when Hidden UpdatePhysicsInternal(dt, frame): speedSquared = dot(velocity, velocity) if speedSquared > 0: if speedSquared > 50^2: normalize velocity and clamp to 50 calculate friction using dt and the clamped pre-friction speedSquared if speedSquared < (0.25^2 + 0.0002): zero velocity frame.position += velocity*dt + 0.5*acceleration*dt^2 velocity += acceleration*dt frame.rotation = globalRotate(frame.rotation, omega*dt) calc_acceleration: if transient has Contact AND OnWalkable AND state lacks Sledding: acceleration=0 and omega=0 else if Gravity is clear: acceleration=0 (omega is preserved) else acceleration=(0,0,PhysicsGlobals.gravity) ``` The `Sledding` constant (`0x00800000`) in `calc_acceleration` is confirmed from matching-binary disassembly at `0x00510961`; the named lift misidentified it as a string address. The near-stop test is deliberately based on the original or 50-unit-clamped `speedSquared`, not a speed recomputed after friction. The same matching executable resolves the `update_object` globals that the named lift displays as zero: `MAX_QUANTUM` is 0.2 seconds and `MIN_QUANTUM` is 1/30 second. Thus a long but valid frame is replayed as 0.2-second catch-up steps, a final remainder at or below 1/30 second stays accumulated for the next tick, and a gap above 2 seconds is discarded instead of simulating a huge projectile jump. AlignPath uses the full normalized 3D movement vector through `Frame::set_vector_heading`; it is not yaw-only. `get_object_info` adds transition flag `0x8` when PhysicsState.Missile is set. That is PathClipped behavior. It does not add PerfectClip, so ordinary missiles must not implicitly arm PerfectClip. ## 10. Missile collision exclusion Oracle: `OBJECTINFO::missile_ignore` at `0x0050CEB0`. ```text missile_ignore(other): if other is a missile: ignore if moving object is not a missile: do not ignore if other.id == designatedTargetId: do not ignore if other is ethereal and has a weenie: ignore if designatedTargetId != 0 and other is a creature: ignore otherwise collide ``` The moving object's target ID initializes to zero. The client must not infer it from the currently selected UI target. Retail builds the transition sweep from Setup physics spheres when present, scaled by object scale. Although the lower-level `transition` routine has a dummy-sphere path, ordinary `UpdateObjectInternal` does not call `transition` when `GetNumSphere() == 0`; it commits the frame without a collision sweep. Projectile shape selection must therefore come from Setup data, not a mesh-size guess or a synthesized dummy radius. ## 11. Installed-DAT and fixture audit The installed DAT files at `C:\Turbine\Asheron's Call` are private interoperability inputs and are not copied into the repository. The audit is reproducible with: ```powershell dotnet run --project tools/ProjectileVfxAudit/ProjectileVfxAudit.csproj -c Release ``` The command is a conformance gate: missing required assets, changed hashes or sphere shapes, parse failures, a changed recall-motion closure, or a changed raw CreateBlocking inventory produce a nonzero exit code. The projectile WCID mapping comes from the local ACE weenie corpus at `C:\Users\erikn\source\repos\acdream\references\weenies(single file).json` by selecting WCIDs 300, 305, 7276-7282, and 23144 and reading DID properties Setup (1) and PhysicsEffectTable (22). | WCID | Name | Setup | PhysicsScriptTable | |---:|---|---:|---:| | 300 | Arrow | `0x02000124` | `0x3400002B` | | 305 | Quarrel / crossbow bolt | `0x0200012A` | `0x3400002B` | | 7276 | Acid Stream | `0x020003F6` | `0x34000082` | | 7277 | Flame Bolt | `0x0200040D` | `0x34000005` | | 7278 | Force Bolt | `0x0200087D` | none in template | | 7279 | Frost Bolt | `0x020003F4` | `0x34000080` | | 7280 | Lightning Bolt | `0x02000880` | `0x34000081` | | 7281 | Shockwave | `0x020003FA` | none in template | | 7282 | Whirling Blade | `0x020003FC` | none in template | | 23144 | Tusker Fist | `0x02000EAE` | none in template | ### 11.1 Collision shapes Every audited projectile Setup has exactly one ordinary sphere and no cylinder spheres. A sphere has a Setup-local origin and radius, not an orientation. | Setups | Sphere origin | Radius (world units) | |---|---:|---:| | Arrow, quarrel, acid, flame, frost, shockwave, whirling blade, Tusker Fist | `(0, 0, 0)` | `0.10` | | Lightning Bolt `0x02000880` | `(0, 0, 0)` | `0.15` | | Force Bolt `0x0200087D` | `(0, -0.165, 0.1045)` | `0.102` | This pins a one-sphere invariant for the required projectile set, but disproves a centered-sphere shortcut: Force Bolt's sphere is offset. Step 6 must preserve the Setup-local origin and multiply both origin and radius by object scale. ### 11.2 Projectile table order and chains The relevant entries, in stored order, are: - Arrow/quarrel table `0x3400002B`: Create `(1 -> 0x33000719)`, Destroy `(1 -> 0x33000233)`, and DisappearDestroy `(1 -> 0x33000302)`. It has no Launch or Explode entry. - Flame table `0x34000005`: ProjectileCollision `(1 -> 0x33000109)`; Launch `(0 -> 0x3300011B, 0.5 -> 0x3300011C, 1 -> 0x3300011D)`; Explode `(0 -> 0x3300011E, 0.5 -> 0x3300012B, 1 -> 0x33000120)`. - Frost `0x34000080`: collision `0x33000109`, Launch `0x330008BF`, Explode `0x33000119`, each at threshold 1. - Lightning `0x34000081`: collision `0x33000109`, Launch `0x330008C0`, Explode `0x3300013E`, each at threshold 1. - Acid `0x34000082`: collision `0x33000109`, Launch `0x330008C2`, Explode `0x33000132`, each at threshold 1. - Tusker Fist has no network/template table, but its Setup `0x02000EAE` carries direct `DefaultScript=0x33000D77`. Setup initialization therefore runs that direct PES: Scale, CreateParticle emitter `0x3200071B` on part 0, and SoundTable, all at time zero. The Launch scripts contain CreateParticle trails plus sound-table hooks; the collision script contains SetLight; Explode scripts contain particles and sound. This confirms that physical and spell missiles use the same generic PhysicsScript/hook sinks rather than a special render-only trail path. ### 11.3 Recall and Hidden/UnHide `0x10000153` is the ACE/local-DAT motion command, not an Animation DID. In the human MotionTable `0x09000001`, the NonCombat link resolves it to Animation `0x030009BF`, frames `0..end` at `9.96` frames/second. At frame 16 that Animation directly creates emitter `0x3200078E`, attached to root part `-1` with logical ID 19 and zero offset, then plays SoundTweaked hooks at frames 16, 46, 76, and 116. There is no CallPES in this human recall chain. Other local creature MotionTables resolve the same command to `0x03000047`, `0x0300098F`, or a `0x03000C34 -> 0x030009BF -> 0x03000C34` sequence. Installed tables containing both typed transitions group into two asset pairs: - Tables `0x34000004`, `0x34000064`, `0x34000066`, `0x34000076`, `0x34000095`, `0x340000B2`, `0x340000B4`, `0x340000C9`, `0x340000CA`, `0x340000CB`, `0x340000CD`, and `0x340000D1`: UnHide `(1 -> 0x3300032F)`, Hidden `(1 -> 0x33000331)`. - Tables `0x34000063`, `0x340000A9`, `0x340000B9`, and `0x340000BB`: UnHide `(1 -> 0x330002E0)`, Hidden `(1 -> 0x330002F3)`. This installed metadata also validates the table comparison recovered from x86: state transitions call intensity 1 and the entries have Mod 1, so equality must match. ### 11.4 CreateBlocking and fixture provenance An aligned raw retail-layout scan finds CreateBlocking hooks in PhysicsScripts `0x33000AEA` through `0x33000AF8` inclusive and none in Animations. The scan validates the type, a retail direction value, and the `0x32xxxxxx` emitter DID prefix directly from raw bytes, so the defective stock decoder cannot skip or misalign a following hook. The repository commits only synthetic, noncopyrighted fixtures: - `tests/AcDream.Core.Net.Tests/Messages/ProjectileVfxPacketFixtures.cs`: exact F754/F755, a broad projectile-field CreateObject, isolated Movement and AnimationFrame branches, and absent-versus-present-zero PeTable shapes. Step 1 tests include truncation at each gated cursor. - `tests/AcDream.Content.Tests/Vfx/ProjectileVfxDatFixtures.cs`: a synthetic raw PhysicsScript and Animation container, each containing a complete CreateBlocking payload followed by an AnimationDone hook at the exact expected cursor. `RetailDatLoaderTests` verifies both corrected containers, ordinary-hook parity with the package decoder, malformed-entry rejection, and representative installed blocking scripts. Representative installed raw-entry identities are pinned as metadata, not copied bytes: | DID | Length | SHA-256 | |---:|---:|---| | Arrow Setup `0x02000124` | 812 | `630D19A9C79137F7BA90D01448B03E8F44D655FF77A4725792CB5660930A079E` | | Quarrel Setup `0x0200012A` | 776 | `ED529E834CC1799B5655549200F7D1060FB62A707988E7B7D4FBED2D9C4BA0D1` | | Force Bolt Setup `0x0200087D` | 344 | `B30469764673F55BCBB4F123559256299C34D819974EAB42AC476E3FEED6EAE8` | | Flame table `0x34000005` | 88 | `86906B0EDDC9AA2242091EAC7A22C88FB2150F0773B6CCA1FA5E4AAEAFF2A386` | | Recall Animation `0x030009BF` | 143560 | `21EF99FE5A760216AF48946B9C41D0FA7E4B94297B8DEC8B91C41BB8C7A0D02D` | | Blocking PES `0x33000AEA` | 64 | `C0D75973ADB55386ED6AC860C6D08EC43862ABE8CE07AC4E942EBA31DAF61320` | | Tusker Fist default PES `0x33000D77` | 108 | `9F27628BDEA6B05C95CFADD037746AB380542DF8B040265EF881B3366CB8E12E` | ACE's current `SpellProjectile.Setup` cross-check confirms that server-created spell projectiles set ReportCollisions, Missile, AlignPath, and PathClipped; rotating projectile definitions instead set omega and clear AlignPath. On impact ACE broadcasts authoritative SetState, typed effect scripts, velocity zero, and eventual deletion. The client therefore predicts presentation but does not invent impact or destruction. The recall Animation's direct DAT hooks must be executed rather than replaced with a guessed cloud. Hidden/UnHide state transitions and explicit typed scripts remain distinct packet-driven events. A normal visible spawn alone is not evidence for an UnHide materialization effect; the observer portal-in sequence must be validated against its actual inbound state/effect packets during Step 8. ## 12. Secondary-reference cross-checks The following secondary implementations were inspected after the named-retail oracle. They confirm wire/schema or server-authority details; none overrides a retail disagreement: - ACE `Source/ACE.Entity/Enum/PhysicsDescriptionFlag.cs` and `Source/ACE.Entity/Enum/PhysicsState.cs` confirm PhysicsDesc gates and state bits. - ACE `Source/ACE.Server/Network/GameMessages/Messages/GameMessageScript.cs` confirms F755's opcode/GUID/raw type/float layout. - ACE `Source/ACE.Server/WorldObjects/SpellProjectile.cs` confirms the server creates missile bodies and remains authoritative for impact state, effect, vector correction, and deletion. - Holtburger `crates/holtburger-protocol/src/messages/object/messages/description.rs` confirms PhysicsDesc field order and the nine trailing sequences. - ACViewer `ACViewer/Entity/AnimationHooks/CreateParticleHook.cs` and `ACViewer/Physics/Scripts/PhysicsScriptTableData.cs` were used as schema interpretation aids only; their behavior is not substituted for retail's blocking-emitter or stored-order threshold rules. Repository links: - - - - - - - ## 13. Port invariants 1. Server GUID is translated once to a canonical local entity ID before an effect reaches render, audio, light, translucency, or particle sinks. 2. Logical entity creation/destruction is separate from spatial rebucketing. 3. Server Position/Vector/State/Delete packets remain authoritative. 4. No client-side impact, explosion, damage, or deletion is synthesized. 5. Per-object frame ordering preserves retail: animation/timed object hooks, movement commit and child updates, particle tick, then PhysicsScript tick. A script-created emitter begins particle simulation on the following tick. 6. Hidden suppresses mesh/collision/interaction but does not destroy the effect owner. 7. DAT assets determine models, colors, timing, and attachment behavior. 8. Every ported branch receives a conformance test and a named-retail citation.