Match native asset fallback behavior
This commit is contained in:
@@ -214,6 +214,24 @@ graphics, voice/SFX, and movie bytes. Resolution and opening remain separate onl
|
|||||||
packed id selects one record, then the store applies loose-first/archive-second opening. There is no
|
packed id selects one record, then the store applies loose-first/archive-second opening. There is no
|
||||||
scene-local numeric addressing mode.
|
scene-local numeric addressing mode.
|
||||||
|
|
||||||
|
**Native/port fallback fidelity recheck and correction (2026-08-15).** A fresh `/v2` decompile and disassembly
|
||||||
|
comparison confirmed the normal installed-asset contract above, including base/append packed selection, exact loose
|
||||||
|
basename, archive offset/size, and successful-open catalog tracking. `Sys4AssetStore` now also matches the two
|
||||||
|
previously divergent open details: any ordinary loose-file open failure (`IOException` or
|
||||||
|
`UnauthorizedAccessException`) continues through later override roots and then the archive, and both loose and
|
||||||
|
archive handles request `FileOptions.SequentialScan`. A loose file that opens but fails later decoding remains
|
||||||
|
authoritative in both implementations; decode failure never reveals the archive copy.
|
||||||
|
|
||||||
|
Native still throws for a base id outside the table, a missing signed append selector, an append id outside its
|
||||||
|
table, or a selected entry that cannot be opened loose or archived. The typed port intentionally retains its safer
|
||||||
|
`null` result for invalid, unmounted, placeholder, or wrong-type packed records. `ResourceMap` now emits a
|
||||||
|
deduplicated diagnostic for each `(resource kind, packed id, failure)` through an optional host callback; Godot sends
|
||||||
|
these to `GD.PushWarning`, and the CLI audio/gfx diagnostics send them to standard error. Thus malformed scripts or
|
||||||
|
mods remain nonfatal without becoming silent.
|
||||||
|
|
||||||
|
The corresponding Ghidra helper is named `asset_register_open_handle_range@0x44edf0`; it stores the selected
|
||||||
|
handle, current position, and readable length in the FileDB stream table.
|
||||||
|
|
||||||
### Proposed layers
|
### Proposed layers
|
||||||
|
|
||||||
1. **Catalog + read-only ALF store (VFS-A DONE).** `Sys4AssetCatalog` parses SYS4INI at runtime while preserving all 13208 raw records
|
1. **Catalog + read-only ALF store (VFS-A DONE).** `Sys4AssetCatalog` parses SYS4INI at runtime while preserving all 13208 raw records
|
||||||
|
|||||||
@@ -307,11 +307,19 @@ the dispatch table (op `0x03` → `ctx[0x26c93+3]` = **`FUN_0041bc90`**), then t
|
|||||||
**SYS4INI 80-byte layout** `{name[64], arc_id@0x40, file_number@0x44, offset@0x48, size@0x4c}`
|
**SYS4INI 80-byte layout** `{name[64], arc_id@0x40, file_number@0x44, offset@0x48, size@0x4c}`
|
||||||
(count = `[FileDB+0x40c]`, archive-name table = `[FileDB+0x410]`; absolute EngineCtx fields
|
(count = `[FileDB+0x40c]`, archive-name table = `[FileDB+0x410]`; absolute EngineCtx fields
|
||||||
`+0x9c658/+0x9c65c/+0x9c660`). It tries a **loose override first**
|
`+0x9c658/+0x9c65c/+0x9c660`). It tries a **loose override first**
|
||||||
(`CreateFileA` on `record.name` → the mod/patch hook point), else opens archive
|
(`CreateFileA` on `record.name` → the mod/patch hook point). Any failed loose `CreateFileA` result—not only
|
||||||
|
file-not-found—takes the archive branch; a successfully opened but malformed loose payload remains authoritative.
|
||||||
|
Both loose and archive opens use read access, read sharing, `OPEN_EXISTING`, and the sequential-scan hint. The
|
||||||
|
archive branch opens
|
||||||
`[record.arc_id*0x100 + FileDB+0x410]`, `SetFilePointer` to `record.offset`, size = `record.size`.
|
`[record.arc_id*0x100 + FileDB+0x410]`, `SetFilePointer` to `record.offset`, size = `record.size`.
|
||||||
High-byte-tagged ids select `[FileDB+0x3028 + signed_selector*4]` and use the low 24 bits as the
|
High-byte-tagged ids select `[FileDB+0x3028 + signed_selector*4]` and use the low 24 bits as the
|
||||||
selected AAI record. The base corpus has no explicit high-byte call-script operand; INIT2 op `0x143`
|
selected AAI record. The base corpus has no explicit high-byte call-script operand; INIT2 op `0x143`
|
||||||
supplies mounted record-zero ids dynamically.
|
supplies mounted record-zero ids dynamically.
|
||||||
|
- **`asset_register_open_handle_range@0x44edf0`** registers the selected handle in FileDB's 256-slot stream
|
||||||
|
table, retaining its current file position and the loose-file length or catalog-record length supplied by the
|
||||||
|
opener. A full table closes the new handle and throws. Base out-of-range ids, missing signed append selectors,
|
||||||
|
append out-of-range ids, and entries for which neither loose nor archive open succeeds likewise throw rather
|
||||||
|
than returning an unresolved sentinel.
|
||||||
|
|
||||||
**So `call-script <id>` = a direct RAW index into the SYS4INI global file table** — the same table
|
**So `call-script <id>` = a direct RAW index into the SYS4INI global file table** — the same table
|
||||||
`parse_sys4ini.py` reads, but indexed *without* skipping `@` placeholders (13208 records, 2
|
`parse_sys4ini.py` reads, but indexed *without* skipping `@` placeholders (13208 records, 2
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ if (args[0] == "audio")
|
|||||||
// each resolved via ResourceMap (same rule as the Godot host). Diagnostic only.
|
// each resolved via ResourceMap (same rule as the Godot host). Diagnostic only.
|
||||||
var sceneName = args[1];
|
var sceneName = args[1];
|
||||||
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
|
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
|
||||||
var res = ResourceMap.Load();
|
var res = ResourceMap.Load(Console.Error.WriteLine);
|
||||||
var host = new AudioTraceHost(res);
|
var host = new AudioTraceHost(res);
|
||||||
var vm = new VirtualMachine(ScriptByName(sceneName), table, host);
|
var vm = new VirtualMachine(ScriptByName(sceneName), table, host);
|
||||||
// optional: seed globals, e.g. `audio SC0000.BIN 0xa57=1` to set Lily's form-A flag
|
// optional: seed globals, e.g. `audio SC0000.BIN 0xa57=1` to set Lily's form-A flag
|
||||||
@@ -69,7 +69,7 @@ if (args[0] == "gfx")
|
|||||||
bool boot = args.Contains("--boot");
|
bool boot = args.Contains("--boot");
|
||||||
var sceneName = args.First(a => a.EndsWith(".BIN", StringComparison.OrdinalIgnoreCase));
|
var sceneName = args.First(a => a.EndsWith(".BIN", StringComparison.OrdinalIgnoreCase));
|
||||||
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
|
var sceneKey = Path.GetFileNameWithoutExtension(sceneName).ToUpperInvariant();
|
||||||
var res = ResourceMap.Load();
|
var res = ResourceMap.Load(Console.Error.WriteLine);
|
||||||
var host = new GfxTraceHost(res);
|
var host = new GfxTraceHost(res);
|
||||||
var session = new GameSession();
|
var session = new GameSession();
|
||||||
foreach (var s in args.Where(a => a.Contains('=')))
|
foreach (var s in args.Where(a => a.Contains('=')))
|
||||||
|
|||||||
@@ -307,6 +307,10 @@ public class Sys4AssetStoreTests
|
|||||||
File.Delete(Path.Combine(loose, "TEST.BIN"));
|
File.Delete(Path.Combine(loose, "TEST.BIN"));
|
||||||
Assert.Equal(new byte[] { 1, 2, 3 }, store.ReadAll(entry));
|
Assert.Equal(new byte[] { 1, 2, 3 }, store.ReadAll(entry));
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.Combine(loose, "TEST.BIN"));
|
||||||
|
Assert.Equal(new byte[] { 1, 2, 3 }, store.ReadAll(entry));
|
||||||
|
Directory.Delete(Path.Combine(loose, "TEST.BIN"));
|
||||||
|
|
||||||
var reads = await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => Task.Run(() => store.ReadAll(entry))));
|
var reads = await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => Task.Run(() => store.ReadAll(entry))));
|
||||||
Assert.All(reads, bytes => Assert.Equal(new byte[] { 1, 2, 3 }, bytes));
|
Assert.All(reads, bytes => Assert.Equal(new byte[] { 1, 2, 3 }, bytes));
|
||||||
Assert.Throws<InvalidDataException>(() => store.Open(entry with { Name = "../TEST.BIN" }));
|
Assert.Throws<InvalidDataException>(() => store.Open(entry with { Name = "../TEST.BIN" }));
|
||||||
@@ -319,6 +323,28 @@ public class Sys4AssetStoreTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TypedResolutionReportsInvalidAndMismatchedPackedIdsOnce()
|
||||||
|
{
|
||||||
|
var catalog = Sys4AssetCatalog.Parse(Sys4StartupSettingsTests.BuildCatalog(
|
||||||
|
includeTrailer: true, fileName: "TEST.BIN"));
|
||||||
|
var diagnostics = new List<string>();
|
||||||
|
var resources = new ResourceMap(catalog, diagnostic: diagnostics.Add);
|
||||||
|
|
||||||
|
Assert.Null(resources.ResolveTexture(0x02000000));
|
||||||
|
Assert.Null(resources.ResolveTexture(0x02000000));
|
||||||
|
Assert.Null(resources.ResolveTexture(0));
|
||||||
|
Assert.Null(resources.ResolveTexture(0));
|
||||||
|
|
||||||
|
Assert.Collection(diagnostics,
|
||||||
|
message => Assert.Equal(
|
||||||
|
"[asset-resolution] texture resource 0x2000000 does not select a mounted SYS4INI/AAI record",
|
||||||
|
message),
|
||||||
|
message => Assert.Equal(
|
||||||
|
"[asset-resolution] texture resource 0x0 selects TEST.BIN, expected an AGF record",
|
||||||
|
message));
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] ReadToEnd(Stream stream)
|
private static byte[] ReadToEnd(Stream stream)
|
||||||
{
|
{
|
||||||
using var copy = new MemoryStream();
|
using var copy = new MemoryStream();
|
||||||
|
|||||||
@@ -101,11 +101,11 @@ public class Sys4StartupSettingsTests
|
|||||||
params (string Key, string Value)[] pairs)
|
params (string Key, string Value)[] pairs)
|
||||||
=> WrapCatalog(BuildExpanded(pairs));
|
=> WrapCatalog(BuildExpanded(pairs));
|
||||||
|
|
||||||
internal static byte[] BuildCatalog(bool includeTrailer)
|
internal static byte[] BuildCatalog(bool includeTrailer, string fileName = "@")
|
||||||
=> WrapCatalog(BuildExpanded(Array.Empty<(string, string)>(), includeTrailer));
|
=> WrapCatalog(BuildExpanded(Array.Empty<(string, string)>(), includeTrailer, fileName));
|
||||||
|
|
||||||
private static byte[] BuildExpanded(
|
private static byte[] BuildExpanded(
|
||||||
(string Key, string Value)[] pairs, bool includeTrailer = true)
|
(string Key, string Value)[] pairs, bool includeTrailer = true, string fileName = "@")
|
||||||
{
|
{
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
Encoding cp932 = Encoding.GetEncoding(932);
|
Encoding cp932 = Encoding.GetEncoding(932);
|
||||||
@@ -113,7 +113,7 @@ public class Sys4StartupSettingsTests
|
|||||||
AddU32(blob, 1);
|
AddU32(blob, 1);
|
||||||
AddFixedString(blob, "DATA.ALF", 256, cp932);
|
AddFixedString(blob, "DATA.ALF", 256, cp932);
|
||||||
AddU32(blob, 1);
|
AddU32(blob, 1);
|
||||||
AddFixedString(blob, "@", 64, cp932);
|
AddFixedString(blob, fileName, 64, cp932);
|
||||||
AddU32(blob, 0);
|
AddU32(blob, 0);
|
||||||
AddU32(blob, 0);
|
AddU32(blob, 0);
|
||||||
AddU32(blob, 0);
|
AddU32(blob, 0);
|
||||||
|
|||||||
@@ -11,49 +11,43 @@ public sealed class ResourceMap
|
|||||||
{
|
{
|
||||||
private readonly Sys4AssetCatalog _catalog;
|
private readonly Sys4AssetCatalog _catalog;
|
||||||
private readonly IAssetStore _store;
|
private readonly IAssetStore _store;
|
||||||
|
private readonly Action<string>? _diagnostic;
|
||||||
|
private readonly HashSet<string> _reportedDiagnostics = new(StringComparer.Ordinal);
|
||||||
|
private readonly object _diagnosticLock = new();
|
||||||
|
|
||||||
public ResourceMap(Sys4AssetCatalog catalog, IAssetStore? store = null)
|
public ResourceMap(Sys4AssetCatalog catalog, IAssetStore? store = null,
|
||||||
|
Action<string>? diagnostic = null)
|
||||||
{
|
{
|
||||||
_catalog = catalog;
|
_catalog = catalog;
|
||||||
_store = store ?? new Sys4AssetStore(catalog, Paths.GameDir, Paths.GameDir);
|
_store = store ?? new Sys4AssetStore(catalog, Paths.GameDir, Paths.GameDir);
|
||||||
|
_diagnostic = diagnostic;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ResourceMap Load() => new(Sys4AssetCatalog.Load(Paths.Sys4Ini));
|
public static ResourceMap Load(Action<string>? diagnostic = null)
|
||||||
|
=> new(Sys4AssetCatalog.Load(Paths.Sys4Ini), diagnostic: diagnostic);
|
||||||
|
|
||||||
/// <summary>Resolve a universal packed SYS4INI/AAI id to an AGF texture record.</summary>
|
/// <summary>Resolve a universal packed SYS4INI/AAI id to an AGF texture record.</summary>
|
||||||
public AssetEntry? ResolveTexture(long resourceId)
|
public AssetEntry? ResolveTexture(long resourceId)
|
||||||
{
|
=> ResolveTypedPacked(resourceId, "texture", "an AGF record",
|
||||||
var entry = _catalog.ResolvePacked(resourceId);
|
entry => entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase));
|
||||||
return entry is { IsPlaceholder: false } &&
|
|
||||||
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Resolve a universal packed SYS4INI/AAI id to a voice audio record.</summary>
|
/// <summary>Resolve a universal packed SYS4INI/AAI id to a voice audio record.</summary>
|
||||||
public AssetEntry? ResolveVoice(long resourceId)
|
public AssetEntry? ResolveVoice(long resourceId)
|
||||||
{
|
=> ResolveTypedPacked(resourceId, "voice", "an OGG/WAV record", IsAudio);
|
||||||
var entry = _catalog.ResolvePacked(resourceId);
|
|
||||||
return entry is { IsPlaceholder: false } && IsAudio(entry) ? entry : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Resolve a universal packed SYS4INI/AAI id to an AGF-named movie record. ReadMovie
|
/// <summary>Resolve a universal packed SYS4INI/AAI id to an AGF-named movie record. ReadMovie
|
||||||
/// validates the MPEG signature because still images use the same extension.</summary>
|
/// validates the MPEG signature because still images use the same extension.</summary>
|
||||||
public AssetEntry? ResolveMovie(long resourceId)
|
public AssetEntry? ResolveMovie(long resourceId)
|
||||||
{
|
=> ResolveTypedPacked(resourceId, "movie", "an AGF record",
|
||||||
var entry = _catalog.ResolvePacked(resourceId);
|
entry => entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase));
|
||||||
return entry is { IsPlaceholder: false } &&
|
|
||||||
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Decode an AGF directly from loose-first VFS bytes.</summary>
|
/// <summary>Decode an AGF directly from loose-first VFS bytes.</summary>
|
||||||
public RgbaImage DecodeTexture(AssetEntry entry) => AgfDecoder.Decode(_store, entry);
|
public RgbaImage DecodeTexture(AssetEntry entry) => AgfDecoder.Decode(_store, entry);
|
||||||
|
|
||||||
/// <summary>Resolve a native packed raw id to one of AGE's Windows cursor resources.</summary>
|
/// <summary>Resolve a native packed raw id to one of AGE's Windows cursor resources.</summary>
|
||||||
public AssetEntry? ResolveCursor(long resourceId)
|
public AssetEntry? ResolveCursor(long resourceId)
|
||||||
{
|
=> ResolveTypedPacked(resourceId, "cursor", "a CUR record",
|
||||||
var entry = _catalog.ResolvePacked(resourceId);
|
entry => entry.Name.EndsWith(".CUR", StringComparison.OrdinalIgnoreCase));
|
||||||
return entry is { IsPlaceholder: false }
|
|
||||||
&& entry.Name.EndsWith(".CUR", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CursorImage DecodeCursor(AssetEntry entry)
|
public CursorImage DecodeCursor(AssetEntry entry)
|
||||||
{
|
{
|
||||||
@@ -79,10 +73,7 @@ public sealed class ResourceMap
|
|||||||
|
|
||||||
/// <summary>Resolve opcode 0xb4's universal packed SYS4INI/AAI id to an audio entry.</summary>
|
/// <summary>Resolve opcode 0xb4's universal packed SYS4INI/AAI id to an audio entry.</summary>
|
||||||
public AssetEntry? ResolveSoundEffect(long packedRawId)
|
public AssetEntry? ResolveSoundEffect(long packedRawId)
|
||||||
{
|
=> ResolveTypedPacked(packedRawId, "sound effect", "an OGG/WAV record", IsAudio);
|
||||||
var entry = _catalog.ResolvePacked(packedRawId);
|
|
||||||
return entry is { IsPlaceholder: false } && IsAudio(entry) ? entry : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Read a catalog-resolved OGG/WAV payload through the loose-first ALF/AAI byte store.</summary>
|
/// <summary>Read a catalog-resolved OGG/WAV payload through the loose-first ALF/AAI byte store.</summary>
|
||||||
public AudioPayload ReadAudio(AssetEntry entry)
|
public AudioPayload ReadAudio(AssetEntry entry)
|
||||||
@@ -108,6 +99,43 @@ public sealed class ResourceMap
|
|||||||
private static bool IsAudio(AssetEntry entry)
|
private static bool IsAudio(AssetEntry entry)
|
||||||
=> entry.Name.EndsWith(".OGG", StringComparison.OrdinalIgnoreCase)
|
=> entry.Name.EndsWith(".OGG", StringComparison.OrdinalIgnoreCase)
|
||||||
|| entry.Name.EndsWith(".WAV", StringComparison.OrdinalIgnoreCase);
|
|| entry.Name.EndsWith(".WAV", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private AssetEntry? ResolveTypedPacked(long resourceId, string kind, string expected,
|
||||||
|
Func<AssetEntry, bool> matches)
|
||||||
|
{
|
||||||
|
AssetEntry? entry = _catalog.ResolvePacked(resourceId);
|
||||||
|
if (entry == null)
|
||||||
|
{
|
||||||
|
ReportOnce($"[asset-resolution] {kind} resource {FormatResourceId(resourceId)} " +
|
||||||
|
"does not select a mounted SYS4INI/AAI record");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (entry.IsPlaceholder)
|
||||||
|
{
|
||||||
|
ReportOnce($"[asset-resolution] {kind} resource {FormatResourceId(resourceId)} " +
|
||||||
|
"selects a placeholder catalog record");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!matches(entry))
|
||||||
|
{
|
||||||
|
ReportOnce($"[asset-resolution] {kind} resource {FormatResourceId(resourceId)} " +
|
||||||
|
$"selects {entry.Name}, expected {expected}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReportOnce(string message)
|
||||||
|
{
|
||||||
|
if (_diagnostic == null) return;
|
||||||
|
bool report;
|
||||||
|
lock (_diagnosticLock)
|
||||||
|
report = _reportedDiagnostics.Add(message);
|
||||||
|
if (report) _diagnostic(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatResourceId(long resourceId)
|
||||||
|
=> resourceId < 0 ? resourceId.ToString() : $"0x{resourceId:x}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record AudioPayload(string Name, byte[] Bytes);
|
public sealed record AudioPayload(string Name, byte[] Bytes);
|
||||||
|
|||||||
@@ -64,10 +64,12 @@ public sealed class Sys4AssetStore : IAssetStore
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
return new FileStream(candidate, FileMode.Open, FileAccess.Read, FileShare.Read,
|
return new FileStream(candidate, FileMode.Open, FileAccess.Read, FileShare.Read,
|
||||||
64 * 1024, FileOptions.RandomAccess);
|
64 * 1024, FileOptions.SequentialScan);
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException) { }
|
// Native falls through on any INVALID_HANDLE_VALUE result from the loose CreateFileA.
|
||||||
catch (DirectoryNotFoundException) { }
|
// Keep malformed payloads authoritative once open; only open failures reach the archive.
|
||||||
|
catch (IOException) { }
|
||||||
|
catch (UnauthorizedAccessException) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateBasename(entry.Archive, "archive");
|
ValidateBasename(entry.Archive, "archive");
|
||||||
@@ -75,7 +77,7 @@ public sealed class Sys4AssetStore : IAssetStore
|
|||||||
if (!IsDirectChild(_archiveRoot, archivePath))
|
if (!IsDirectChild(_archiveRoot, archivePath))
|
||||||
throw new InvalidDataException($"unsafe archive name: {entry.Archive}");
|
throw new InvalidDataException($"unsafe archive name: {entry.Archive}");
|
||||||
var file = new FileStream(archivePath, FileMode.Open, FileAccess.Read, FileShare.Read,
|
var file = new FileStream(archivePath, FileMode.Open, FileAccess.Read, FileShare.Read,
|
||||||
64 * 1024, FileOptions.RandomAccess);
|
64 * 1024, FileOptions.SequentialScan);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (entry.Offset < 0 || entry.Size < 0 || entry.Offset > file.Length
|
if (entry.Offset < 0 || entry.Size < 0 || entry.Offset > file.Length
|
||||||
|
|||||||
@@ -301,8 +301,8 @@ public partial class Main : Godot.Control
|
|||||||
_locator = new PageLocatorState(scene, _selftest ? null : pageMapPath);
|
_locator = new PageLocatorState(scene, _selftest ? null : pageMapPath);
|
||||||
_locatorHud.Visible = _locatorHudVisible;
|
_locatorHud.Visible = _locatorHudVisible;
|
||||||
var resources = scripts != null
|
var resources = scripts != null
|
||||||
? new ResourceMap(scripts.Catalog, trackedAssetStore)
|
? new ResourceMap(scripts.Catalog, trackedAssetStore, GD.PushWarning)
|
||||||
: new ResourceMap(catalog, _assetStore);
|
: new ResourceMap(catalog, _assetStore, GD.PushWarning);
|
||||||
IGlyphMaskRasterizer? surfaceTextRasterizer = null;
|
IGlyphMaskRasterizer? surfaceTextRasterizer = null;
|
||||||
PortableTextRenderingPolicy? portableTextPolicy = null;
|
PortableTextRenderingPolicy? portableTextPolicy = null;
|
||||||
string? exactUnavailable = null;
|
string? exactUnavailable = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user