Parse the complete PhysicsDesc plus F754/F755 packets, correct every PhysicsState bit, and gate all nine retail update channels with generation-safe immutable snapshots. Preserve ForcePosition, teleport, placement, velocity, parent, pickup, delete, and same-generation CreateObject ordering from the named client. Separate accepted logical lifecycle notifications from retained UI qualities, make GUID replacement and session reset clear every projection exactly once, and add packet, wraparound, malformed-input, parent FIFO, canonical-position, reconnect, and GUID-reuse conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
31 KiB
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.
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.
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:
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.
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):
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::HandleReceivedPositionat0x00453FD0SmartBox::HandleObjDescEventat0x00453340andSmartBox::UpdateVisualDescat0x00451F40SmartBox::HandleDeleteObjectat0x00451EA0
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:
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::HandlePlayScriptIDat0x00452020SmartBox::HandlePlayScriptTypeat0x00452070
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:
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::GetScriptat0x00521E40PhysicsScriptTableData::GetScriptat0x00521A20
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::AddScriptInternalat0x0051B310ScriptManager::NextHookat0x0051B3F0ScriptManager::UpdateScriptsat0x0051B480
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.
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::Executeat0x00526EF0ParticleManager::CreateBlockingParticleEmitterat0x0051B8A0
CreateBlockingParticle inherits the complete CreateParticle payload:
emitterInfoDid, partIndex, offsetFrame(position+orientation), logicalEmitterId
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:
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::UpdatePositionInternalat0x00512C30CPhysicsObj::UpdateObjectInternalat0x005156B0CPhysicsObj::update_objectat0x00515D10CPhysicsObj::UpdatePhysicsInternalat0x00510700CPhysicsObj::calc_accelerationat0x00510950CPhysicsObj::get_object_infoat0x00511CC0Frame::set_vector_headingat0x00535DB0
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.
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:
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: collision0x33000109, Launch0x330008BF, Explode0x33000119, each at threshold 1. - Lightning
0x34000081: collision0x33000109, Launch0x330008C0, Explode0x3300013E, each at threshold 1. - Acid
0x34000082: collision0x33000109, Launch0x330008C2, Explode0x33000132, each at threshold 1. - Tusker Fist has no network/template table, but its Setup
0x02000EAEcarries directDefaultScript=0x33000D77. Setup initialization therefore runs that direct PES: Scale, CreateParticle emitter0x3200071Bon 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, and0x340000D1: UnHide(1 -> 0x3300032F), Hidden(1 -> 0x33000331). - Tables
0x34000063,0x340000A9,0x340000B9, and0x340000BB: 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 adds 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.
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.csandSource/ACE.Entity/Enum/PhysicsState.csconfirm PhysicsDesc gates and state bits. - ACE
Source/ACE.Server/Network/GameMessages/Messages/GameMessageScript.csconfirms F755's opcode/GUID/raw type/float layout. - ACE
Source/ACE.Server/WorldObjects/SpellProjectile.csconfirms 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.rsconfirms PhysicsDesc field order and the nine trailing sequences. - ACViewer
ACViewer/Entity/AnimationHooks/CreateParticleHook.csandACViewer/Physics/Scripts/PhysicsScriptTableData.cswere used as schema interpretation aids only; their behavior is not substituted for retail's blocking-emitter or stored-order threshold rules.
Repository links:
- https://github.com/ACEmulator/ACE/blob/master/Source/ACE.Entity/Enum/PhysicsDescriptionFlag.cs
- https://github.com/ACEmulator/ACE/blob/master/Source/ACE.Entity/Enum/PhysicsState.cs
- https://github.com/ACEmulator/ACE/blob/master/Source/ACE.Server/Network/GameMessages/Messages/GameMessageScript.cs
- https://github.com/ACEmulator/ACE/blob/master/Source/ACE.Server/WorldObjects/SpellProjectile.cs
- https://github.com/merklejerk/holtburger/blob/main/crates/holtburger-protocol/src/messages/object/messages/description.rs
- https://github.com/ACEmulator/ACViewer/blob/master/ACViewer/Entity/AnimationHooks/CreateParticleHook.cs
- https://github.com/ACEmulator/ACViewer/blob/master/ACViewer/Physics/Scripts/PhysicsScriptTableData.cs
13. Port invariants
- Server GUID is translated once to a canonical local entity ID before an effect reaches render, audio, light, translucency, or particle sinks.
- Logical entity creation/destruction is separate from spatial rebucketing.
- Server Position/Vector/State/Delete packets remain authoritative.
- No client-side impact, explosion, damage, or deletion is synthesized.
- 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.
- Hidden suppresses mesh/collision/interaction but does not destroy the effect owner.
- DAT assets determine models, colors, timing, and attachment behavior.
- Every ported branch receives a conformance test and a named-retail citation.