Late-bind radar snapshots to the canonical LiveEntityRuntime maps so the retained radar survives bootstrap and session replacement instead of capturing empty sentinels. Route paperdoll drops through the retail AutoWield blocker transaction. Move conflicting held weapons, shields, explicit jewelry destinations, and mismatched ammo to the backpack one authoritative confirmation at a time; preserve compatible arrows when switching to melee. Render mode-1 and no-degrade particle GfxObjs as their authored modern-pipeline meshes, retain Always2D billboards, interleave both paths back-to-front, balance emitter mesh ownership, and fail safely on corrupt DAT material metadata. This restores the closed apex on Armor Self/protection effects. Retain Studio fixture controller lifetimes, add installed-DAT and adversarial regression coverage, synchronize retail research/divergence bookkeeping, and pass all three review tracks plus the full Release suite. Co-Authored-By: Codex <noreply@openai.com>
236 lines
7.1 KiB
C#
236 lines
7.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
|
|
namespace AcDream.Core.Vfx;
|
|
|
|
/// <summary>
|
|
/// Retail particle motion integrators from <c>ParticleType</c> in
|
|
/// <c>acclient.h</c>. Values are the retail dat values.
|
|
/// </summary>
|
|
public enum ParticleType
|
|
{
|
|
Unknown = 0,
|
|
Still = 1,
|
|
LocalVelocity = 2,
|
|
ParabolicLVGA = 3,
|
|
ParabolicLVGAGR = 4,
|
|
Swarm = 5,
|
|
Explode = 6,
|
|
Implode = 7,
|
|
ParabolicLVLA = 8,
|
|
ParabolicLVLALR = 9,
|
|
ParabolicGVGA = 10,
|
|
ParabolicGVGAGR = 11,
|
|
GlobalVelocity = 12,
|
|
NumParticleType = 13,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail <c>EmitterType</c> from <c>acclient.h</c>.
|
|
/// </summary>
|
|
public enum ParticleEmitterKind
|
|
{
|
|
Unknown = 0,
|
|
BirthratePerSec = 1,
|
|
BirthratePerMeter = 2,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Render stage for an active particle emitter.
|
|
/// </summary>
|
|
public enum ParticleRenderPass
|
|
{
|
|
Scene = 0,
|
|
SkyPreScene = 1,
|
|
SkyPostScene = 2,
|
|
}
|
|
|
|
[Flags]
|
|
public enum EmitterFlags : uint
|
|
{
|
|
None = 0,
|
|
Additive = 0x01,
|
|
Billboard = 0x02,
|
|
FaceCamera = 0x04,
|
|
AttachLocal = 0x08,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-emitter configuration from the retail <c>ParticleEmitterInfo</c>
|
|
/// dat object.
|
|
/// </summary>
|
|
public sealed class EmitterDesc
|
|
{
|
|
public uint DatId { get; init; }
|
|
public ParticleType Type { get; init; }
|
|
public ParticleEmitterKind EmitterKind { get; init; } = ParticleEmitterKind.BirthratePerSec;
|
|
public EmitterFlags Flags { get; init; }
|
|
public uint TextureSurfaceId { get; init; }
|
|
public uint GfxObjId { get; init; }
|
|
public uint HwGfxObjId { get; init; }
|
|
public uint SoundOnSpawn { get; init; }
|
|
|
|
// Emission behavior.
|
|
public float Birthrate { get; init; }
|
|
public float EmitRate { get; init; }
|
|
public int MaxParticles { get; init; }
|
|
public int InitialParticles { get; init; }
|
|
public int TotalParticles { get; init; }
|
|
public float LifetimeMin { get; init; }
|
|
public float LifetimeMax { get; init; }
|
|
public float Lifespan { get; init; }
|
|
public float LifespanRand { get; init; }
|
|
public float StartDelay { get; init; }
|
|
public float TotalDuration { get; init; }
|
|
|
|
// Spawn geometry.
|
|
public Vector3 OffsetDir { get; init; } = new(0, 0, 1);
|
|
public float MinOffset { get; init; }
|
|
public float MaxOffset { get; init; }
|
|
public float SpawnDiskRadius { get; init; }
|
|
|
|
// Kinematics. A/B/C are the retail vector coefficients.
|
|
public Vector3 InitialVelocity { get; init; }
|
|
public float VelocityJitter { get; init; }
|
|
public Vector3 Gravity { get; init; } = new(0, 0, -9.8f);
|
|
public Vector3 A { get; init; }
|
|
public float MinA { get; init; } = 1f;
|
|
public float MaxA { get; init; } = 1f;
|
|
public Vector3 B { get; init; }
|
|
public float MinB { get; init; } = 1f;
|
|
public float MaxB { get; init; } = 1f;
|
|
public Vector3 C { get; init; }
|
|
public float MinC { get; init; } = 1f;
|
|
public float MaxC { get; init; } = 1f;
|
|
|
|
// Appearance over lifetime.
|
|
public uint StartColorArgb { get; init; } = 0xFFFFFFFF;
|
|
public uint EndColorArgb { get; init; } = 0xFFFFFFFF;
|
|
public float StartAlpha { get; init; } = 1f;
|
|
public float EndAlpha { get; init; } = 0f;
|
|
public float StartSize { get; init; } = 0.5f;
|
|
public float EndSize { get; init; } = 0.5f;
|
|
public float ScaleRand { get; init; }
|
|
public float TransRand { get; init; }
|
|
public float StartRotation { get; init; }
|
|
public float EndRotation { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// A PhysicsScript (0x3Axxxxxx range in retail) is a list of hooks to
|
|
/// fire at specific start-times. Each hook creates an emitter or plays
|
|
/// a sound. Chaining hooks at different times gives "animation".
|
|
/// </summary>
|
|
public sealed class PhysicsScript
|
|
{
|
|
public uint ScriptId { get; init; }
|
|
public IReadOnlyList<PhysicsScriptHook> Hooks { get; init; } = Array.Empty<PhysicsScriptHook>();
|
|
}
|
|
|
|
public sealed record PhysicsScriptHook(
|
|
float StartTime,
|
|
PhysicsScriptHookType Type,
|
|
uint RefDataId,
|
|
int PartIndex,
|
|
Vector3 Offset,
|
|
bool IsParentLocal);
|
|
|
|
public enum PhysicsScriptHookType
|
|
{
|
|
CreateParticle = 18,
|
|
DestroyParticle = 19,
|
|
PlaySound = 1,
|
|
AnimationDone = 2,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Individual runtime particle. Owned by the <c>ParticleSystem</c>.
|
|
/// </summary>
|
|
public struct Particle
|
|
{
|
|
public Vector3 EmissionOrigin;
|
|
public Quaternion SpawnRotation;
|
|
public Vector3 Position;
|
|
public Vector3 Velocity;
|
|
public Vector3 Offset;
|
|
public Vector3 A;
|
|
public Vector3 B;
|
|
public Vector3 C;
|
|
public float SpawnedAt;
|
|
public float Lifetime;
|
|
public float Age;
|
|
public float StartSize;
|
|
public float EndSize;
|
|
public float StartAlpha;
|
|
public float EndAlpha;
|
|
public uint ColorArgb;
|
|
public float Size;
|
|
public float Rotation;
|
|
public bool Alive;
|
|
}
|
|
|
|
/// <summary>
|
|
/// One active emitter instance. The <c>ParticleSystem</c> holds a pool
|
|
/// of these; each one maintains its own particle array.
|
|
/// </summary>
|
|
public sealed class ParticleEmitter
|
|
{
|
|
/// <summary>
|
|
/// Stable logical identity assigned by <see cref="ParticleSystem"/>.
|
|
/// Render owners use it to keep DAT mesh references balanced with the
|
|
/// emitter's logical lifetime; it is not a GPU resource identifier.
|
|
/// </summary>
|
|
public int Handle { get; init; }
|
|
public EmitterDesc Desc { get; init; } = null!;
|
|
public Vector3 AnchorPos { get; set; }
|
|
public Quaternion AnchorRot { get; set; } = Quaternion.Identity;
|
|
public uint AttachedObjectId { get; set; }
|
|
/// <summary>
|
|
/// Spatial presentation latch. A cell-less owner submits no particles.
|
|
/// </summary>
|
|
public bool PresentationVisible { get; set; } = true;
|
|
/// <summary>
|
|
/// Retail cell-membership update gate. Paused emitters retain their exact
|
|
/// particles, logical identity, and elapsed-time position until re-entry.
|
|
/// </summary>
|
|
public bool SimulationEnabled { get; set; } = true;
|
|
public int AttachedPartIndex { get; set; } = -1;
|
|
public Particle[] Particles { get; init; } = null!;
|
|
public ParticleRenderPass RenderPass { get; init; }
|
|
public int ActiveCount;
|
|
public float EmittedAccumulator;
|
|
public float StartedAt;
|
|
public float LastEmitTime;
|
|
public Vector3 LastEmitOffset;
|
|
public int TotalEmitted;
|
|
public bool Finished;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Top-level particle orchestrator. App-layer renderer batches these.
|
|
/// </summary>
|
|
public interface IParticleSystem
|
|
{
|
|
/// <summary>Spawn an emitter attached to a world position or entity.</summary>
|
|
int SpawnEmitter(
|
|
EmitterDesc desc,
|
|
Vector3 anchor,
|
|
Quaternion? rot = null,
|
|
uint attachedObjectId = 0,
|
|
int attachedPartIndex = -1,
|
|
ParticleRenderPass renderPass = ParticleRenderPass.Scene);
|
|
|
|
/// <summary>Fire a full PhysicsScript at a target.</summary>
|
|
void PlayScript(uint scriptId, uint targetObjectId, float modifier = 1f);
|
|
|
|
/// <summary>Advance all active emitters by dt seconds.</summary>
|
|
void Tick(float dt);
|
|
|
|
/// <summary>Stop an emitter early.</summary>
|
|
void StopEmitter(int handle, bool fadeOut);
|
|
|
|
/// <summary>Current active particle count.</summary>
|
|
int ActiveParticleCount { get; }
|
|
int ActiveEmitterCount { get; }
|
|
}
|