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