Harden movie playback lifecycle and diagnostics
This commit is contained in:
@@ -26,6 +26,11 @@
|
||||
<Compile Include="..\..\godot\MovieRuntime.cs" Link="MovieRuntime.cs" />
|
||||
<Compile Include="..\..\godot\FfmpegMovieNative.cs" Link="FfmpegMovieNative.cs" />
|
||||
<Compile Include="..\..\godot\FfmpegMovieDecoder.cs" Link="FfmpegMovieDecoder.cs" />
|
||||
<Compile Include="..\..\godot\PageLocatorState.cs" Link="PageLocatorState.cs" />
|
||||
<Compile Include="..\..\godot\GodotTimelineLog.cs" Link="GodotTimelineLog.cs" />
|
||||
<Compile Include="..\..\godot\GodotTraceSink.cs" Link="GodotTraceSink.cs" />
|
||||
<Compile Include="..\..\godot\MovieSurfaceRegistry.cs" Link="MovieSurfaceRegistry.cs" />
|
||||
<Compile Include="..\..\tools\movie-corpus-gate\MovieCorpusGate.cs" Link="MovieCorpusGate.cs" />
|
||||
<Compile Include="..\..\godot\DirectShowMovieDecoder.cs" Link="DirectShowMovieDecoder.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
24
engine/Age.Engine.Tests/GfxDiagnosticSnapshotTests.cs
Normal file
24
engine/Age.Engine.Tests/GfxDiagnosticSnapshotTests.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Age.Engine.Model;
|
||||
|
||||
public class GfxDiagnosticSnapshotTests
|
||||
{
|
||||
[Fact]
|
||||
public void SnapshotIdentifiesOnlyChannelsThatCanBlockPresentationWait()
|
||||
{
|
||||
var gfx = new GfxState();
|
||||
gfx.BindDraw(0x100, 7, 0, 0, 32, 32, 0, 0);
|
||||
gfx.SetTranslationChannel(0x100, 25, 300, (20, 30, 0));
|
||||
gfx.BindDraw(0x200, 8, 0, 0, 32, 32, 0, 0);
|
||||
gfx.SetScaleChannel(0x200, 0, 500, (200, 200, 100));
|
||||
gfx.SetOneShotAnimationControl(0x200, 1);
|
||||
|
||||
GfxDiagnosticSnapshot snapshot = gfx.CaptureDiagnosticSnapshot(1000);
|
||||
|
||||
Assert.True(snapshot.HasActiveTimedPresentation);
|
||||
var blocking = Assert.Single(snapshot.BlockingObjects);
|
||||
Assert.Equal(0x100, blocking.Handle);
|
||||
Assert.True(blocking.TranslationEnabled);
|
||||
Assert.Equal(25, blocking.TranslationDelayMs);
|
||||
Assert.Equal(300, blocking.TranslationDurationMs);
|
||||
}
|
||||
}
|
||||
31
engine/Age.Engine.Tests/GodotTraceSinkTests.cs
Normal file
31
engine/Age.Engine.Tests/GodotTraceSinkTests.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Age.Engine.Diagnostics;
|
||||
using Age.Engine.Model;
|
||||
|
||||
public class GodotTraceSinkTests
|
||||
{
|
||||
[Fact]
|
||||
public void SnapshotRetainsCurrentStackAndBoundedRecentSteps()
|
||||
{
|
||||
using var locator = new PageLocatorState("SYSTEM4", null);
|
||||
var sink = new GodotTraceSink(locator);
|
||||
sink.Emit(TraceEvent.FrameEnter("SYSTEM4.BIN", 0, FrameCause.TopScene));
|
||||
sink.Emit(TraceEvent.FrameEnter("BTL.BIN", 1, FrameCause.CallScript, 0x2b10));
|
||||
|
||||
for (int index = 0; index < 140; index++)
|
||||
{
|
||||
var instruction = new Instruction(0x2400 + index, 0x100 + index,
|
||||
Array.Empty<Operand>());
|
||||
sink.Emit(TraceEvent.Step(index, instruction, 1));
|
||||
}
|
||||
|
||||
GodotTraceSnapshot snapshot = sink.Snapshot();
|
||||
|
||||
Assert.Equal("BTL.BIN", snapshot.CurrentScript);
|
||||
Assert.Equal(0x2400 + 139, snapshot.CurrentOffset);
|
||||
Assert.Equal(0x100 + 139, snapshot.CurrentOpcode);
|
||||
Assert.Equal(new[] { "SYSTEM4.BIN", "BTL.BIN" }, snapshot.CallStack);
|
||||
Assert.Equal(128, snapshot.RecentSteps.Count);
|
||||
Assert.Equal(0x2400 + 12, snapshot.RecentSteps[0].Offset);
|
||||
Assert.Equal(0x2400 + 139, snapshot.RecentSteps[^1].Offset);
|
||||
}
|
||||
}
|
||||
102
engine/Age.Engine.Tests/MovieCorpusGateTests.cs
Normal file
102
engine/Age.Engine.Tests/MovieCorpusGateTests.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Age.Engine.Sys4;
|
||||
|
||||
public class MovieCorpusGateTests
|
||||
{
|
||||
private sealed class FakeFrameSource(FfmpegMovieInfo info,
|
||||
params FfmpegVideoFrame[] frames) : IFfmpegFrameSource
|
||||
{
|
||||
private readonly Queue<FfmpegVideoFrame> _frames = new(frames);
|
||||
public FfmpegMovieInfo Info { get; } = info;
|
||||
public bool Disposed { get; private set; }
|
||||
public bool TryDecodeNextVideoFrame(out FfmpegVideoFrame frame)
|
||||
=> _frames.TryDequeue(out frame!);
|
||||
public void Dispose() => Disposed = true;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SequenceHeaderProbeReadsIndependentDimensions()
|
||||
{
|
||||
byte[] payload = SyntheticPayload(320, 240);
|
||||
|
||||
Assert.True(MovieCorpusDiscovery.TryReadSequenceDimensions(payload, out int width, out int height));
|
||||
Assert.Equal(320, width);
|
||||
Assert.Equal(240, height);
|
||||
Assert.False(MovieCorpusDiscovery.TryReadSequenceDimensions(new byte[] { 0, 0, 1, 0xba }, out _, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateReportsFramesMetadataTimingAndTeardown()
|
||||
{
|
||||
var source = new FakeFrameSource(new FfmpegMovieInfo(16, 16, 100, 20, 1, false),
|
||||
Frame(16, 16, 1, 0), Frame(16, 16, 2, 50));
|
||||
var input = new MovieCorpusInput(0x2bc2, "SYNTH.AGF", () =>
|
||||
new MoviePayload("SYNTH.AGF", SyntheticPayload(16, 16)));
|
||||
|
||||
MovieCorpusReport report = new MovieCorpusGate(_ => source).Run([input], 1, 1000);
|
||||
|
||||
Assert.True(report.Passed);
|
||||
var item = Assert.Single(report.Movies);
|
||||
Assert.True(item.Passed);
|
||||
Assert.Equal("0x2bc2", item.PackedIdHex);
|
||||
Assert.Equal(16, item.ExpectedWidth);
|
||||
Assert.Equal(2, item.FrameCount);
|
||||
Assert.Equal(0, item.FirstPresentationTimeMs);
|
||||
Assert.Equal(50, item.LastPresentationTimeMs);
|
||||
Assert.True(item.FramesChanged);
|
||||
Assert.True(source.Disposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateContinuesAfterValidationFailureAndReportsCountMismatch()
|
||||
{
|
||||
var badDimensions = new FakeFrameSource(new FfmpegMovieInfo(8, 16, 100, 20, 1, false),
|
||||
Frame(8, 16, 1, 0));
|
||||
var decreasingTimestamps = new FakeFrameSource(new FfmpegMovieInfo(16, 16, 100, 20, 1, false),
|
||||
Frame(16, 16, 1, 50), Frame(16, 16, 2, 40));
|
||||
var sources = new Queue<IFfmpegFrameSource>([badDimensions, decreasingTimestamps]);
|
||||
MovieCorpusInput[] inputs =
|
||||
[
|
||||
new(1, "BAD-DIMS.AGF", () => new MoviePayload("BAD-DIMS.AGF", SyntheticPayload(16, 16))),
|
||||
new(2, "BAD-PTS.AGF", () => new MoviePayload("BAD-PTS.AGF", SyntheticPayload(16, 16))),
|
||||
];
|
||||
|
||||
MovieCorpusReport report = new MovieCorpusGate(_ => sources.Dequeue()).Run(inputs, 3, 1000);
|
||||
|
||||
Assert.False(report.Passed);
|
||||
Assert.Equal(2, report.FailedCount);
|
||||
Assert.Contains("expected 3", Assert.Single(report.SelectionErrors));
|
||||
Assert.Contains("differ", report.Movies[0].Error);
|
||||
Assert.Contains("timestamp", report.Movies[1].Error);
|
||||
Assert.True(badDimensions.Disposed);
|
||||
Assert.True(decreasingTimestamps.Disposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateRejectsInvalidFrameRateMetadata()
|
||||
{
|
||||
var source = new FakeFrameSource(new FfmpegMovieInfo(16, 16, 100, 0, 1, false),
|
||||
Frame(16, 16, 1, 0));
|
||||
var input = new MovieCorpusInput(1, "BAD-RATE.AGF", () =>
|
||||
new MoviePayload("BAD-RATE.AGF", SyntheticPayload(16, 16)));
|
||||
|
||||
MovieCorpusReport report = new MovieCorpusGate(_ => source).Run([input], 1, 1000);
|
||||
|
||||
Assert.False(report.Passed);
|
||||
Assert.Contains("rate 0/1", Assert.Single(report.Movies).Error);
|
||||
Assert.True(source.Disposed);
|
||||
}
|
||||
|
||||
private static byte[] SyntheticPayload(int width, int height) =>
|
||||
[
|
||||
0, 0, 1, 0xba,
|
||||
0, 0, 1, 0xb3,
|
||||
(byte)(width >> 4),
|
||||
(byte)(((width & 0x0f) << 4) | (height >> 8)),
|
||||
(byte)height,
|
||||
0,
|
||||
];
|
||||
|
||||
private static FfmpegVideoFrame Frame(int width, int height, byte value, long timestamp)
|
||||
=> new(new RgbaImage(width, height,
|
||||
Enumerable.Repeat(value, checked(width * height * 4)).ToArray()), timestamp);
|
||||
}
|
||||
@@ -100,10 +100,11 @@ public class MovieOpcodeTests
|
||||
var factory = new FakeMovieDecoderFactory(decoder);
|
||||
var payload = new MoviePayload("TEST.AGF", new byte[] { 0, 0, 1, 0xba });
|
||||
|
||||
var runtime = MovieRuntime.Open("TEST.AGF", 7, payload, factory);
|
||||
var runtime = MovieRuntime.Open("TEST.AGF", 7, 0x123, payload, factory);
|
||||
|
||||
Assert.Same(payload, factory.OpenedPayload);
|
||||
Assert.Same(decoder, runtime.Decoder);
|
||||
Assert.Equal(0x123, runtime.ResourceId);
|
||||
Assert.Equal(1876, runtime.Decoder.StopTimeMs);
|
||||
Assert.Equal(5000, runtime.WatchdogMs);
|
||||
Assert.True(runtime.Decoder.TryTakeFrame(out var frame));
|
||||
@@ -120,7 +121,7 @@ public class MovieOpcodeTests
|
||||
public void MovieRuntimeComputesBoundedWatchdogFromDecoderMetadata(long? stopTimeMs, long expected)
|
||||
{
|
||||
var decoder = new FakeMovieDecoder { StopTimeMs = stopTimeMs };
|
||||
var runtime = MovieRuntime.Open("TEST.AGF", 7,
|
||||
var runtime = MovieRuntime.Open("TEST.AGF", 7, 0x123,
|
||||
new MoviePayload("TEST.AGF", new byte[] { 0, 0, 1, 0xba }),
|
||||
new FakeMovieDecoderFactory(decoder));
|
||||
|
||||
|
||||
65
engine/Age.Engine.Tests/MovieSurfaceRegistryTests.cs
Normal file
65
engine/Age.Engine.Tests/MovieSurfaceRegistryTests.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Age.Engine.Sys4;
|
||||
|
||||
public class MovieSurfaceRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void SameResourceOnTwoSurfacesHasIndependentFramesCompletionAndRelease()
|
||||
{
|
||||
var registry = new MovieSurfaceRegistry();
|
||||
MovieSurfaceBinding first = registry.Begin(7, 0x2b42, out _);
|
||||
MovieSurfaceBinding second = registry.Begin(8, 0x2b42, out _);
|
||||
var firstFrame = Frame(1);
|
||||
var secondFrame = Frame(2);
|
||||
|
||||
Assert.True(registry.PublishFrame(first.PlaybackId, firstFrame, "MVB126.AGF", 0x2b42));
|
||||
Assert.True(registry.PublishFrame(second.PlaybackId, secondFrame, "MVB126.AGF", 0x2b42));
|
||||
Assert.True(registry.Complete(first.PlaybackId));
|
||||
|
||||
MovieSurfaceRelease released = registry.ReleaseIfCompleted(7);
|
||||
|
||||
Assert.Equal(MovieSurfaceReleaseKind.Released, released.Kind);
|
||||
Assert.False(registry.IsBound(7));
|
||||
Assert.True(registry.IsActive(8));
|
||||
Assert.True(registry.TryResolveSurface(8, out MovieSurfaceFrame? remaining));
|
||||
Assert.Same(secondFrame, remaining!.Image);
|
||||
Assert.True(registry.Complete(second.PlaybackId));
|
||||
Assert.Equal(MovieSurfaceReleaseKind.Released, registry.ReleaseIfCompleted(8).Kind);
|
||||
Assert.False(registry.HasActivePlayback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplacingSurfaceInvalidatesLateEventsFromPriorPlayback()
|
||||
{
|
||||
var registry = new MovieSurfaceRegistry();
|
||||
MovieSurfaceBinding prior = registry.Begin(7, 0x2bdc, out _);
|
||||
|
||||
MovieSurfaceBinding current = registry.Begin(7, 0x2bad, out MovieSurfaceBinding? replaced);
|
||||
|
||||
Assert.Equal(prior, replaced);
|
||||
Assert.False(registry.Complete(prior.PlaybackId));
|
||||
Assert.False(registry.PublishFrame(prior.PlaybackId, Frame(1), "OLD.AGF", 1));
|
||||
Assert.True(registry.IsActive(7));
|
||||
Assert.True(registry.PublishFrame(current.PlaybackId, Frame(2), "NEW.AGF", 2));
|
||||
Assert.True(registry.TryResolveSurface(7, out MovieSurfaceFrame? frame));
|
||||
Assert.Equal("NEW.AGF", frame!.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReleasedMovieResourceRemainsTypedAsMovieForCleanupFrame()
|
||||
{
|
||||
var registry = new MovieSurfaceRegistry();
|
||||
MovieSurfaceBinding binding = registry.Begin(7, 0x2bde, out _);
|
||||
|
||||
Assert.True(registry.IsKnownMovieResource(0x2bde));
|
||||
Assert.True(registry.Complete(binding.PlaybackId));
|
||||
Assert.Equal(MovieSurfaceReleaseKind.Released, registry.ReleaseIfCompleted(7).Kind);
|
||||
|
||||
Assert.False(registry.IsBound(7));
|
||||
Assert.False(registry.TryResolveResource(0x2bde, out _));
|
||||
Assert.True(registry.IsKnownMovieResource(0x2bde));
|
||||
Assert.False(registry.IsKnownMovieResource(0x2af5));
|
||||
}
|
||||
|
||||
private static RgbaImage Frame(byte value)
|
||||
=> new(1, 1, new[] { value, value, value, (byte)255 });
|
||||
}
|
||||
@@ -39,6 +39,23 @@ public class Sys4AssetStoreTests
|
||||
Assert.Equal("23F0C104A45C099CEFB7D333362716EDE6F20B9EC53E4C3705A8E3A87063708E", digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompletePackedAssetEnumerationPreservesBaseThenAppendOrder()
|
||||
{
|
||||
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
||||
var append = Assert.Single(catalog.AppendPacks).Value;
|
||||
|
||||
var assets = catalog.EnumerateAssets();
|
||||
|
||||
Assert.Equal(catalog.Files.Count + append.Files.Count, assets.Count);
|
||||
Assert.Equal(0, assets[0].PackedId);
|
||||
Assert.Same(catalog.Files[0], assets[0].Asset);
|
||||
Assert.Equal(0x01000000, assets[catalog.Files.Count].PackedId);
|
||||
Assert.Same(append.Files[0], assets[catalog.Files.Count].Asset);
|
||||
Assert.Equal(catalog.EnumerateScripts(), assets.Where(entry =>
|
||||
entry.Asset.Name.EndsWith(".BIN", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteAppendDirectoryAndPayloadsMatchBinExtractAlf()
|
||||
{
|
||||
|
||||
@@ -31,6 +31,22 @@ public readonly record struct NumericGlyphStyle(int SurfaceSlot, int AtlasX, int
|
||||
public bool Registered => SurfaceSlot != 0;
|
||||
}
|
||||
|
||||
public sealed record BlockingGfxObjectDiagnostic(
|
||||
long Handle, int SourceSlot, long StartMs, long ControlFlags,
|
||||
bool ColorEnabled, long ColorDelayMs, long ColorDurationMs,
|
||||
bool ScaleEnabled, long ScaleDelayMs, long ScaleDurationMs,
|
||||
bool RotationEnabled, long RotationDelayMs, long RotationDurationMs,
|
||||
bool TranslationEnabled, long TranslationDelayMs, long TranslationDurationMs);
|
||||
|
||||
public sealed record GfxDiagnosticSnapshot(
|
||||
long NowMs, bool HasActiveTimedPresentation, int ObjectCount, int VisibleObjectCount,
|
||||
int ActiveSurfaceTransitionCount, long AnimationServiceFlags,
|
||||
long AnimClockDurationTicks, long AnimClockGeneration,
|
||||
uint PreviousFrameTimeMilliseconds, uint CurrentFrameTimeMilliseconds,
|
||||
long RangeTransformFirst, long RangeTransformCount,
|
||||
BlockingGfxObjectDiagnostic? BlockingRangeTransform,
|
||||
IReadOnlyList<BlockingGfxObjectDiagnostic> BlockingObjects);
|
||||
|
||||
/// <summary>A renderable view of one visible gfx object — the host composites these in ascending-handle order
|
||||
/// (= the engine's z-order) each frame. Built by <see cref="GfxState.SnapshotVisibleObjects"/>; the surface
|
||||
/// resId/colorkey are resolved from the object's live source slot at snapshot time (see docs/engine-re.md,
|
||||
@@ -545,6 +561,51 @@ public sealed class GfxState
|
||||
o.RotationChannelEnabled || o.TranslationEnabled));
|
||||
}
|
||||
|
||||
/// <summary>Observe-only state for a runtime stall capture. It identifies the exact finite channels
|
||||
/// which can keep the host's op-0x21c presentation wait active.</summary>
|
||||
public GfxDiagnosticSnapshot CaptureDiagnosticSnapshot(long nowMs)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
BlockingGfxObjectDiagnostic? range = HasBlockingChannels(_rangeTransform)
|
||||
? DescribeBlockingObject(-1, _rangeTransform)
|
||||
: null;
|
||||
var objects = _objects
|
||||
.Where(pair => pair.Value.Visible
|
||||
&& (pair.Value.OneShotAnimationControlFlags & 1) == 0
|
||||
&& HasBlockingChannels(pair.Value))
|
||||
.OrderBy(pair => pair.Key)
|
||||
.Select(pair => DescribeBlockingObject(pair.Key, pair.Value))
|
||||
.ToArray();
|
||||
return new GfxDiagnosticSnapshot(
|
||||
nowMs,
|
||||
_surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0)
|
||||
|| range != null || objects.Length != 0,
|
||||
_objects.Count,
|
||||
_objects.Values.Count(o => o.Visible),
|
||||
_surfaceTransitions.Values.Count(t => TransitionProgress(t, nowMs) < 1.0),
|
||||
AnimationServiceFlags,
|
||||
AnimClockDurationTicks,
|
||||
AnimClockGeneration,
|
||||
PreviousFrameTimeMilliseconds,
|
||||
CurrentFrameTimeMilliseconds,
|
||||
_rangeTransformFirst,
|
||||
_rangeTransformCount,
|
||||
range,
|
||||
objects);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasBlockingChannels(GfxObject o)
|
||||
=> o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled || o.TranslationEnabled;
|
||||
|
||||
private static BlockingGfxObjectDiagnostic DescribeBlockingObject(long handle, GfxObject o)
|
||||
=> new(handle, o.SourceSlot, o.OneShotStartMs, o.OneShotAnimationControlFlags,
|
||||
o.OneShotColorEnabled, o.ColorDelayMs, o.ColorDurationMs,
|
||||
o.ScaleEnabled, o.ScaleDelayMs, o.ScaleDurationMs,
|
||||
o.RotationChannelEnabled, o.RotationDelayMs, o.RotationDurationMs,
|
||||
o.TranslationEnabled, o.TranslationDelayMs, o.TranslationDurationMs);
|
||||
|
||||
/// <summary>Op 0x242: replace the retained object's animation-control word. Native bit 0 makes its
|
||||
/// finite one-shot channels nonblocking and immune to op 0x243 forced completion.</summary>
|
||||
public void SetOneShotAnimationControl(long handle, long flags)
|
||||
|
||||
@@ -179,23 +179,27 @@ public sealed class Sys4AssetCatalog
|
||||
.Where(f => f.Name.EndsWith(".BIN", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(f => f.Name.ToUpperInvariant()).ToArray();
|
||||
|
||||
/// <summary>Enumerate every real asset in native packed-id order, including mounted append packs.</summary>
|
||||
public IReadOnlyList<PackedAssetEntry> EnumerateAssets()
|
||||
{
|
||||
var assets = new List<PackedAssetEntry>();
|
||||
AddAssets(this, assets);
|
||||
foreach (var append in _appendPacks.OrderBy(pair => pair.Key).Select(pair => pair.Value))
|
||||
AddAssets(append, assets);
|
||||
return assets;
|
||||
}
|
||||
|
||||
/// <summary>Enumerate every script in native packed-id order, including mounted append packs.
|
||||
/// Placeholder slots and non-script assets are excluded without collapsing raw indices.</summary>
|
||||
public IReadOnlyList<PackedAssetEntry> EnumerateScripts()
|
||||
{
|
||||
var scripts = new List<PackedAssetEntry>();
|
||||
AddScripts(this, scripts);
|
||||
foreach (var append in _appendPacks.OrderBy(pair => pair.Key).Select(pair => pair.Value))
|
||||
AddScripts(append, scripts);
|
||||
return scripts;
|
||||
}
|
||||
=> EnumerateAssets().Where(entry =>
|
||||
entry.Asset.Name.EndsWith(".BIN", StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
|
||||
private static void AddScripts(Sys4AssetCatalog catalog, List<PackedAssetEntry> scripts)
|
||||
private static void AddAssets(Sys4AssetCatalog catalog, List<PackedAssetEntry> assets)
|
||||
{
|
||||
long selector = (long)catalog.PackId << 24;
|
||||
foreach (var entry in catalog.Files)
|
||||
if (entry.Name.EndsWith(".BIN", StringComparison.OrdinalIgnoreCase))
|
||||
scripts.Add(new PackedAssetEntry(selector | (uint)entry.RawIndex, entry));
|
||||
assets.Add(new PackedAssetEntry(selector | (uint)entry.RawIndex, entry));
|
||||
}
|
||||
|
||||
private static Dictionary<string, (int Start, int End)> BuildSceneRanges(IReadOnlyList<AssetEntry> files)
|
||||
|
||||
Reference in New Issue
Block a user