Apply SYS4INI logical resolution

This commit is contained in:
gamer147
2026-07-28 21:55:53 -04:00
parent 2e8265f6cc
commit c6f1a60e79
13 changed files with 501 additions and 64 deletions

View File

@@ -38,8 +38,8 @@ public static class RetainedSurfaceRasterizer
if (source == null)
{
if (item.SurfaceResId != 0 || item.Blend == BlendKind.Opaque) continue;
int width = item.W > 0 ? item.W : 800;
int height = item.H > 0 ? item.H : 600;
int width = item.W > 0 ? item.W : destination.Width;
int height = item.H > 0 ? item.H : destination.Height;
float fillOpacity = item.MultiplyTint ? opacity : opacity * tintStrength;
SoftwareAffineRasterizer.FillRgba(
destination.Pixels, destination.Width, destination.Height,

View File

@@ -40,9 +40,12 @@ public sealed class Sys4AssetCatalog
public IReadOnlyList<AssetEntry> RawSlots { get; }
public IReadOnlyList<AssetEntry> Files { get; }
public IReadOnlyDictionary<int, Sys4AssetCatalog> AppendPacks => _appendPacks;
public Sys4StartupSettings StartupSettings { get; }
public Sys4LogicalCanvas LogicalCanvas { get; }
private Sys4AssetCatalog(string magic, string title, int packId,
List<string> archives, List<AssetEntry> rawSlots)
List<string> archives, List<AssetEntry> rawSlots,
Sys4StartupSettings startupSettings)
{
Magic = magic;
Title = title;
@@ -52,6 +55,8 @@ public sealed class Sys4AssetCatalog
Files = rawSlots.Where(r => !r.IsPlaceholder).ToArray();
_byName = Files.ToDictionary(r => r.Name, StringComparer.OrdinalIgnoreCase);
_sceneRanges = BuildSceneRanges(Files);
StartupSettings = startupSettings;
LogicalCanvas = Sys4LogicalCanvas.FromSettings(startupSettings);
}
public static Sys4AssetCatalog Load(string path)
@@ -130,7 +135,10 @@ public sealed class Sys4AssetCatalog
string magic = ReadCString(data.AsSpan(0, Math.Min(8, data.Length)));
string title = ReadCString(data.AsSpan(8, Math.Min(256, data.Length - 8)));
return new Sys4AssetCatalog(magic, title, packId, archives, slots);
Sys4StartupSettings startupSettings = isBase
? Sys4StartupSettings.ParseTrailer(blob, ref p, name)
: Sys4StartupSettings.Empty;
return new Sys4AssetCatalog(magic, title, packId, archives, slots, startupSettings);
}
/// <summary>Mount an append catalog by its native header selector. A later mount of the same

View File

