fix(world): honour SetOmega — the birds and butterflies fly again

Ambient flyers played their wing animation and stayed put.

A Static object whose Setup declares a DefaultAnimation joins retail's
CPhysics::static_animating_objects workset (CPhysicsObj::InitDefaults
@0x00513A7B) and is driven by animate_static_object @0x00513DF0. That function
has exactly one motion step:

    CPartArray::Update(part_array, dt, nullptr);            // animate
    Frame::grotate(&this->m_position.frame, &this->m_omegaVector);

Note the nullptr: unlike UpdatePositionInternal @0x00512C30, which combines the
animation's accumulated frame into the object's position, the static branch
DISCARDS it. These objects cannot move by animation translation at all. The
omega vector is the whole mechanism, and one thing writes it —
SetOmegaHook::Execute @0x00526F30 -> CPhysicsObj::set_omega @0x0050F6D0.

We decoded that hook and then dropped it on the floor: IAnimationHookSink's own
docs list SetOmegaHook among the unwired ones, and PhysicsBody.Omega was
assigned nowhere outside projectiles. The scheduler's GRotate call was already
correct — it was multiplying by a permanent zero.

The hook is now applied to the owning body at process_hooks time. Retail runs
process_hooks AFTER the grotate in the same pass, so a newly-set omega first
takes effect on the following frame; our Tick/ProcessHooks split already had
that order.

Scoped from the data rather than guessed. tools/AnimHookScan (new) walks the
dat: of 2,066 animations exactly 8 contain SetOmega, and all 8 are the
DefaultAnimation of one of the 8 setups that use it. No creature animation uses
it, so this belongs precisely where body.Omega is read and nowhere else.

The same scan is why the fix is believable as FLIGHT rather than a pirouette.
Every authored omega is pure yaw, and the setups' parts sit 5.6m, 4.2m, 12m and
36.8m from the origin they spin about. Rotating a frame whose mesh hangs 12m
off-axis carries it around a 12m circle — that offset IS the flight radius. An
installed-DAT test pins both properties, because the fix is only correct while
they hold and neither is visible from the code.

Also checked and deliberately NOT conflated: CSequence::set_omega @0x005248A0
writes CSequence::omega, a different field from CPhysicsObj::m_omegaVector,
fed by the motion table for creature turning. Only the latter drives grotate.

Solution builds clean; 14,473 tests pass on the standard hermetic lane filter
plus the new installed-DAT test, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-21 05:42:59 +02:00
parent 255b0aaeda
commit 0c552eecac
7 changed files with 643 additions and 0 deletions

View file

@ -516,6 +516,16 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
continue;
}
// SetOmegaHook::Execute 0x00526F30 -> CPhysicsObj::set_omega
// 0x0050F6D0 writes m_omegaVector on the owning physics object,
// which animate_static_object then feeds to Frame::grotate every
// frame. Apply it BEFORE the presentation sink drains the queue.
//
// Retail runs process_hooks AFTER the grotate in the same pass, so
// a newly-set omega first takes effect on the following frame; our
// Tick/ProcessHooks split preserves that ordering.
ApplyOmegaHooks(owner, sequencer.PendingHooks);
// Clear before the callback: hook delivery may unregister or
// replace the owner, and a nested caller must not replay this tail.
owner.PendingProcessHooks = null;
@ -523,6 +533,46 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
}
}
private static void ApplyOmegaHooks(Owner owner, IReadOnlyList<AnimationHook> hooks)
{
if (owner.Body is not { } body)
return;
if (TryResolveOmega(hooks, out Vector3 omega))
body.Omega = omega;
}
/// <summary>
/// The omega a hook batch leaves on the physics object, if any.
/// </summary>
/// <remarks>
/// <para>
/// This is the mechanism behind AC's circling birds and flitting
/// butterflies, and it is not a translation: every authored omega in the
/// dat is pure yaw, and the setup's parts sit well off the origin (5.6 m to
/// 36.8 m across the eight setups that use it). Spinning a frame whose mesh
/// hangs metres off the axis carries that mesh around a circle of the same
/// radius, which is what reads as flight.
/// </para>
/// <para>
/// Last hook wins: retail executes a batch in order and every
/// <c>set_omega</c> overwrites the vector outright rather than accumulating.
/// </para>
/// </remarks>
internal static bool TryResolveOmega(
IReadOnlyList<AnimationHook> hooks, out Vector3 omega)
{
omega = default;
bool found = false;
for (int i = 0; i < hooks.Count; i++)
{
if (hooks[i] is not SetOmegaHook set)
continue;
omega = new Vector3(set.Axis.X, set.Axis.Y, set.Axis.Z);
found = true;
}
return found;
}
private bool IsResidentAtVersion(Owner owner, ulong version) =>
_isResident(owner.Entity)
&& _residencyVersion(owner.Entity) == version;

View file

@ -548,6 +548,20 @@ public sealed class AnimationSequencer
/// Empty when no frame boundary was crossed. Safe to call multiple
/// times per frame; second and subsequent calls return an empty list.
/// </summary>
/// <summary>
/// The hooks that have fired since the last <see cref="ConsumePendingHooks"/>,
/// WITHOUT draining them.
/// </summary>
/// <remarks>
/// Retail's <c>CPhysicsObj::process_hooks</c> executes every queued hook
/// against the owning <c>CPhysicsObj</c>, and a hook may act on BOTH the
/// physics object and presentation — <c>SetOmegaHook::Execute</c>
/// (<c>0x00526F30</c>) writes <c>m_omegaVector</c>. Draining the queue for
/// the presentation sink would hide those hooks from the physics owner, so
/// the owner peeks here and the sink still consumes the complete stream.
/// </remarks>
public IReadOnlyList<AnimationHook> PendingHooks => _pendingHooks;
public IReadOnlyList<AnimationHook> ConsumePendingHooks()
{
if (_pendingHooks.Count == 0)

View file

@ -0,0 +1,57 @@
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter.Types;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// The SetOmega hook is what moves AC's ambient flyers; ignoring it left the
/// birds and butterflies animating in place.
/// </summary>
public sealed class StaticAnimatingOmegaHookTests
{
private static SetOmegaHook Omega(float x, float y, float z)
=> new() { Axis = new Vector3(x, y, z) };
[Fact]
public void NoHooksLeavesOmegaUntouched()
{
Assert.False(
RetailStaticAnimatingObjectScheduler.TryResolveOmega([], out _));
}
[Fact]
public void AHookBatchWithoutSetOmegaLeavesOmegaUntouched()
{
// A batch that carries other hooks must not zero a previously set
// omega: retail's set_omega is only ever called BY the hook.
Assert.False(
RetailStaticAnimatingObjectScheduler.TryResolveOmega(
[new SoundTableHook()], out _));
}
[Fact]
public void SetOmegaIsTakenVerbatim()
{
Assert.True(
RetailStaticAnimatingObjectScheduler.TryResolveOmega(
[Omega(0f, 0f, -0.027f)], out Vector3 omega));
// CPhysicsObj::set_omega @ 0x0050F6D0 assigns the axis outright — no
// scaling, and animate_static_object does not multiply it by elapsed
// time either.
Assert.Equal(new Vector3(0f, 0f, -0.027f), omega);
}
[Fact]
public void TheLastSetOmegaInABatchWins()
{
// Retail executes a hook batch in order and each set_omega overwrites
// the vector outright rather than accumulating.
Assert.True(
RetailStaticAnimatingObjectScheduler.TryResolveOmega(
[Omega(0f, 0f, -0.02f), Omega(0f, 0f, 0.05f)],
out Vector3 omega));
Assert.Equal(new Vector3(0f, 0f, 0.05f), omega);
}
}

View file

@ -0,0 +1,100 @@
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
namespace AcDream.Content.Tests;
/// <summary>
/// Pins the data the ambient flyers (circling birds, flitting butterflies)
/// depend on, against the installed retail DATs.
/// </summary>
/// <remarks>
/// <para>
/// A Static object whose Setup declares a DefaultAnimation joins retail's
/// <c>CPhysics::static_animating_objects</c> workset
/// (<c>CPhysicsObj::InitDefaults @ 0x00513A7B</c>) and is driven by
/// <c>animate_static_object @ 0x00513DF0</c>, whose only motion step is
/// <c>Frame::grotate(&amp;m_position.frame, &amp;m_omegaVector)</c>. That
/// vector is written by exactly one thing:
/// <c>SetOmegaHook::Execute @ 0x00526F30</c> → <c>CPhysicsObj::set_omega
/// @ 0x0050F6D0</c>.
/// </para>
/// <para>
/// So these objects move ONLY if the SetOmega hook is honoured. acdream
/// decoded the hook and then ignored it, leaving m_omegaVector at zero: the
/// wings animated and nothing flew.
/// </para>
/// <para>
/// This test exists because the fix rests on two claims about shipped data
/// that are invisible from the code: that the authored omegas are pure YAW,
/// and that the meshes hang far off the axis they spin about. Rotation only
/// reads as flight because of the second one — a bird spinning about its own
/// centre would just pirouette. If a dat ever contradicts either, the fix is
/// wrong and this should say so rather than the behaviour quietly changing.
/// </para>
/// </remarks>
[Trait("Lane", "InstalledDat")]
public sealed class InstalledStaticAnimatingOmegaTests
{
[Fact]
public void EverySetOmegaAnimationIsPureYawOnAMeshOffsetFromItsAxis()
{
string? datDir = ContentConformanceDats.ResolveDatDir();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
var found = new List<(uint Setup, float Yaw, double Radius)>();
foreach (uint id in dats.Portal.Tree.Select(e => e.Id).Where(i => (i >> 24) == 0x02))
{
if (!dats.Portal.TryGet<Setup>(id, out Setup? setup) || setup is null)
continue;
uint animId = setup.DefaultAnimation?.DataId ?? 0u;
if (animId == 0
|| !dats.Portal.TryGet<Animation>(animId, out Animation? anim)
|| anim is null)
{
continue;
}
foreach (var frame in anim.PartFrames)
foreach (var hook in frame.Hooks)
{
if (hook is not SetOmegaHook set)
continue;
// Pure yaw: a non-zero X or Y would tumble the object, and
// the circle-tracing reading of this mechanism would fail.
Assert.Equal(0f, set.Axis.X, 5);
Assert.Equal(0f, set.Axis.Y, 5);
Assert.NotEqual(0f, set.Axis.Z);
double radius = 0d;
foreach (var placement in setup.PlacementFrames.Values)
foreach (var af in placement.Frames)
radius = Math.Max(
radius,
Math.Sqrt((af.Origin.X * af.Origin.X)
+ (af.Origin.Y * af.Origin.Y)));
found.Add((id, set.Axis.Z, radius));
}
}
Assert.NotEmpty(found);
// Every one of them hangs metres off its own spin axis — that offset IS
// the flight radius. One metre would be a pirouette, not a circuit.
foreach ((uint setupId, _, double radius) in found)
{
Assert.True(
radius > 1d,
$"setup 0x{setupId:X8} spins about an axis only {radius:0.###}m "
+ "from its mesh; rotation would not read as flight.");
}
}
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>AnimHookScan</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chorizite.DatReaderWriter" />
<ProjectReference Include="..\..\src\AcDream.Content\AcDream.Content.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,105 @@
// What does retail actually put in the animations of STATIC ANIMATING objects?
//
// A Static object whose Setup declares a DefaultAnimation or DefaultScript
// joins CPhysics::static_animating_objects (CPhysicsObj::InitDefaults
// @0x00513A7B) and is driven every frame by animate_static_object
// @0x00513DF0. This walks every Setup in portal.dat, follows those defaults,
// and tallies the hook types they carry -- so "which hooks must be honoured
// for this class of object to behave" is read from the data, not assumed.
using AcDream.Content;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
using SysEnv = System.Environment;
string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
using var dats = new DatCollection(datDir, DatAccessType.Read);
var animHookCounts = new Dictionary<AnimationHookType, int>();
var scriptHookCounts = new Dictionary<AnimationHookType, int>();
var omegaSetups = new List<(uint Setup, uint Anim)>();
int setups = 0, withAnim = 0, withScript = 0;
foreach (uint id in dats.Portal.Tree.Select(e => e.Id).Where(i => (i >> 24) == 0x02))
{
if (!dats.Portal.TryGet<Setup>(id, out var setup) || setup is null) continue;
setups++;
uint animId = setup.DefaultAnimation?.DataId ?? 0u;
uint scriptId = setup.DefaultScript?.DataId ?? 0u;
if (animId != 0) withAnim++;
if (scriptId != 0) withScript++;
if (animId != 0 && dats.Portal.TryGet<Animation>(animId, out var anim) && anim is not null)
{
bool omega = false;
foreach (var frame in anim.PartFrames)
foreach (var hook in frame.Hooks)
{
animHookCounts[hook.HookType] = animHookCounts.GetValueOrDefault(hook.HookType) + 1;
if (hook.HookType == AnimationHookType.SetOmega) omega = true;
}
if (omega) omegaSetups.Add((id, animId));
}
if (scriptId != 0
&& dats.Portal.TryGet<PhysicsScript>(scriptId, out var script) && script is not null)
{
foreach (var d in script.ScriptData)
{
AnimationHookType t = d.Hook.HookType;
scriptHookCounts[t] = scriptHookCounts.GetValueOrDefault(t) + 1;
}
}
}
// Every animation in the dat, not just the Setup defaults: tells us whether
// SetOmega is exclusively a static-animating-scenery mechanism or whether
// creatures use it too (which would widen where the hook must be honoured).
int allAnims = 0, animsWithOmega = 0;
foreach (uint id in dats.Portal.Tree.Select(e => e.Id).Where(i => (i >> 24) == 0x03))
{
if (!dats.Portal.TryGet<Animation>(id, out var a2) || a2 is null) continue;
allAnims++;
bool has = false;
foreach (var fr in a2.PartFrames)
foreach (var h in fr.Hooks)
if (h.HookType == AnimationHookType.SetOmega) has = true;
if (has) animsWithOmega++;
}
Console.WriteLine($"ALL animations={allAnims} containingSetOmega={animsWithOmega}");
Console.WriteLine($"setups={setups} withDefaultAnimation={withAnim} withDefaultScript={withScript}");
Console.WriteLine("\n-- hook types in DefaultAnimation --");
foreach (var kv in animHookCounts.OrderByDescending(k => k.Value))
Console.WriteLine($" {kv.Key,-28} {kv.Value}");
Console.WriteLine("\n-- hook types in DefaultScript --");
foreach (var kv in scriptHookCounts.OrderByDescending(k => k.Value))
Console.WriteLine($" {kv.Key,-28} {kv.Value}");
Console.WriteLine($"\n-- setups whose DefaultAnimation carries SetOmega: {omegaSetups.Count} --");
foreach (var (s, a) in omegaSetups)
{
Console.Write($" setup 0x{s:X8} -> anim 0x{a:X8}");
if (dats.Portal.TryGet<Setup>(s, out var su) && su is not null)
{
Console.Write($" parts={su.Parts.Count}");
// How far the authored part frames sit from the setup origin: a mesh
// offset from the point it spins about traces a CIRCLE under grotate.
double maxR = 0;
foreach (var f in su.PlacementFrames.Values)
foreach (var af in f.Frames)
maxR = Math.Max(maxR, Math.Sqrt(
af.Origin.X * af.Origin.X + af.Origin.Y * af.Origin.Y));
Console.Write($" maxPartRadius={maxR:0.###}m");
}
if (dats.Portal.TryGet<Animation>(a, out var an) && an is not null)
foreach (var fr in an.PartFrames)
foreach (var h in fr.Hooks)
if (h is SetOmegaHook so)
Console.Write($" omega=({so.Axis.X:0.###},{so.Axis.Y:0.###},{so.Axis.Z:0.###})");
Console.WriteLine();
}

View file

@ -0,0 +1,304 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Chorizite.DatReaderWriter": {
"type": "Direct",
"requested": "[2.1.7, )",
"resolved": "2.1.7",
"contentHash": "6CpUhfHDV/O+lNx0xUZUcXK7wGkEkHYCnGHFtD8BBeAv4i1BuaNfTID+VQoSzx2EtmOVM0A2O/9wRFibApz6rQ==",
"dependencies": {
"DotNet.Standard.Common": "2.0.1",
"ZLibDotNet": "0.1.1"
}
},
"Autofac": {
"type": "Transitive",
"resolved": "8.4.0",
"contentHash": "XMWHyO6fXTv8rwCfhm6+64mQS6CyL0rve/hWSODsUrVuEGtq1fjxSOlVTBqCRsW6L8K3OQDskJaPB1boVMI2eQ=="
},
"Chorizite.ACProtocol": {
"type": "Transitive",
"resolved": "1.0.1",
"contentHash": "PVDw/KRu4WPxT+2MzHwOQ9UFqYlOgpIRswSnco/EgHSHnbQyxOqxwiOwSK0Il+cI6dTSXTW34QNmL7iH0lXLKw==",
"dependencies": {
"Chorizite.Common": "1.0.0",
"Medo.PcapRW": "1.2.0",
"Microsoft.Extensions.Logging.Abstractions": "9.0.0",
"System.CodeDom": "9.0.0"
}
},
"Chorizite.Common": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "KqI0su7UY2diiSQuq11gF/NztqR6orZLr/e5UKTQ91XM8OsZGgBGHmhf2/jopX9VhwpXOTTLIeFBYTBc30cK8w==",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "9.0.0"
}
},
"CommunityToolkit.HighPerformance": {
"type": "Transitive",
"resolved": "8.4.0",
"contentHash": "flxspiBs0G/0GMp7IK2J2ijV9bTG6hEwFc/z6ekHqB6nwRJ4Ry2yLdx+TkbCUYFCl4XhABkAwomeKbT6zM2Zlg=="
},
"Cyotek.Drawing.BitmapFont": {
"type": "Transitive",
"resolved": "2.0.4",
"contentHash": "iA6WehGVdMUuNbfsQQDq/Bt+mMd/OqHjiMUtKFLIQd/0pyYh4ehT7FEjTxN9/4OXNKQZsp9bAJgltP2nnswUJg=="
},
"DotNet.Standard.Common": {
"type": "Transitive",
"resolved": "2.0.1",
"contentHash": "zW0m0ytHi43ccbEOTNDa10cDDnT7BAzY1R1Rb1dlhbdkiyglsALjrsyTPSEjbdnTmCOAvAvl4kkbvBLoYhC6dQ=="
},
"FontStashSharp": {
"type": "Transitive",
"resolved": "1.3.10",
"contentHash": "7JTrihTt3DR8LYbb4L1eZcnbwOUOu/mvY+PJoZ3WVWiKjA6xNUk93GSW3OC/kZBD3iYzrXK8QlmKyYY5Lef/Rg==",
"dependencies": {
"Cyotek.Drawing.BitmapFont": "2.0.4",
"FontStashSharp.Base": "1.1.9",
"FontStashSharp.Rasterizers.StbTrueTypeSharp": "1.1.9",
"StbImageSharp": "2.30.15"
}
},
"FontStashSharp.Base": {
"type": "Transitive",
"resolved": "1.1.9",
"contentHash": "/AjkOcPNijs8vyNgcCj3FfBJbVWmsSH744hqkhLfBt8qspDz/tEoD+U09my5u9eRBX6zX+RLQ/gdCvwy+ZBOtg=="
},
"FontStashSharp.Rasterizers.StbTrueTypeSharp": {
"type": "Transitive",
"resolved": "1.1.9",
"contentHash": "yi5iuTERem46uyHC5p+jRi3Jh8dKWzgNWLqcvHciGlyVHD1cWFdERgnxshZU4xWB2hRGnogxaCudCirbMpg4eQ==",
"dependencies": {
"FontStashSharp.Base": "1.1.9",
"StbTrueTypeSharp": "1.26.12"
}
},
"Medo.PcapRW": {
"type": "Transitive",
"resolved": "1.2.0",
"contentHash": "vgwcHDg60Q9LJfry7twA78pUFio1P4EypI2IIlk+7mEuySsIRInm+Gx2OINDICyocbuyQZdS/zABjbmejAObeg=="
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg=="
},
"Microsoft.Diagnostics.NETCore.Client": {
"type": "Transitive",
"resolved": "0.2.410101",
"contentHash": "I4hMjlbPcM5R+M4ThD2Zt1z58M8uZnWkDbFLXHntOOAajajEucrw4XYNSaoi5rgoqksgxQ3g388Vof4QzUNwdQ==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "1.1.0",
"Microsoft.Extensions.Logging": "2.1.1"
}
},
"Microsoft.Diagnostics.Runtime": {
"type": "Transitive",
"resolved": "3.1.512801",
"contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==",
"dependencies": {
"Microsoft.Diagnostics.NETCore.Client": "0.2.410101"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "LjVKO6P2y52c5ZhTLX/w8zc5H4Y3J/LJsgqTBj49TtFq/hAtVNue/WA0F6/7GMY90xhD7K0MDZ4qpOeWXbLvzg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "2.1.1"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "VfuZJNa0WUshZ/+8BFZAhwFKiKuu/qOUCFntfdLpHj7vcRnsGHqd3G2Hse78DM+pgozczGM63lGPRLmy+uhUOA==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.1.1"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "fcLCTS03poWE4v9tSNBr3pWn0QwGgAn1vzqHXlXgvqZeOc7LvQNzaWcKRQZTdEc3+YhQKwMsOtm3VKSA2aWQ8w==",
"dependencies": {
"Microsoft.Extensions.Configuration": "2.1.1"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "/hymojfWbE9AlDOa0mczR44m00Jj+T3+HZO0ZnVTI032fVycI0ZbNOVFP6kqZMcXiLSYXzR2ilcwaRi6dzeGyA=="
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "hh+mkOAQDTp6XH80xJt3+wwYVzkbwYQl9XZRCz4Um0JjP/o7N9vHM3rZ6wwwtr+BBe/L6iBO2sz0px6OWBzqZQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Binder": "2.1.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1",
"Microsoft.Extensions.Logging.Abstractions": "2.1.1",
"Microsoft.Extensions.Options": "2.1.1"
}
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "V7lXCU78lAbzaulCGFKojcCyG8RTJicEbiBkPJjFqiqXwndEBBIehdXRMWEVU3UtzQ1yDvphiWUL9th6/4gJ7w==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1",
"Microsoft.Extensions.Primitives": "2.1.1"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "2.1.1",
"contentHash": "scJ1GZNIxMmjpENh0UZ8XCQ6vzr/LzeF9WvEA51Ix2OQGAs9WPgPu8ABVUdvpKPLuor/t05gm6menJK3PwqOXg=="
},
"Namotion.Reflection": {
"type": "Transitive",
"resolved": "3.4.3",
"contentHash": "KLk2gLR9f8scM82EiL+p9TONXXPy9+IAZVMzJOA/Wsa7soZD7UJGG6j0fq0D9ZoVnBRRnSeEC7kShhRo3Olgaw=="
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"NJsonSchema": {
"type": "Transitive",
"resolved": "11.5.1",
"contentHash": "3a7ntoBncSKkLgpIhT3uQ8BiyDzYKOHIzpzNF4o1vtKc+Re4vWxBcDXFDarOWcr/UkxZ8nxRXbbWk05j6bXFzQ==",
"dependencies": {
"NJsonSchema.Annotations": "11.5.1",
"Namotion.Reflection": "3.4.3",
"Newtonsoft.Json": "13.0.3"
}
},
"NJsonSchema.Annotations": {
"type": "Transitive",
"resolved": "11.5.1",
"contentHash": "xiqZ2DBJM1HuV+EhXgueb5ZUBlWFN3kVfLTKdtpTSxvtyQCO/vit8lqZiUiejnReUMRMIUhtS9m0GbieHZlSow=="
},
"SixLabors.Fonts": {
"type": "Transitive",
"resolved": "2.1.3",
"contentHash": "ORWbZ5BHrC/LZvo+Y09MnoJq5VUKD85LsYALk+YI7CHFra+m5arCkz00IntDM6SrAiB22bvSdKtKmuCyHOKlqg=="
},
"SixLabors.ImageSharp.Drawing": {
"type": "Transitive",
"resolved": "2.1.7",
"contentHash": "9KwCo9Fa350cx6ckpsy8NqXQZKwir4RQ8Kj0sdCmJA7wsK9FMyfgC527Sn4l/D6bj2ditSHlhS7dGzcgGszvSQ==",
"dependencies": {
"SixLabors.Fonts": "2.1.3",
"SixLabors.ImageSharp": "3.1.11"
}
},
"System.CodeDom": {
"type": "Transitive",
"resolved": "9.0.0",
"contentHash": "oTE5IfuMoET8yaZP/vdvy9xO47guAv/rOhe4DODuFBN3ySprcQOlXqO3j+e/H/YpKKR5sglrxRaZ2HYOhNJrqA=="
},
"ZLibDotNet": {
"type": "Transitive",
"resolved": "0.1.1",
"contentHash": "QEti4O7dwRcOb9zbnLuudSrt2IT61OYjq0R7lcJb+EzUm5N6djOVGU/cp+FZIZzJUnaFIntwrpwiuRvhpS7ZHg=="
},
"acdream.content": {
"type": "Project",
"dependencies": {
"AcDream.Core": "[1.0.0, )",
"BCnEncoder.Net.ImageSharp": "[1.1.2, )",
"SixLabors.ImageSharp": "[3.1.12, )"
}
},
"acdream.core": {
"type": "Project",
"dependencies": {
"AcDream.Plugin.Abstractions": "[1.0.0, )",
"BCnEncoder.Net": "[2.2.1, )",
"Chorizite.Core": "[0.0.18, )",
"Chorizite.DatReaderWriter": "[2.1.7, )",
"Serilog": "[4.0.2, )",
"StbImageSharp": "[2.30.16, )"
}
},
"acdream.plugin.abstractions": {
"type": "Project"
},
"BCnEncoder.Net": {
"type": "CentralTransitive",
"requested": "[2.2.1, )",
"resolved": "2.2.1",
"contentHash": "tI5+/OQo0kciLqWrViRjpOH+IL3FjexYnoWZajiGV41g/EM9CGbWsxsPzBDmpoxNkrV9uox/EtIhCIi9chBSFw==",
"dependencies": {
"CommunityToolkit.HighPerformance": "8.4.0"
}
},
"BCnEncoder.Net.ImageSharp": {
"type": "CentralTransitive",
"requested": "[1.1.2, )",
"resolved": "1.1.2",
"contentHash": "qUi8L+bNfHJii95BMBcV6MhBchkKU2VV6sd6D1yyzgm77YhMt+aFT0keh5uf70bvTsRrq/ZKQnE4UQScNU6XAA==",
"dependencies": {
"BCnEncoder.Net": "2.2.0",
"CommunityToolkit.HighPerformance": "8.4.0",
"SixLabors.ImageSharp": "3.1.7"
}
},
"Chorizite.Core": {
"type": "CentralTransitive",
"requested": "[0.0.18, )",
"resolved": "0.0.18",
"contentHash": "Pvf5idSsN0NfhZspJhrpo7QiNTmHx3mnKFkmAvHFpEFx+RnqfXh96Q+Pxam+hrqXbimmKGfk1ooIl5kpQcMo6w==",
"dependencies": {
"Autofac": "8.4.0",
"Chorizite.ACProtocol": "1.0.1",
"Chorizite.Common": "1.0.3",
"Chorizite.DatReaderWriter": "1.0.0",
"FontStashSharp": "1.3.10",
"Microsoft.Diagnostics.Runtime": "3.1.512801",
"Microsoft.Extensions.Logging.Abstractions": "9.0.9",
"NJsonSchema": "11.5.1",
"SixLabors.ImageSharp": "3.1.11",
"SixLabors.ImageSharp.Drawing": "2.1.7"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[9.0.9, )",
"resolved": "9.0.9",
"contentHash": "FEgpSF+Z9StMvrsSViaybOBwR0f0ZZxDm8xV5cSOFiXN/t+ys+rwAlTd/6yG7Ld1gfppgvLcMasZry3GsI9lGA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.9"
}
},
"Serilog": {
"type": "CentralTransitive",
"requested": "[4.0.2, )",
"resolved": "4.0.2",
"contentHash": "Vehq4uNYtURe/OnHEpWGvMgrvr5Vou7oZLdn3BuEH5FSCeHXDpNJtpzWoqywXsSvCTuiv0I65mZDRnJSeUvisA=="
},
"SixLabors.ImageSharp": {
"type": "CentralTransitive",
"requested": "[3.1.12, )",
"resolved": "3.1.12",
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
},
"StbImageSharp": {
"type": "CentralTransitive",
"requested": "[2.30.16, )",
"resolved": "2.30.16",
"contentHash": "qg1i+NHihXVKLKYacGKrauhSQIGL31eBWcTC4Vc7jnGmBFj87LkRuCdXB5aDZisWMRnB6x2mcfwYXQxMWEG/lw=="
},
"StbTrueTypeSharp": {
"type": "CentralTransitive",
"requested": "[1.26.12, )",
"resolved": "1.26.12",
"contentHash": "hCc6/OsfcPa5VsLECcEU2m78WOshBrKwK42nAodSm9Z5wH68f7n66SoiRLCdGCkDaqbWz2TlX4zYHIjogj1HJA=="
}
}
}
}