Land SYSTEM4-rooted Godot boot
This commit is contained in:
@@ -9,16 +9,19 @@ public class CallScriptTests
|
||||
{
|
||||
// Opcodes (from build/opcodes.json): exit=0x2, call-script=0x3(argc1), mov=0x55(argc2).
|
||||
// Operand types: imm=0, global-int=3, local-int=9.
|
||||
private const uint OP_EXIT = 0x2, OP_CALLSCRIPT = 0x3, OP_MOV = 0x55;
|
||||
private const uint OP_EXIT = 0x2, OP_CALLSCRIPT = 0x3, OP_MOV = 0x55, OP_SETTEXTURE = 0x1f9;
|
||||
|
||||
private sealed class NullHost : IHost
|
||||
private class NullHost : IHost
|
||||
{
|
||||
public virtual void EnterScriptContext(string scriptName) { }
|
||||
public virtual void ExitScriptContext() { }
|
||||
public virtual long ResolveTextureResourceId(long resourceId) => resourceId;
|
||||
public void ShowText(int o, string t) { }
|
||||
public void WaitForInput() { }
|
||||
public void Sleep(long duration) { }
|
||||
public void FrameYield() { }
|
||||
public void CreateTexture(int s, int w, int h) { }
|
||||
public void SetTexture(long r, int s) { }
|
||||
public virtual void SetTexture(long r, int s) { }
|
||||
public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||
public (int Width, int Height) GetTextureSize(int s) => (0, 0);
|
||||
public void PlayBgm(long id) { }
|
||||
@@ -32,6 +35,29 @@ public class CallScriptTests
|
||||
public Script? GetById(long id) => _m.TryGetValue(id, out var s) ? s : null;
|
||||
}
|
||||
|
||||
private sealed class ContextHost : NullHost
|
||||
{
|
||||
private readonly Stack<string> _contexts = new();
|
||||
public List<string> Events { get; } = new();
|
||||
public List<(long ResourceId, int Slot)> Textures { get; } = new();
|
||||
|
||||
public override void EnterScriptContext(string scriptName)
|
||||
{
|
||||
_contexts.Push(scriptName);
|
||||
Events.Add($"enter:{scriptName}");
|
||||
}
|
||||
|
||||
public override void ExitScriptContext()
|
||||
{
|
||||
Events.Add($"exit:{_contexts.Pop()}");
|
||||
}
|
||||
|
||||
public override long ResolveTextureResourceId(long resourceId)
|
||||
=> resourceId + (_contexts.Peek() == "CALLEE" ? 700 : 70);
|
||||
|
||||
public override void SetTexture(long resourceId, int slot) => Textures.Add((resourceId, slot));
|
||||
}
|
||||
|
||||
// Build a Script from raw dwords via the real loader (guarantees identical decode).
|
||||
private static Script Asm(OpcodeTable t, string name, params uint[] body)
|
||||
{
|
||||
@@ -105,4 +131,25 @@ public class CallScriptTests
|
||||
vm.Run();
|
||||
Assert.Equal(1, vm.Globals[0x20]); // caller's local 0 unchanged by callee's local 0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptLocalTextureIdsFollowTheActiveNestedFrame()
|
||||
{
|
||||
var t = Table();
|
||||
var callee = Asm(t, "CALLEE",
|
||||
OP_SETTEXTURE, 0, 7, 0, 2, 0, uint.MaxValue,
|
||||
OP_EXIT);
|
||||
var caller = Asm(t, "CALLER",
|
||||
OP_SETTEXTURE, 0, 7, 0, 1, 0, uint.MaxValue,
|
||||
OP_CALLSCRIPT, 0, 5,
|
||||
OP_SETTEXTURE, 0, 7, 0, 3, 0, uint.MaxValue,
|
||||
OP_EXIT);
|
||||
var host = new ContextHost();
|
||||
var vm = new VirtualMachine(caller, t, host, null, new MapProvider(new() { [5] = callee }));
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(new[] { (77L, 1), (707L, 2), (77L, 3) }, host.Textures);
|
||||
Assert.Equal(new[] { "enter:CALLER", "enter:CALLEE", "exit:CALLEE", "exit:CALLER" }, host.Events);
|
||||
}
|
||||
}
|
||||
|
||||
111
engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs
Normal file
111
engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using Age.Engine.Diagnostics;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
using Xunit;
|
||||
|
||||
public class NaturalBootIntegrationTests
|
||||
{
|
||||
private sealed class ReachedSc0000Exception : Exception { }
|
||||
|
||||
private sealed class StopAtSc0000Sink : ITraceSink
|
||||
{
|
||||
public readonly List<string> Entered = new();
|
||||
public Action<string>? OnEnter;
|
||||
public bool TracingSteps => false;
|
||||
|
||||
public void Emit(in TraceEvent e)
|
||||
{
|
||||
if (e.Kind == TraceEventKind.FrameEnter && e.Name != null)
|
||||
{
|
||||
Entered.Add(e.Name);
|
||||
OnEnter?.Invoke(e.Name);
|
||||
if (e.Name.Equals("SC0000.BIN", StringComparison.OrdinalIgnoreCase))
|
||||
throw new ReachedSc0000Exception();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NewGameInputHost : RecordingHost
|
||||
{
|
||||
public VirtualMachine Vm = null!;
|
||||
private long _now;
|
||||
public int TitlePollSleeps;
|
||||
private bool _inGameStart;
|
||||
private int _gameStartPollSleeps;
|
||||
public override long InputClockMilliseconds => _now;
|
||||
|
||||
public void BeginGameStart()
|
||||
{
|
||||
_inGameStart = true;
|
||||
_gameStartPollSleeps = 0;
|
||||
Vm.UpdateMouseButtonState(0x1, false);
|
||||
Vm.UpdateInputCallbackState(4, false);
|
||||
Vm.QueueInputCallback(10);
|
||||
}
|
||||
|
||||
public override void Sleep(long duration)
|
||||
{
|
||||
base.Sleep(duration);
|
||||
_now += Math.Max(1, duration);
|
||||
if (duration > 1) return;
|
||||
|
||||
// TITLE's first menu entry is Game Start. Hold the pointer over its native 800x600
|
||||
// rectangle, then provide one complete primary-button edge to its raw input callback.
|
||||
int polls = _inGameStart ? ++_gameStartPollSleeps : ++TitlePollSleeps;
|
||||
int cycle = polls % 200;
|
||||
if (_inGameStart && polls <= 200) return;
|
||||
if (cycle == 1)
|
||||
{
|
||||
int input = _inGameStart && polls <= 400 ? 0 : 4;
|
||||
if (!_inGameStart)
|
||||
{
|
||||
Vm.UpdatePointer(400, 300);
|
||||
Vm.UpdateMouseButtonState(0x1, true);
|
||||
}
|
||||
Vm.UpdateInputCallbackState(input, true);
|
||||
}
|
||||
// TITLE polls its registered mouse callback every 50 ms and activates on the release edge.
|
||||
// Keep the button down through one callback, then release it before the next.
|
||||
else if (cycle == 120)
|
||||
{
|
||||
int input = _inGameStart && polls <= 400 ? 0 : 4;
|
||||
if (!_inGameStart) Vm.UpdateMouseButtonState(0x1, false);
|
||||
Vm.UpdateInputCallbackState(input, false);
|
||||
Vm.QueueInputCallback(10); // native/main-thread release callback unlocks the menu input gate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void System4Root_NewGameSelectionNaturallyCallsSc0000()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var scripts = Sys4ScriptProvider.Load(table);
|
||||
var host = new NewGameInputHost();
|
||||
var sink = new StopAtSc0000Sink();
|
||||
var vm = new VirtualMachine(scripts.RequireByName("SYSTEM4.BIN"), table, host,
|
||||
new VmOptions(MaxSteps: 5_000_000), scripts, sink);
|
||||
host.Vm = vm;
|
||||
sink.OnEnter = name =>
|
||||
{
|
||||
if (name.Equals("GAMESTART.BIN", StringComparison.OrdinalIgnoreCase)) host.BeginGameStart();
|
||||
};
|
||||
|
||||
var exception = Record.Exception(() => vm.Run());
|
||||
Assert.True(exception is ReachedSc0000Exception,
|
||||
$"halt={vm.HaltReason}; title_sleeps={host.TitlePollSleeps}; entered={string.Join(",", sink.Entered)}");
|
||||
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"SYSTEM4.BIN", "INITCONFIG.BIN", "INIT2.BIN",
|
||||
}, sink.Entered.Take(3));
|
||||
Assert.Contains("TITLE.BIN", sink.Entered);
|
||||
Assert.Contains("GAMESTART.BIN", sink.Entered);
|
||||
Assert.Contains("UNITECH.BIN", sink.Entered);
|
||||
Assert.Contains("CALCARR.BIN", sink.Entered);
|
||||
Assert.Equal("SC0000.BIN", sink.Entered[^1]);
|
||||
Assert.Equal(1, vm.Globals.GetValueOrDefault(0));
|
||||
Assert.Equal(0x22, vm.Globals.GetValueOrDefault(0x699));
|
||||
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x6c1));
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ public class RenderObjectBlendTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mode1_UsesArgbAlphaAsOpacityAndRgbAsMultiplicativeModulation()
|
||||
public void Mode1_UsesAdditiveBlendWithArgbSourceScaleAndRgbModulation()
|
||||
{
|
||||
var g = WithVisibleObject(0x100, resId: 5, colorKey: -1);
|
||||
g.SetStaticObjectColorResolved(0x100, 1, 0x40, 0x80ff40);
|
||||
@@ -103,7 +103,7 @@ public class RenderObjectBlendTests
|
||||
Assert.Equal(0, ro.TintStrength);
|
||||
Assert.Equal(0x80ff40, ro.Tint);
|
||||
Assert.True(ro.MultiplyTint);
|
||||
Assert.Equal(BlendKind.Alpha, ro.Blend);
|
||||
Assert.Equal(BlendKind.Additive, ro.Blend);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -99,6 +99,22 @@ public class SoftwareAffineRasterizerTests
|
||||
Assert.Equal(new byte[4], hidden);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlitRgba_AdditiveMakesBlackTransparentAndAddsScaledColor()
|
||||
{
|
||||
var identity = new Affine2D(1, 0, 0, 1, 0, 0);
|
||||
byte[] background = { 40, 50, 60, 255 };
|
||||
byte[] black = { 0, 0, 0, 255 };
|
||||
SoftwareAffineRasterizer.BlitRgba(background, 1, 1, black, 1, 1, 0, 0, 1, 1,
|
||||
identity, 0xffffff, 0, 1, multiplyTint: true, blend: BlendKind.Additive);
|
||||
Assert.Equal(new byte[] { 40, 50, 60, 255 }, background);
|
||||
|
||||
byte[] blueGlow = { 16, 32, 200, 128 };
|
||||
SoftwareAffineRasterizer.BlitRgba(background, 1, 1, blueGlow, 1, 1, 0, 0, 1, 1,
|
||||
identity, 0xffffff, 0, 0.5f, multiplyTint: true, blend: BlendKind.Additive);
|
||||
Assert.Equal(new byte[] { 43, 57, 109, 255 }, background);
|
||||
}
|
||||
|
||||
// Pre-fast-path affine algorithm retained here as an independent differential oracle.
|
||||
private static void ReferenceBlit(byte[] dst, int dstW, int dstH, byte[] src, int srcW,
|
||||
int srcX, int srcY, int width, int height, Affine2D transform,
|
||||
|
||||
@@ -15,6 +15,11 @@ public readonly record struct SurfaceRectFill(
|
||||
|
||||
public interface IHost
|
||||
{
|
||||
// Script-local resource ids resolve against the currently executing frame's SYS4INI section.
|
||||
// Interactive hosts track this stack; headless hosts may keep the no-op/default identity behavior.
|
||||
void EnterScriptContext(string scriptName) { }
|
||||
void ExitScriptContext() { }
|
||||
long ResolveTextureResourceId(long resourceId) => resourceId;
|
||||
void ShowText(int offset, string text);
|
||||
// Native ADV text subsystem: op 0x7a updates the selected layout's last 20-byte cursor record;
|
||||
// op 0x204 rasterizes a string into a numbered surface before 0x1fb binds that surface.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace Age.Engine.Model;
|
||||
|
||||
/// <summary>How an object's surface composites onto the canvas. Opaque = straight copy; Alpha = source-alpha
|
||||
/// blend (fades). Additive (glow/flash, native blit mode 2/3) is a documented seam — NOT implemented in the
|
||||
/// blend/transparency slice; see docs/superpowers/specs/2026-07-08-blend-transparency-design.md.</summary>
|
||||
/// blend (fades); Additive = native SRCALPHA/ONE glow composition.</summary>
|
||||
public enum BlendKind { Opaque, Alpha, Additive }
|
||||
|
||||
@@ -665,10 +665,15 @@ public sealed class GfxState
|
||||
{
|
||||
var (a, r, g, b) = BlendMath.UnpackArgb(sampledColor);
|
||||
tint = ((long)r << 16) | ((long)g << 8) | (long)b;
|
||||
if (o.StaticColorMode == 1 || (o.StaticColorMode == 0 && o.OneShotColorBlend))
|
||||
if (o.StaticColorMode == 1)
|
||||
{
|
||||
// Native mode 1 enables SRCALPHA/ONE additive blending and passes packed ARGB as
|
||||
// D3D modulation. Black therefore contributes nothing (TITLE's SO022 flames),
|
||||
// while the high byte scales the additive source contribution.
|
||||
alpha = a; strength = 0; blend = BlendKind.Additive; multiplyTint = true;
|
||||
}
|
||||
else if (o.StaticColorMode == 0 && o.OneShotColorBlend)
|
||||
{
|
||||
// Native mode 1 enables SRCALPHA/INVSRCALPHA and passes packed ARGB as D3D
|
||||
// modulation. Its high byte is opacity, not mode-0 tint/fill strength.
|
||||
alpha = a; strength = 0; blend = BlendKind.Alpha; multiplyTint = true;
|
||||
}
|
||||
else if (o.StaticColorMode == 2)
|
||||
|
||||
@@ -5,7 +5,8 @@ public static class SoftwareAffineRasterizer
|
||||
{
|
||||
public static void BlitRgba(byte[] dst, int dstW, int dstH, byte[] src, int srcW, int srcH,
|
||||
int srcX, int srcY, int width, int height, Affine2D localToDest,
|
||||
long tint, float tintStrength, float opacity, bool multiplyTint = false)
|
||||
long tint, float tintStrength, float opacity, bool multiplyTint = false,
|
||||
BlendKind blend = BlendKind.Alpha)
|
||||
{
|
||||
if (width <= 0 || height <= 0) return;
|
||||
int istr = (int)(System.Math.Clamp(tintStrength, 0f, 1f) * 255);
|
||||
@@ -15,7 +16,7 @@ public static class SoftwareAffineRasterizer
|
||||
if (TryIntegerTranslation(localToDest, out int tx, out int ty))
|
||||
{
|
||||
BlitTranslated(dst, dstW, dstH, src, srcW, srcX, srcY, width, height,
|
||||
tx, ty, tr, tg, tb, istr, ia, multiplyTint);
|
||||
tx, ty, tr, tg, tb, istr, ia, multiplyTint, blend);
|
||||
return;
|
||||
}
|
||||
if (!localToDest.TryInverse(out var inv)) return;
|
||||
@@ -30,7 +31,7 @@ public static class SoftwareAffineRasterizer
|
||||
int sr=multiplyTint ? src[si]*tr/255 : (src[si]*(255-istr)+tr*istr)/255;
|
||||
int sg=multiplyTint ? src[si+1]*tg/255 : (src[si+1]*(255-istr)+tg*istr)/255;
|
||||
int sb=multiplyTint ? src[si+2]*tb/255 : (src[si+2]*(255-istr)+tb*istr)/255;
|
||||
Blend(dst,di,sr,sg,sb,sa);
|
||||
Blend(dst,di,sr,sg,sb,sa,blend);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +68,8 @@ public static class SoftwareAffineRasterizer
|
||||
|
||||
private static void BlitTranslated(byte[] dst, int dstW, int dstH, byte[] src, int srcW,
|
||||
int srcX, int srcY, int width, int height, int tx, int ty,
|
||||
int tr, int tg, int tb, int istr, int ia, bool multiplyTint)
|
||||
int tr, int tg, int tb, int istr, int ia, bool multiplyTint,
|
||||
BlendKind blend)
|
||||
{
|
||||
int x0 = System.Math.Max(0, tx), y0 = System.Math.Max(0, ty);
|
||||
int x1 = (int)System.Math.Min(dstW, (long)tx + width);
|
||||
@@ -84,7 +86,7 @@ public static class SoftwareAffineRasterizer
|
||||
int sr = multiplyTint ? src[si] * tr / 255 : (src[si] * (255 - istr) + tr * istr) / 255;
|
||||
int sg = multiplyTint ? src[si + 1] * tg / 255 : (src[si + 1] * (255 - istr) + tg * istr) / 255;
|
||||
int sb = multiplyTint ? src[si + 2] * tb / 255 : (src[si + 2] * (255 - istr) + tb * istr) / 255;
|
||||
Blend(dst, di, sr, sg, sb, sa);
|
||||
Blend(dst, di, sr, sg, sb, sa, blend);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +110,12 @@ public static class SoftwareAffineRasterizer
|
||||
x1=System.Math.Min(dw,(int)System.Math.Ceiling(System.Math.Max(System.Math.Max(a.X,b.X),System.Math.Max(c.X,d.X))));
|
||||
y1=System.Math.Min(dh,(int)System.Math.Ceiling(System.Math.Max(System.Math.Max(a.Y,b.Y),System.Math.Max(c.Y,d.Y))));
|
||||
}
|
||||
private static void Blend(byte[] d,int i,int r,int g,int b,int a){
|
||||
private static void Blend(byte[] d,int i,int r,int g,int b,int a,BlendKind blend=BlendKind.Alpha){
|
||||
if(blend==BlendKind.Additive){
|
||||
d[i]=(byte)System.Math.Min(255,d[i]+r*a/255);d[i+1]=(byte)System.Math.Min(255,d[i+1]+g*a/255);
|
||||
d[i+2]=(byte)System.Math.Min(255,d[i+2]+b*a/255);d[i+3]=(byte)System.Math.Min(255,d[i+3]+a);
|
||||
return;
|
||||
}
|
||||
d[i]=(byte)((r*a+d[i]*(255-a))/255);d[i+1]=(byte)((g*a+d[i+1]*(255-a))/255);
|
||||
d[i+2]=(byte)((b*a+d[i+2]*(255-a))/255);d[i+3]=(byte)System.Math.Min(255,d[i+3]+a);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,14 @@ public sealed class ResourceMap
|
||||
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
||||
}
|
||||
|
||||
/// <summary>Resolve an already-normalized raw catalog id without applying a scene section base.</summary>
|
||||
public AssetEntry? ResolveRawTexture(long rawId)
|
||||
{
|
||||
var entry = _catalog.ResolveRaw(rawId);
|
||||
return entry is { IsPlaceholder: false } &&
|
||||
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
||||
}
|
||||
|
||||
/// <summary>Decode an AGF directly from loose-first VFS bytes.</summary>
|
||||
public RgbaImage DecodeTexture(AssetEntry entry) => AgfDecoder.Decode(_store, entry);
|
||||
|
||||
|
||||
@@ -341,32 +341,48 @@ public sealed class VirtualMachine
|
||||
previousRawInputFrame = _rawInputFrame;
|
||||
}
|
||||
var prev = _cur; _cur = frame; _depth++;
|
||||
_sink.Emit(TraceEvent.FrameEnter(frame.Script.Name, _depth, cause, callId));
|
||||
var outcome = FrameOutcome.RanOff;
|
||||
int pc = frame.Pc;
|
||||
while (pc >= 0 && pc < frame.Script.Instructions.Count)
|
||||
bool hostContextEntered = false;
|
||||
try
|
||||
{
|
||||
if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; outcome = FrameOutcome.Halted; break; }
|
||||
Steps++;
|
||||
if (_sink.TracingSteps) _sink.Emit(TraceEvent.Step(pc, frame.Script.Instructions[pc], _depth));
|
||||
int next = Step(frame.Script.Instructions[pc], pc);
|
||||
_host.FrameYield();
|
||||
if (next == FRAME_RETURN) { outcome = FrameOutcome.Returned; break; }
|
||||
if (next == HALT) { outcome = FrameOutcome.Halted; break; }
|
||||
pc = next;
|
||||
_host.EnterScriptContext(frame.Script.Name);
|
||||
hostContextEntered = true;
|
||||
_sink.Emit(TraceEvent.FrameEnter(frame.Script.Name, _depth, cause, callId));
|
||||
var outcome = FrameOutcome.RanOff;
|
||||
int pc = frame.Pc;
|
||||
while (pc >= 0 && pc < frame.Script.Instructions.Count)
|
||||
{
|
||||
if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; outcome = FrameOutcome.Halted; break; }
|
||||
Steps++;
|
||||
if (_sink.TracingSteps) _sink.Emit(TraceEvent.Step(pc, frame.Script.Instructions[pc], _depth));
|
||||
int next = Step(frame.Script.Instructions[pc], pc);
|
||||
_host.FrameYield();
|
||||
if (next == FRAME_RETURN) { outcome = FrameOutcome.Returned; break; }
|
||||
if (next == HALT) { outcome = FrameOutcome.Halted; break; }
|
||||
pc = next;
|
||||
}
|
||||
_sink.Emit(TraceEvent.FrameExit(frame.Script.Name, _depth, outcome.ToString()));
|
||||
return outcome;
|
||||
}
|
||||
_sink.Emit(TraceEvent.FrameExit(frame.Script.Name, _depth, outcome.ToString()));
|
||||
lock (_interactiveLock)
|
||||
finally
|
||||
{
|
||||
if (cause == FrameCause.CallScript)
|
||||
_interactiveFrame = previousInteractiveFrame?.Hotspots.Armed == true
|
||||
? previousInteractiveFrame : null;
|
||||
else if (ReferenceEquals(_interactiveFrame, frame))
|
||||
_interactiveFrame = null;
|
||||
if (ReferenceEquals(_rawInputFrame, frame)) _rawInputFrame = previousRawInputFrame;
|
||||
lock (_interactiveLock)
|
||||
{
|
||||
if (cause == FrameCause.CallScript)
|
||||
_interactiveFrame = previousInteractiveFrame?.Hotspots.Armed == true
|
||||
? previousInteractiveFrame : null;
|
||||
else if (ReferenceEquals(_interactiveFrame, frame))
|
||||
_interactiveFrame = null;
|
||||
if (ReferenceEquals(_rawInputFrame, frame)) _rawInputFrame = previousRawInputFrame;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (hostContextEntered) _host.ExitScriptContext();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cur = prev; _depth--;
|
||||
}
|
||||
}
|
||||
_cur = prev; _depth--;
|
||||
return outcome;
|
||||
}
|
||||
|
||||
private bool ServiceHotspotCallback()
|
||||
@@ -951,12 +967,17 @@ public sealed class VirtualMachine
|
||||
Gfx.ClearSurface((int)Read(a[0]));
|
||||
_host.CreateTexture((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2])); return pc + 1;
|
||||
case "set-texture": // 0x1f9 (resId)(slot)(colorkey) — load a file into the slot's surface
|
||||
{
|
||||
long requestedResourceId = Read(a[0]);
|
||||
long resolvedResourceId = _host.ResolveTextureResourceId(requestedResourceId);
|
||||
if (_diagSetTexture) // AGE_DIAG_SETTEX: log the SLOT operand source (literal vs which global) — grey-BG slot dig
|
||||
System.Console.Error.WriteLine($"[settex] resId=0x{Read(a[0]):x} slot={(int)Read(a[1])} " +
|
||||
System.Console.Error.WriteLine($"[settex] resId=0x{requestedResourceId:x}->0x{resolvedResourceId:x} slot={(int)Read(a[1])} " +
|
||||
$"slotOp=(type={a[1].Type} val=0x{a[1].Value:x}){(a[1].Type == 3 ? $" G[0x{a[1].Value:x}]" : "")}");
|
||||
_host.ReleaseSurface((int)Read(a[1]));
|
||||
Gfx.SetSurface((int)Read(a[1]), Read(a[0]), a.Count > 2 ? Read(a[2]) : 0);
|
||||
_host.SetTexture(Read(a[0]), (int)Read(a[1])); return pc + 1; // host still tracks dims for get-texture-size
|
||||
Gfx.SetSurface((int)Read(a[1]), resolvedResourceId, a.Count > 2 ? Read(a[2]) : 0);
|
||||
_host.SetTexture(resolvedResourceId, (int)Read(a[1]));
|
||||
return pc + 1; // host still tracks dims for get-texture-size
|
||||
}
|
||||
case "draw-texture": // 0x1fb (handle)(slot)(srcX)(srcY)(w)(h)(dstX)(dstY) — bind object -> surface + rect + pos
|
||||
Gfx.BindDraw(Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),
|
||||
(int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7]));
|
||||
|
||||
Reference in New Issue
Block a user