@@ -0,0 +1,141 @@
using System.Buffers.Binary;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
namespace Age.Engine.Sys4;
/// <summary>
/// Ordered per-game key/value pairs stored after the base SYS4INI directory and VM metadata.
/// Unknown keys are retained so newer AGE profiles remain inspectable before they have consumers.
/// </summary>
public sealed class Sys4StartupSettings
{
private readonly IReadOnlyList<KeyValuePair<string, string>> _pairs;
private readonly IReadOnlyDictionary<string, string> _values;
public static Sys4StartupSettings Empty { get; } = new([]);
public IReadOnlyList<KeyValuePair<string, string>> Pairs => _pairs;
public int Count => _pairs.Count;
private Sys4StartupSettings(List<KeyValuePair<string, string>> pairs)
{
_pairs = new ReadOnlyCollection<KeyValuePair<string, string>>(pairs);
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in pairs)
values[pair.Key] = pair.Value;
_values = new ReadOnlyDictionary<string, string>(values);
}
public bool TryGetValue(string key, out string value)
=> _values.TryGetValue(key, out value!);
public string? GetValueOrDefault(string key)
=> _values.GetValueOrDefault(key);
internal static Sys4StartupSettings ParseTrailer(byte[] blob, ref int offset, string source)
{
if (offset == blob.Length) return Empty;
uint vmMetadataBytes = ReadU32(blob, ref offset, source, "VM metadata length");
if (vmMetadataBytes > blob.Length - offset)
throw new InvalidDataException(
$"{source}: VM metadata length {vmMetadataBytes} exceeds {blob.Length - offset} remaining bytes");
offset += checked((int)vmMetadataBytes);
if (offset == blob.Length) return Empty;
uint stringBytes = ReadU32(blob, ref offset, source, "settings string length");
if (stringBytes == 0)
{
if (offset != blob.Length)
throw new InvalidDataException($"{source}: zero-length settings block has trailing data");
return Empty;
}
if (offset > blob.Length - 4)
throw new InvalidDataException($"{source}: settings pair count is truncated");
if (stringBytes > blob.Length - offset - 4)
throw new InvalidDataException(
$"{source}: settings string length {stringBytes} exceeds the remaining block");
uint pairCount = ReadU32(blob, ref offset, source, "settings pair count");
if (pairCount > stringBytes / 2)
throw new InvalidDataException(
$"{source}: settings pair count {pairCount} cannot fit in {stringBytes} string bytes");
int end = checked(offset + (int)stringBytes);
var pairs = new List<KeyValuePair<string, string>>(checked((int)pairCount));
for (int i = 0; i < pairCount; i++)
{
string key = ReadCString(blob, ref offset, end, source, $"settings key {i}");
string value = ReadCString(blob, ref offset, end, source, $"settings value {i}");
pairs.Add(new KeyValuePair<string, string>(key, value));
}
if (offset != end)
throw new InvalidDataException(
$"{source}: settings pair count consumed {offset} bytes through the blob, expected {end}");
if (end != blob.Length)
throw new InvalidDataException($"{source}: settings block has {blob.Length - end} trailing bytes");
return new Sys4StartupSettings(pairs);
}
private static uint ReadU32(byte[] blob, ref int offset, string source, string field)
{
if (offset > blob.Length - 4)
throw new InvalidDataException($"{source}: {field} is truncated");
uint value = BinaryPrimitives.ReadUInt32LittleEndian(blob.AsSpan(offset, 4));
offset += 4;
return value;
}
private static string ReadCString(
byte[] blob, ref int offset, int end, string source, string field)
{
if (offset >= end)
throw new InvalidDataException($"{source}: {field} is missing");
int zero = blob.AsSpan(offset, end - offset).IndexOf((byte)0);
if (zero < 0)
throw new InvalidDataException($"{source}: {field} is not NUL-terminated");
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
string value = Encoding.GetEncoding(932).GetString(blob, offset, zero);
offset += zero + 1;
return value;
}
}
/// <summary>The validated per-game logical canvas selected before presentation allocation.</summary>
public readonly record struct Sys4LogicalCanvas(int Width, int Height)
{
public const int DefaultWidth = 640;
public const int DefaultHeight = 480;
public const int MaximumDimension = 16_384;
public const int MaximumPixels = 67_108_864;
public int PixelCount => checked(Width * Height);
public int RgbaByteCount => checked(PixelCount * 4);
public static Sys4LogicalCanvas FromSettings(Sys4StartupSettings settings)
{
ArgumentNullException.ThrowIfNull(settings);
int width = ReadDimension(settings, "SCREENX", DefaultWidth);
int height = ReadDimension(settings, "SCREENY", DefaultHeight);
if (width > MaximumDimension || height > MaximumDimension)
throw new InvalidDataException(
$"SYS4 logical canvas {width}x{height} exceeds maximum dimension {MaximumDimension}");
long pixels = (long)width * height;
if (pixels > MaximumPixels)
throw new InvalidDataException(
$"SYS4 logical canvas {width}x{height} exceeds maximum pixel count {MaximumPixels}");
return new Sys4LogicalCanvas(width, height);
}
private static int ReadDimension(
Sys4StartupSettings settings, string key, int fallback)
{
string? raw = settings.GetValueOrDefault(key);
return int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value)
&& value > 0
? value
: fallback;
}
}