fix #28: port retail's sky default-script playback (aurora) and the particle facing law
The aurora was never missing data — it was a missing mechanism plus a misread. New decompile evidence closes the April-2026 contradiction: retail plays the sky carriers' PES through the Setup's own DefaultScript (GameSky::MakeObject @0x00506EE0 -> CPhysicsObj::makeObject @0x00513970 sets state|=0x80000; animate_static_object @0x00513DF0 ticks ScriptManager + ParticleManager). The pes_id column stays dead — that half of the April finding stands; the ids are byte-equal mirrors. - SkyPesFrameController is now the production owner (ACDREAM_ENABLE_SKY_PES deleted): script ids resolve from the Setup DefaultScript (SkyObjectData.DefaultScriptId; the pes_id column is a one-time-logged cross-check), slots persist by (index, gfx id, properties) per CreateDeletePhysicsObjects @0x005073C0 — a day-group swap keeping the carrier no longer restarts its emitters — and stale slots stop before replacements claim the slot-derived owner id. - RetailParticleFacing ports calc_draw_frame @0x0050DFA0: degrade mode 2 faces the viewer roll-free (set_vector_heading) instead of the camera plane; modes 3/4/5 spin the authored frame around one local axis (rotate_around_axis_to_vector) — Dereth authors 54 mode-5 emitters that previously got no facing at all; 1,583 mode-2 emitters get the exact law; authored/mode-1 paths are unchanged. - The 2026-08-23 'whole-sky tint' was the Rainy-group lightning/thunder PES playing at the debug anchor inside their 0.03-0.19 window, not the aurora: the aurora is nine faint viewer-facing glows pulsing on 6.7/15/55-minute rebirth cycles, in every day group, all day. Research: docs/research/2026-08-23-sky-default-script-port.md. Register: AD-112 filed (camera-anchored synthetic owners vs sky-cell physics objects). ISSUES #2 corrected (the playback ban is lifted by the new evidence); #28 fix landed pending the connected night gate. Tests: RetailParticleFacingTests (16), SkyPesFrameControllerTests (6); hermetic suites App 6,076/0, Core 4,905/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6f47740af0
commit
18fce7bb5a
13 changed files with 906 additions and 113 deletions
|
|
@ -105,6 +105,7 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
private readonly Dictionary<uint, ParticleGfxInfo> _particleGfxInfoByGfxObj = new();
|
||||
private readonly Dictionary<int, ParticleGfxInfo> _particleGfxInfoByEmitter = new();
|
||||
private readonly Dictionary<uint, RetailParticleGeometryKind> _geometryKindByGfxObj = new();
|
||||
private readonly Dictionary<uint, uint?> _firstDegradeModeByGfxObj = new();
|
||||
private readonly Dictionary<uint, TranslucencyKind> _meshBlendBySurface = new();
|
||||
private readonly ParticleMeshReferenceTracker? _meshReferences;
|
||||
private readonly ParticleEmitterRetirementTracker _emitterRetirements;
|
||||
|
|
@ -400,18 +401,75 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
var key = new BatchKey(additive);
|
||||
Vector3 axisX;
|
||||
Vector3 axisY;
|
||||
Vector3 toViewer = cameraWorldPos - pos;
|
||||
float toViewerLength = toViewer.Length();
|
||||
if (gfxInfo.IsBillboard)
|
||||
{
|
||||
pos += Vector3.UnitZ * (gfxInfo.CenterOffset.Z * p.Size);
|
||||
axisX = cameraRight * (gfxInfo.Size.X * p.Size);
|
||||
axisY = cameraUp * (gfxInfo.Size.Y * p.Size);
|
||||
// Degrade mode 2 — face the viewer roll-free
|
||||
// (CPhysicsPart::calc_draw_frame @0x0050DFA0 via
|
||||
// Frame::set_vector_heading), not the camera plane: the two
|
||||
// agree at screen centre and diverge toward the edges and
|
||||
// overhead, where retail's sprites tilt toward the viewer.
|
||||
Vector3 xd;
|
||||
Vector3 yd;
|
||||
if (toViewerLength > 1e-3f)
|
||||
{
|
||||
(xd, yd) = RetailParticleFacing.OrientQuad(
|
||||
2u,
|
||||
Quaternion.Identity,
|
||||
Vector3.UnitX,
|
||||
Vector3.UnitY,
|
||||
toViewer / toViewerLength,
|
||||
cameraRight,
|
||||
cameraUp);
|
||||
}
|
||||
else
|
||||
{
|
||||
(xd, yd) = (cameraRight, cameraUp);
|
||||
}
|
||||
|
||||
// The sprite's authored (X, Z) plane rides the quad axes; the
|
||||
// out-of-plane component (authored Y, ~0 on flat sprites) is
|
||||
// dropped rather than pushed along the view direction.
|
||||
pos += (xd * gfxInfo.CenterOffset.X
|
||||
+ yd * gfxInfo.CenterOffset.Z) * p.Size;
|
||||
axisX = xd * (gfxInfo.Size.X * p.Size);
|
||||
axisY = yd * (gfxInfo.Size.Y * p.Size);
|
||||
}
|
||||
else
|
||||
{
|
||||
Quaternion orientation = ParticleOrientation(em, p);
|
||||
pos += Vector3.Transform(gfxInfo.CenterOffset * p.Size, orientation);
|
||||
axisX = Vector3.Transform(gfxInfo.AxisX, orientation) * (gfxInfo.Size.X * p.Size);
|
||||
axisY = Vector3.Transform(gfxInfo.AxisY, orientation) * (gfxInfo.Size.Y * p.Size);
|
||||
if (RetailParticleFacing.Faces(gfxInfo.DegradeMode)
|
||||
&& toViewerLength > 1e-3f)
|
||||
{
|
||||
// Modes 3/4/5 — authored geometry spun around one local
|
||||
// axis toward the viewer
|
||||
// (Frame::rotate_around_axis_to_vector).
|
||||
(Vector3 xd, Vector3 yd) = RetailParticleFacing.OrientQuad(
|
||||
gfxInfo.DegradeMode,
|
||||
orientation,
|
||||
gfxInfo.AxisX,
|
||||
gfxInfo.AxisY,
|
||||
toViewer / toViewerLength,
|
||||
cameraRight,
|
||||
cameraUp);
|
||||
Vector3 localNormal = Vector3.Cross(gfxInfo.AxisX, gfxInfo.AxisY);
|
||||
Vector3 spunNormal = Vector3.Cross(xd, yd);
|
||||
if (spunNormal.LengthSquared() > 1e-10f)
|
||||
spunNormal = Vector3.Normalize(spunNormal);
|
||||
Vector3 c = gfxInfo.CenterOffset;
|
||||
pos += (xd * Vector3.Dot(c, gfxInfo.AxisX)
|
||||
+ yd * Vector3.Dot(c, gfxInfo.AxisY)
|
||||
+ spunNormal * Vector3.Dot(c, localNormal)) * p.Size;
|
||||
axisX = xd * (gfxInfo.Size.X * p.Size);
|
||||
axisY = yd * (gfxInfo.Size.Y * p.Size);
|
||||
}
|
||||
else
|
||||
{
|
||||
pos += Vector3.Transform(gfxInfo.CenterOffset * p.Size, orientation);
|
||||
axisX = Vector3.Transform(gfxInfo.AxisX, orientation) * (gfxInfo.Size.X * p.Size);
|
||||
axisY = Vector3.Transform(gfxInfo.AxisY, orientation) * (gfxInfo.Size.Y * p.Size);
|
||||
}
|
||||
}
|
||||
|
||||
float distSq = Vector3.DistanceSquared(pos, cameraWorldPos);
|
||||
|
|
@ -576,7 +634,23 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
if (_geometryKindByGfxObj.TryGetValue(gfxObjId, out RetailParticleGeometryKind kind))
|
||||
return kind;
|
||||
|
||||
uint? firstDegradeMode = null;
|
||||
kind = RetailParticleGeometryClassifier.Classify(
|
||||
ResolveFirstDegradeMode(gfxObjId));
|
||||
_geometryKindByGfxObj[gfxObjId] = kind;
|
||||
return kind;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sprite's FIRST degrade entry's mode — retail's facing selector
|
||||
/// (<c>GfxObjDegradeInfo::get_degrade @0x0051E4B0</c> feeding
|
||||
/// <c>CPhysicsPart::calc_draw_frame @0x0050DFA0</c>). Null when the
|
||||
/// GfxObj has no degrade table.
|
||||
/// </summary>
|
||||
private uint? ResolveFirstDegradeMode(uint gfxObjId)
|
||||
{
|
||||
if (_firstDegradeModeByGfxObj.TryGetValue(gfxObjId, out uint? mode))
|
||||
return mode;
|
||||
|
||||
try
|
||||
{
|
||||
if (_dats?.Get<GfxObj>(gfxObjId) is { } gfx
|
||||
|
|
@ -584,7 +658,7 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
&& gfx.DIDDegrade != 0
|
||||
&& _dats.Get<GfxObjDegradeInfo>(gfx.DIDDegrade) is { Degrades.Count: > 0 } degrade)
|
||||
{
|
||||
firstDegradeMode = degrade.Degrades[0].DegradeMode;
|
||||
mode = degrade.Degrades[0].DegradeMode;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -596,9 +670,8 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
$"[particle-geometry] Failed to decode GfxObj 0x{gfxObjId:X8} degrade metadata: {ex.Message}");
|
||||
}
|
||||
|
||||
kind = RetailParticleGeometryClassifier.Classify(firstDegradeMode);
|
||||
_geometryKindByGfxObj[gfxObjId] = kind;
|
||||
return kind;
|
||||
_firstDegradeModeByGfxObj[gfxObjId] = mode;
|
||||
return mode;
|
||||
}
|
||||
|
||||
private void OnEmitterDied(int handle)
|
||||
|
|
@ -672,7 +745,8 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
texture: AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned,
|
||||
additive,
|
||||
hasMaterial: surfaceId != 0,
|
||||
surfaceId: surfaceId);
|
||||
surfaceId: surfaceId,
|
||||
degradeMode: ResolveFirstDegradeMode(gfxObjId) ?? 0u);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -685,7 +759,8 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
AcDream.App.Rendering.Gpu.GpuTextureSlot texture,
|
||||
bool additive,
|
||||
bool hasMaterial,
|
||||
uint surfaceId)
|
||||
uint surfaceId,
|
||||
uint degradeMode)
|
||||
{
|
||||
if (gfx.VertexArray.Vertices.Count == 0)
|
||||
return ParticleGfxInfo.Billboard(
|
||||
|
|
@ -779,24 +854,12 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
false,
|
||||
additive,
|
||||
hasMaterial,
|
||||
surfaceId);
|
||||
surfaceId,
|
||||
degradeMode);
|
||||
}
|
||||
|
||||
private bool IsPointSprite(GfxObj gfx)
|
||||
{
|
||||
if (!gfx.Flags.HasFlag(GfxObjFlags.HasDIDDegrade) || gfx.DIDDegrade == 0 || _dats is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var degrade = _dats.Get<GfxObjDegradeInfo>(gfx.DIDDegrade);
|
||||
return degrade?.Degrades.Count > 0 && degrade.Degrades[0].DegradeMode == 2;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
=> ResolveFirstDegradeMode(gfx.Id) == 2u;
|
||||
|
||||
private static float FallbackParticleExtent(float value)
|
||||
=> value > 1e-4f ? Math.Clamp(value, 1e-4f, 10_000f) : 1f;
|
||||
|
|
@ -886,10 +949,21 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
_particleGfxInfoByEmitter.Clear();
|
||||
_particleGfxInfoByGfxObj.Clear();
|
||||
_geometryKindByGfxObj.Clear();
|
||||
_firstDegradeModeByGfxObj.Clear();
|
||||
_meshBlendBySurface.Clear();
|
||||
_deferredAlpha.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <paramref name="DegradeMode"/> is the sprite GfxObj's FIRST degrade
|
||||
/// entry's mode — retail's facing selector
|
||||
/// (<c>CPhysicsPart::calc_draw_frame @0x0050DFA0</c>, see
|
||||
/// <see cref="AcDream.Core.Vfx.RetailParticleFacing"/>). Mode 2 sprites
|
||||
/// take the <paramref name="IsBillboard"/> quad path (face viewer,
|
||||
/// roll-free); modes 3–5 keep authored geometry but spin around one
|
||||
/// local axis toward the viewer; every other mode draws authored.
|
||||
/// Synthetic texture-only billboards carry mode 2 by construction.
|
||||
/// </summary>
|
||||
private readonly record struct ParticleGfxInfo(
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot TextureSlot,
|
||||
Vector2 Size,
|
||||
|
|
@ -899,7 +973,8 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
bool IsBillboard,
|
||||
bool Additive,
|
||||
bool HasMaterial,
|
||||
uint SurfaceId)
|
||||
uint SurfaceId,
|
||||
uint DegradeMode)
|
||||
{
|
||||
public static ParticleGfxInfo Default { get; } =
|
||||
Billboard(
|
||||
|
|
@ -926,6 +1001,7 @@ public sealed unsafe partial class ParticleRenderer : IDisposable
|
|||
true,
|
||||
additive,
|
||||
hasMaterial,
|
||||
surfaceId);
|
||||
surfaceId,
|
||||
DegradeMode: 2u);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue