fix(vfx): classify hardwareless particle emitters once

This commit is contained in:
Erik 2026-07-27 00:03:31 +02:00
parent 921712f412
commit 5b3fb17775
8 changed files with 374 additions and 18 deletions

View file

@ -207,6 +207,15 @@ anchors: `CPhysicsPart::GetMaxDegradeDistance` `0x0050D510`,
`CPhysicsObj::ShouldDrawParticles` `0x0050FE60`, and
`ParticleEmitter::UpdateParticles` `0x0051D180`.
Hardwareless ParticleEmitterInfo records are also retained exactly. Retail
`ParticleEmitter::SetInfo @ 0x0051CE90` returns false when
`hw_gfxobj_id == INVALID_DID`; it does not substitute the software GfxObj.
`EmitterDescRegistry` negative-caches that authored outcome and the hook sink
reports it once per DAT ID rather than once per owner. This is classification
and bounded diagnostics around the existing `DatCollection`, not a fallback
reader or invented VFX. Evidence:
`docs/research/2026-07-26-retail-hardwareless-particle-emitter-diagnostics.md`.
**Retail shared world-alpha seam (2026-07-18).** WorldBuilder's editor
renderers classify translucent mesh batches correctly, but they have no live
retail `CPartCell`/`CShadowPart` list and render particles in a separate

View file

@ -0,0 +1,82 @@
# Retail hardwareless particle-emitter resolution
Date: 2026-07-26
Oracle: matching Sept-2013 `acclient.exe` + `acclient.pdb`
## Symptom
Dense scenes repeatedly logged that ParticleEmitterInfo records such as
`0x320002D6` and `0x320003B6``0x320003B9` were “not found” for many different
owners. The records were present in the installed DAT, so the message was
misleading and each owner repeated the same failed resolution work.
## Named-retail behavior
`ParticleEmitter::SetInfo @ 0x0051CE90` begins with:
```text
Destroy()
info = suppliedInfo
if info.hw_gfxobj_id == INVALID_DID:
Destroy()
return false
particleObject = makeParticleObject(info.max_particles, info.sorting_sphere)
...
```
`ParticleEmitter::SetInfo(DataID) @ 0x0051D3C0` first loads the
ParticleEmitterInfo and delegates to the overload above.
`ParticleManager::CreateParticleEmitter @ 0x0051B6C0` destroys the attempted
emitter when either `SetInfo` or parenting fails. Retail does not substitute
the software `gfxobj_id` for an absent hardware GfxObj.
## Installed-DAT audit
The production `DatCollection` and `EmitterDescRegistry` were exercised
through `ProjectileVfxAudit --emitters`:
| Emitter | Record | Software GfxObj | Hardware GfxObj | Retail outcome |
|---|---|---:|---:|---|
| `0x320002D6` | present/parsed | present | `0` | skip |
| `0x320003B6` | present/parsed | present | `0` | skip |
| `0x320003B7` | present/parsed | present | `0` | skip |
| `0x320003B8` | present/parsed | present | `0` | skip |
| `0x320003B9` | present/parsed | present | `0` | skip |
These are authored hardwareless records, not missing DAT files.
## Cross-reference
- ACViewer `ParticleEmitter.SetInfo` returns false when `HWGfxObjID == 0`.
Its render initialization explicitly comments that this case is expected and
skips the absent emitter.
- WorldBuilder contains no contradictory live-client hardware-emitter
fallback. Its particle code is an editor/rendering reference, while the
retail client remains the behavior oracle.
## Runtime contract
`EmitterDescRegistry` now stores either one resolved descriptor or one
classified negative result per DAT ID:
```text
MissingEmitterInfo
InvalidHardwareGfxObjId
MissingHardwareGfxObj
```
The particle hook sink reports the first failure for each emitter DAT ID, not
one line per owner. Hardwareless records say that retail skipped them rather
than claiming the ParticleEmitterInfo was absent. No fallback asset or
particle is fabricated.
Registration of an explicit runtime descriptor clears a cached negative
result, preserving test/tool injection and future hot-reload behavior.
## Conformance coverage
- `EmitterDescRegistry_NegativeCachesMissingDatAndRegisterClearsFailure`
- `EmitterDescRegistry_ClassifiesAndCachesAuthoredHardwarelessEmitter`
- `MissingEmitterAsset_ReportsOnlyOnceAcrossManyOwners`