82 lines
2.8 KiB
Markdown
82 lines
2.8 KiB
Markdown
# 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`
|