fix(vfx): classify hardwareless particle emitters once
This commit is contained in:
parent
921712f412
commit
5b3fb17775
8 changed files with 374 additions and 18 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
@ -13,6 +13,21 @@ using DatGfxObjFlags = DatReaderWriter.Enums.GfxObjFlags;
|
|||
|
||||
namespace AcDream.Core.Vfx;
|
||||
|
||||
public enum EmitterDescResolutionFailureKind
|
||||
{
|
||||
None,
|
||||
MissingEmitterInfo,
|
||||
InvalidHardwareGfxObjId,
|
||||
MissingHardwareGfxObj,
|
||||
}
|
||||
|
||||
public readonly record struct EmitterDescResolutionFailure(
|
||||
EmitterDescResolutionFailureKind Kind,
|
||||
uint RelatedDatId = 0u)
|
||||
{
|
||||
public static EmitterDescResolutionFailure None => default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves retail <c>ParticleEmitterInfo</c> dat records
|
||||
/// (<c>0x32xxxxxx</c>) into acdream runtime descriptors.
|
||||
|
|
@ -20,8 +35,11 @@ namespace AcDream.Core.Vfx;
|
|||
public sealed class EmitterDescRegistry
|
||||
{
|
||||
private readonly Func<uint, DatParticleEmitter?>? _resolver;
|
||||
private readonly Func<DatParticleEmitter, float?>? _degradeDistanceResolver;
|
||||
private readonly Func<DatParticleEmitter, EmitterDatResolution>?
|
||||
_degradeDistanceResolver;
|
||||
private readonly ConcurrentDictionary<uint, EmitterDesc> _byId = new();
|
||||
private readonly ConcurrentDictionary<uint, EmitterDescResolutionFailure>
|
||||
_failures = new();
|
||||
|
||||
public EmitterDescRegistry()
|
||||
: this((Func<uint, DatParticleEmitter?>?)null)
|
||||
|
|
@ -44,6 +62,7 @@ public sealed class EmitterDescRegistry
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(desc);
|
||||
_byId[desc.DatId] = desc;
|
||||
_failures.TryRemove(desc.DatId, out _);
|
||||
}
|
||||
|
||||
public EmitterDesc Get(uint emitterId)
|
||||
|
|
@ -55,35 +74,59 @@ public sealed class EmitterDescRegistry
|
|||
}
|
||||
|
||||
public bool TryGet(uint emitterId, out EmitterDesc desc)
|
||||
=> TryGet(emitterId, out desc, out _);
|
||||
|
||||
public bool TryGet(
|
||||
uint emitterId,
|
||||
out EmitterDesc desc,
|
||||
out EmitterDescResolutionFailure failure)
|
||||
{
|
||||
if (_byId.TryGetValue(emitterId, out desc!))
|
||||
{
|
||||
failure = EmitterDescResolutionFailure.None;
|
||||
return true;
|
||||
}
|
||||
if (_failures.TryGetValue(emitterId, out failure))
|
||||
{
|
||||
desc = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_resolver is not null)
|
||||
{
|
||||
var dat = _resolver(emitterId);
|
||||
if (dat is not null)
|
||||
{
|
||||
float? resolvedDistance = _degradeDistanceResolver?.Invoke(dat);
|
||||
if (_degradeDistanceResolver is not null && !resolvedDistance.HasValue)
|
||||
EmitterDatResolution resolved = _degradeDistanceResolver?.Invoke(dat)
|
||||
?? EmitterDatResolution.Success(
|
||||
RetailParticleDegradeDistance.DefaultDistance);
|
||||
if (!resolved.Succeeded)
|
||||
{
|
||||
// ParticleEmitter::SetInfo (0x0051CE90) fails when the
|
||||
// authored hardware GfxObj cannot be loaded. It never
|
||||
// substitutes the software GfxObj.
|
||||
failure = resolved.Failure;
|
||||
_failures.TryAdd(emitterId, failure);
|
||||
desc = null!;
|
||||
return false;
|
||||
}
|
||||
float maxDegradeDistance = resolvedDistance
|
||||
?? RetailParticleDegradeDistance.DefaultDistance;
|
||||
desc = FromDat(emitterId, dat, maxDegradeDistance);
|
||||
desc = FromDat(emitterId, dat, resolved.MaxDegradeDistance);
|
||||
_byId[emitterId] = desc;
|
||||
failure = EmitterDescResolutionFailure.None;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
failure = new EmitterDescResolutionFailure(
|
||||
EmitterDescResolutionFailureKind.MissingEmitterInfo,
|
||||
emitterId);
|
||||
_failures.TryAdd(emitterId, failure);
|
||||
desc = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public int Count => _byId.Count;
|
||||
public int FailureCount => _failures.Count;
|
||||
|
||||
public static EmitterDesc FromDat(
|
||||
uint emitterId,
|
||||
|
|
@ -155,25 +198,34 @@ public sealed class EmitterDescRegistry
|
|||
};
|
||||
}
|
||||
|
||||
private static float? ResolveMaxDegradeDistance(
|
||||
private static EmitterDatResolution ResolveMaxDegradeDistance(
|
||||
IDatObjectSource dats,
|
||||
DatParticleEmitter emitter)
|
||||
{
|
||||
uint gfxObjId = GetRetailHardwareGfxObjId(emitter);
|
||||
if (gfxObjId == 0)
|
||||
return null;
|
||||
{
|
||||
return EmitterDatResolution.Fail(
|
||||
EmitterDescResolutionFailureKind.InvalidHardwareGfxObjId);
|
||||
}
|
||||
|
||||
DatGfxObj? gfx = SafeGet<DatGfxObj>(dats, gfxObjId);
|
||||
if (gfx is null)
|
||||
return null;
|
||||
{
|
||||
return EmitterDatResolution.Fail(
|
||||
EmitterDescResolutionFailureKind.MissingHardwareGfxObj,
|
||||
gfxObjId);
|
||||
}
|
||||
if (!gfx.Flags.HasFlag(DatGfxObjFlags.HasDIDDegrade)
|
||||
|| gfx.DIDDegrade == 0)
|
||||
{
|
||||
return RetailParticleDegradeDistance.DefaultDistance;
|
||||
return EmitterDatResolution.Success(
|
||||
RetailParticleDegradeDistance.DefaultDistance);
|
||||
}
|
||||
|
||||
DatGfxObjDegradeInfo? degrade = SafeGet<DatGfxObjDegradeInfo>(dats, gfx.DIDDegrade);
|
||||
return RetailParticleDegradeDistance.FromEntries(degrade?.Degrades);
|
||||
return EmitterDatResolution.Success(
|
||||
RetailParticleDegradeDistance.FromEntries(degrade?.Degrades));
|
||||
}
|
||||
|
||||
internal static uint GetRetailHardwareGfxObjId(DatParticleEmitter emitter)
|
||||
|
|
@ -216,6 +268,26 @@ public sealed class EmitterDescRegistry
|
|||
DatParticleType.GlobalVelocity => ParticleType.GlobalVelocity,
|
||||
_ => ParticleType.Unknown,
|
||||
};
|
||||
|
||||
private readonly record struct EmitterDatResolution(
|
||||
bool Succeeded,
|
||||
float MaxDegradeDistance,
|
||||
EmitterDescResolutionFailure Failure)
|
||||
{
|
||||
public static EmitterDatResolution Success(float maxDegradeDistance) =>
|
||||
new(
|
||||
true,
|
||||
maxDegradeDistance,
|
||||
EmitterDescResolutionFailure.None);
|
||||
|
||||
public static EmitterDatResolution Fail(
|
||||
EmitterDescResolutionFailureKind kind,
|
||||
uint relatedDatId = 0u) =>
|
||||
new(
|
||||
false,
|
||||
0f,
|
||||
new EmitterDescResolutionFailure(kind, relatedDatId));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
private readonly HashSet<uint> _examinationOwners = [];
|
||||
private readonly HashSet<uint> _hiddenPresentationOwners = [];
|
||||
private readonly HashSet<uint> _stoppingOwners = [];
|
||||
private readonly HashSet<uint> _reportedEmitterResolutionFailures = [];
|
||||
|
||||
// Production pose registries publish actual root/part/cell changes. Only
|
||||
// those owners are recomposed; static world emitters do no refresh work.
|
||||
|
|
@ -417,11 +418,16 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
partIndex,
|
||||
renderPass,
|
||||
visibilityPolicy,
|
||||
out int handle))
|
||||
out int handle,
|
||||
out EmitterDescResolutionFailure failure))
|
||||
{
|
||||
DiagnosticSink?.Invoke(
|
||||
$"ParticleEmitterInfo 0x{emitterInfoId:X8} for owner " +
|
||||
$"0x{ownerLocalId:X8} was not found; no fallback was created.");
|
||||
if (_reportedEmitterResolutionFailures.Add(emitterInfoId))
|
||||
{
|
||||
DiagnosticSink?.Invoke(FormatEmitterResolutionDiagnostic(
|
||||
emitterInfoId,
|
||||
ownerLocalId,
|
||||
failure));
|
||||
}
|
||||
return;
|
||||
}
|
||||
_system.UpdateEmitterOwnerPosition(handle, ownerPosition);
|
||||
|
|
@ -534,6 +540,26 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
return true;
|
||||
}
|
||||
|
||||
private static string FormatEmitterResolutionDiagnostic(
|
||||
uint emitterInfoId,
|
||||
uint ownerLocalId,
|
||||
EmitterDescResolutionFailure failure) =>
|
||||
failure.Kind switch
|
||||
{
|
||||
EmitterDescResolutionFailureKind.InvalidHardwareGfxObjId =>
|
||||
$"ParticleEmitterInfo 0x{emitterInfoId:X8} has no retail " +
|
||||
$"hardware GfxObj; ParticleEmitter::SetInfo skipped it " +
|
||||
$"(first owner 0x{ownerLocalId:X8}).",
|
||||
EmitterDescResolutionFailureKind.MissingHardwareGfxObj =>
|
||||
$"ParticleEmitterInfo 0x{emitterInfoId:X8} references missing " +
|
||||
$"retail hardware GfxObj 0x{failure.RelatedDatId:X8}; " +
|
||||
$"ParticleEmitter::SetInfo skipped it " +
|
||||
$"(first owner 0x{ownerLocalId:X8}).",
|
||||
_ =>
|
||||
$"ParticleEmitterInfo 0x{emitterInfoId:X8} for owner " +
|
||||
$"0x{ownerLocalId:X8} was not found; no fallback was created.",
|
||||
};
|
||||
|
||||
private readonly record struct EmitterBinding(
|
||||
uint OwnerLocalId,
|
||||
int PartIndex,
|
||||
|
|
|
|||
|
|
@ -176,8 +176,29 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
ParticleRenderPass renderPass,
|
||||
ParticleVisibilityPolicy visibilityPolicy,
|
||||
out int handle)
|
||||
=> TrySpawnEmitterById(
|
||||
emitterId,
|
||||
anchor,
|
||||
rot,
|
||||
attachedObjectId,
|
||||
attachedPartIndex,
|
||||
renderPass,
|
||||
visibilityPolicy,
|
||||
out handle,
|
||||
out _);
|
||||
|
||||
public bool TrySpawnEmitterById(
|
||||
uint emitterId,
|
||||
Vector3 anchor,
|
||||
Quaternion? rot,
|
||||
uint attachedObjectId,
|
||||
int attachedPartIndex,
|
||||
ParticleRenderPass renderPass,
|
||||
ParticleVisibilityPolicy visibilityPolicy,
|
||||
out int handle,
|
||||
out EmitterDescResolutionFailure failure)
|
||||
{
|
||||
if (!_registry.TryGet(emitterId, out EmitterDesc? desc))
|
||||
if (!_registry.TryGet(emitterId, out EmitterDesc? desc, out failure))
|
||||
{
|
||||
handle = 0;
|
||||
return false;
|
||||
|
|
@ -191,6 +212,7 @@ public sealed class ParticleSystem : IParticleSystem
|
|||
attachedPartIndex,
|
||||
renderPass,
|
||||
visibilityPolicy);
|
||||
failure = EmitterDescResolutionFailure.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -264,6 +264,29 @@ public sealed class ParticleHookSinkTests
|
|||
Assert.Contains($"0x{Owner:X8}", diagnostic, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingEmitterAsset_ReportsOnlyOnceAcrossManyOwners()
|
||||
{
|
||||
const uint missingId = 0x32DEAD00u;
|
||||
const uint secondOwner = Owner + 1u;
|
||||
var registry = new EmitterDescRegistry();
|
||||
var system = new ParticleSystem(registry, new Random(42));
|
||||
var poses = new MutablePoseSource();
|
||||
poses.Publish(Owner, Matrix4x4.Identity, Matrix4x4.Identity);
|
||||
poses.Publish(secondOwner, Matrix4x4.Identity, Matrix4x4.Identity);
|
||||
var sink = new ParticleHookSink(system, poses);
|
||||
var diagnostics = new List<string>();
|
||||
sink.DiagnosticSink = diagnostics.Add;
|
||||
|
||||
sink.OnHook(Owner, Vector3.Zero, Create(missingId, logicalId: 0u));
|
||||
sink.OnHook(secondOwner, Vector3.Zero, Create(missingId, logicalId: 0u));
|
||||
sink.OnHook(Owner, Vector3.Zero, Create(missingId, logicalId: 0u));
|
||||
|
||||
Assert.Empty(system.EnumerateEmitters());
|
||||
Assert.Single(diagnostics);
|
||||
Assert.Contains($"0x{missingId:X8}", diagnostics[0], StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingLivePose_HidesPresentationWithoutEndingEmitterLifetime()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Content;
|
||||
using AcDream.Core.Vfx;
|
||||
using DatReaderWriter.Lib.IO;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -267,6 +269,35 @@ public sealed class ParticleSystemTests
|
|||
Assert.Same(desc, reg.Get(0x32001234u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitterDescRegistry_NegativeCachesMissingDatAndRegisterClearsFailure()
|
||||
{
|
||||
int calls = 0;
|
||||
var reg = new EmitterDescRegistry(_ =>
|
||||
{
|
||||
calls++;
|
||||
return null;
|
||||
});
|
||||
const uint emitterId = 0x3200DEADu;
|
||||
|
||||
Assert.False(reg.TryGet(emitterId, out _, out var first));
|
||||
Assert.False(reg.TryGet(emitterId, out _, out var second));
|
||||
|
||||
Assert.Equal(1, calls);
|
||||
Assert.Equal(
|
||||
EmitterDescResolutionFailureKind.MissingEmitterInfo,
|
||||
first.Kind);
|
||||
Assert.Equal(first, second);
|
||||
Assert.Equal(1, reg.FailureCount);
|
||||
|
||||
var desc = new EmitterDesc { DatId = emitterId };
|
||||
reg.Register(desc);
|
||||
Assert.True(reg.TryGet(emitterId, out var resolved, out var failure));
|
||||
Assert.Same(desc, resolved);
|
||||
Assert.Equal(EmitterDescResolutionFailureKind.None, failure.Kind);
|
||||
Assert.Equal(0, reg.FailureCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LocalVelocity_TransformsABySpawnRotation()
|
||||
{
|
||||
|
|
@ -398,6 +429,31 @@ public sealed class ParticleSystemTests
|
|||
Assert.Equal(0u, EmitterDescRegistry.GetRetailHardwareGfxObjId(dat));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitterDescRegistry_ClassifiesAndCachesAuthoredHardwarelessEmitter()
|
||||
{
|
||||
const uint emitterId = 0x320002D6u;
|
||||
var dats = new FakeDatObjectSource();
|
||||
dats.Add(
|
||||
emitterId,
|
||||
new DatReaderWriter.DBObjs.ParticleEmitter
|
||||
{
|
||||
GfxObjId = 0x010016C9u,
|
||||
HwGfxObjId = 0u,
|
||||
});
|
||||
var reg = new EmitterDescRegistry(dats);
|
||||
|
||||
Assert.False(reg.TryGet(emitterId, out _, out var first));
|
||||
Assert.False(reg.TryGet(emitterId, out _, out var second));
|
||||
|
||||
Assert.Equal(
|
||||
EmitterDescResolutionFailureKind.InvalidHardwareGfxObjId,
|
||||
first.Kind);
|
||||
Assert.Equal(first, second);
|
||||
Assert.Equal(1, dats.GetCallCount(emitterId));
|
||||
Assert.Equal(1, reg.FailureCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailParticleDegradeDistance_MatchesRetailEntrySelection()
|
||||
{
|
||||
|
|
@ -1100,4 +1156,31 @@ public sealed class ParticleSystemTests
|
|||
sys.Tick(0.01f);
|
||||
Assert.Equal(2, sys.ActiveParticleCount);
|
||||
}
|
||||
|
||||
private sealed class FakeDatObjectSource : IDatObjectSource
|
||||
{
|
||||
private readonly Dictionary<uint, IDBObj> _objects = new();
|
||||
private readonly Dictionary<uint, int> _calls = new();
|
||||
|
||||
public void Add(uint id, IDBObj value) => _objects[id] = value;
|
||||
|
||||
public int GetCallCount(uint id) =>
|
||||
_calls.TryGetValue(id, out int count) ? count : 0;
|
||||
|
||||
public T Get<T>(uint fileId) where T : IDBObj
|
||||
{
|
||||
_calls.TryGetValue(fileId, out int count);
|
||||
_calls[fileId] = count + 1;
|
||||
return _objects.TryGetValue(fileId, out IDBObj? value)
|
||||
&& value is T typed
|
||||
? typed
|
||||
: default!;
|
||||
}
|
||||
|
||||
public bool TryGet<T>(uint fileId, out T value) where T : IDBObj
|
||||
{
|
||||
value = Get<T>(fileId);
|
||||
return value is not null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,18 +11,57 @@ using DatReaderWriter.Lib.IO;
|
|||
using DatReaderWriter.Options;
|
||||
using DatReaderWriter.Types;
|
||||
using DatAnimation = DatReaderWriter.DBObjs.Animation;
|
||||
using DatParticleEmitter = DatReaderWriter.DBObjs.ParticleEmitter;
|
||||
using DatPhysicsScript = DatReaderWriter.DBObjs.PhysicsScript;
|
||||
|
||||
string datDir = System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
?? @"C:\Turbine\Asheron's Call";
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
var physicsScripts = new RetailPhysicsScriptLoader(dats);
|
||||
var animations = new RetailAnimationLoader(dats);
|
||||
using var boundedDats = new AcDream.Content.DatCollectionAdapter(dats);
|
||||
var physicsScripts = new RetailPhysicsScriptLoader(boundedDats);
|
||||
var animations = new RetailAnimationLoader(boundedDats);
|
||||
Console.WriteLine($"DAT metadata audit: {datDir}");
|
||||
var failures = new List<string>();
|
||||
var representativePes = new HashSet<uint>();
|
||||
|
||||
if (args.Contains("--emitters", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
uint[] emitterIds =
|
||||
[
|
||||
0x320002D6u,
|
||||
0x320003B6u,
|
||||
0x320003B7u,
|
||||
0x320003B8u,
|
||||
0x320003B9u,
|
||||
];
|
||||
var registry = new EmitterDescRegistry(boundedDats);
|
||||
foreach (uint emitterId in emitterIds)
|
||||
{
|
||||
bool hasRaw = dats.Portal.Tree.TryGetFile(emitterId, out _);
|
||||
DatParticleEmitter? emitter = SafeGet<DatParticleEmitter>(emitterId);
|
||||
uint softwareGfxObjId = emitter?.GfxObjId.DataId ?? 0u;
|
||||
uint hardwareGfxObjId = emitter?.HwGfxObjId.DataId ?? 0u;
|
||||
bool hasSoftwareGfx = softwareGfxObjId != 0u
|
||||
&& SafeGet<GfxObj>(softwareGfxObjId) is not null;
|
||||
bool hasHardwareGfx = hardwareGfxObjId != 0u
|
||||
&& SafeGet<GfxObj>(hardwareGfxObjId) is not null;
|
||||
bool resolved = registry.TryGet(
|
||||
emitterId,
|
||||
out EmitterDesc? descriptor,
|
||||
out EmitterDescResolutionFailure failure);
|
||||
Console.WriteLine(
|
||||
$"0x{emitterId:X8}: raw={hasRaw}, parsed={emitter is not null}, " +
|
||||
$"gfx=0x{softwareGfxObjId:X8}, gfxParsed={hasSoftwareGfx}, " +
|
||||
$"hwGfx=0x{hardwareGfxObjId:X8}, hwParsed={hasHardwareGfx}, " +
|
||||
$"registry={resolved}, maxDist={descriptor?.MaxDegradeDistance:R}, " +
|
||||
$"failure={failure.Kind}, related=0x{failure.RelatedDatId:X8}, " +
|
||||
$"type={emitter?.EmitterType}/{emitter?.ParticleType}, " +
|
||||
$"initial={emitter?.InitialParticles}, max={emitter?.MaxParticles}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var expectedSetups = new Dictionary<uint, (Vector3 Origin, float Radius, int Length, string Sha256)>
|
||||
{
|
||||
[0x02000124u] = (Vector3.Zero, 0.1f, 812, "630D19A9C79137F7BA90D01448B03E8F44D655FF77A4725792CB5660930A079E"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue