From 0c552eecac0469523a74869fc8597502a3e8a679 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 21 Aug 2026 05:42:59 +0200 Subject: [PATCH] =?UTF-8?q?fix(world):=20honour=20SetOmega=20=E2=80=94=20t?= =?UTF-8?q?he=20birds=20and=20butterflies=20fly=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../RetailStaticAnimatingObjectScheduler.cs | 50 +++ .../Physics/AnimationSequencer.cs | 14 + .../StaticAnimatingOmegaHookTests.cs | 57 ++++ .../InstalledStaticAnimatingOmegaTests.cs | 100 ++++++ tools/AnimHookScan/AnimHookScan.csproj | 13 + tools/AnimHookScan/Program.cs | 105 ++++++ tools/AnimHookScan/packages.neutral.lock.json | 304 ++++++++++++++++++ 7 files changed, 643 insertions(+) create mode 100644 tests/AcDream.App.Tests/Rendering/StaticAnimatingOmegaHookTests.cs create mode 100644 tests/AcDream.Content.Tests/InstalledStaticAnimatingOmegaTests.cs create mode 100644 tools/AnimHookScan/AnimHookScan.csproj create mode 100644 tools/AnimHookScan/Program.cs create mode 100644 tools/AnimHookScan/packages.neutral.lock.json diff --git a/src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs b/src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs index ca3f1b8a..23823d86 100644 --- a/src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs +++ b/src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs @@ -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 hooks) + { + if (owner.Body is not { } body) + return; + if (TryResolveOmega(hooks, out Vector3 omega)) + body.Omega = omega; + } + + /// + /// The omega a hook batch leaves on the physics object, if any. + /// + /// + /// + /// 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. + /// + /// + /// Last hook wins: retail executes a batch in order and every + /// set_omega overwrites the vector outright rather than accumulating. + /// + /// + internal static bool TryResolveOmega( + IReadOnlyList 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; diff --git a/src/AcDream.Core/Physics/AnimationSequencer.cs b/src/AcDream.Core/Physics/AnimationSequencer.cs index 676c20f8..89b3f8f3 100644 --- a/src/AcDream.Core/Physics/AnimationSequencer.cs +++ b/src/AcDream.Core/Physics/AnimationSequencer.cs @@ -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. /// + /// + /// The hooks that have fired since the last , + /// WITHOUT draining them. + /// + /// + /// Retail's CPhysicsObj::process_hooks executes every queued hook + /// against the owning CPhysicsObj, and a hook may act on BOTH the + /// physics object and presentation — SetOmegaHook::Execute + /// (0x00526F30) writes m_omegaVector. 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. + /// + public IReadOnlyList PendingHooks => _pendingHooks; + public IReadOnlyList ConsumePendingHooks() { if (_pendingHooks.Count == 0) diff --git a/tests/AcDream.App.Tests/Rendering/StaticAnimatingOmegaHookTests.cs b/tests/AcDream.App.Tests/Rendering/StaticAnimatingOmegaHookTests.cs new file mode 100644 index 00000000..324cb832 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/StaticAnimatingOmegaHookTests.cs @@ -0,0 +1,57 @@ +using System.Numerics; +using AcDream.App.Rendering; +using DatReaderWriter.Types; + +namespace AcDream.App.Tests.Rendering; + +/// +/// The SetOmega hook is what moves AC's ambient flyers; ignoring it left the +/// birds and butterflies animating in place. +/// +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); + } +} diff --git a/tests/AcDream.Content.Tests/InstalledStaticAnimatingOmegaTests.cs b/tests/AcDream.Content.Tests/InstalledStaticAnimatingOmegaTests.cs new file mode 100644 index 00000000..3ae1c42e --- /dev/null +++ b/tests/AcDream.Content.Tests/InstalledStaticAnimatingOmegaTests.cs @@ -0,0 +1,100 @@ +using DatReaderWriter; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; +using DatReaderWriter.Options; +using DatReaderWriter.Types; + +namespace AcDream.Content.Tests; + +/// +/// Pins the data the ambient flyers (circling birds, flitting butterflies) +/// depend on, against the installed retail DATs. +/// +/// +/// +/// 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, whose only motion step is +/// Frame::grotate(&m_position.frame, &m_omegaVector). That +/// vector is written by exactly one thing: +/// SetOmegaHook::Execute @ 0x00526F30CPhysicsObj::set_omega +/// @ 0x0050F6D0. +/// +/// +/// 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. +/// +/// +/// 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. +/// +/// +[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(id, out Setup? setup) || setup is null) + continue; + uint animId = setup.DefaultAnimation?.DataId ?? 0u; + if (animId == 0 + || !dats.Portal.TryGet(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."); + } + } +} diff --git a/tools/AnimHookScan/AnimHookScan.csproj b/tools/AnimHookScan/AnimHookScan.csproj new file mode 100644 index 00000000..d20c9da7 --- /dev/null +++ b/tools/AnimHookScan/AnimHookScan.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + enable + enable + AnimHookScan + + + + + + diff --git a/tools/AnimHookScan/Program.cs b/tools/AnimHookScan/Program.cs new file mode 100644 index 00000000..6f3ec0db --- /dev/null +++ b/tools/AnimHookScan/Program.cs @@ -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(); +var scriptHookCounts = new Dictionary(); +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(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(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(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(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(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(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(); +} diff --git a/tools/AnimHookScan/packages.neutral.lock.json b/tools/AnimHookScan/packages.neutral.lock.json new file mode 100644 index 00000000..2bb494b4 --- /dev/null +++ b/tools/AnimHookScan/packages.neutral.lock.json @@ -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==" + } + } + } +} \ No newline at end of file