Add OPL4 playback and analyzer support

This commit is contained in:
2026-09-15 14:59:29 +03:00
commit 1686838e03
553 changed files with 50524 additions and 0 deletions
+779
View File
@@ -0,0 +1,779 @@
// WcKernel — WC (Wild Commander) API emulator for the C# debugger
// Traps CALL 0x6006 and emulates function dispatch based on A register
//
// Emulated functions (used by VGM player):
// 0x00 = MNGC_PL — plugin page → Win3
// 0x01 = PRWOW — draw window (stub)
// 0x02 = RRESB — restore window (stub)
// 0x05 = GADRW — get screen address
// 0x0E = TURBOPL — CPU speed (stub)
// 0x0F = GEDPL — restore display (stub)
// 0x30 = LOAD512 — load sectors from file → memory
// 0x41 = MNGCVPL — video page → Win3
// 0x4E = MNG0_PL — plugin page → Win0
// 0x4F = MNG8_PL — plugin page → Win2
// + keyboard stubs, file stubs, etc.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using ZXMAK2.Engine.Cpu;
using ZXMAK2.Engine.Cpu.Processor;
namespace InflateDebugger
{
public class WcKernel
{
// ── WC Constants ─────────────────────────────────────────────
public const ushort WC_ENTRY = 0x6006;
// Physical page bases
public const byte PLGPG = 0x61; // plugin page base (page 0 = 0x61)
public const byte TVBPG = 0x20; // megabuffer / video page base
public const byte MTXPG = 0x00; // main text page
// System variable addresses (in Win1 = 0x4000–0x7FFF)
public const ushort SYS_PG0 = 0x6000;
public const ushort SYS_PG4 = 0x6001;
public const ushort SYS_PG8 = 0x6002;
public const ushort SYS_PGC = 0x6003;
public const ushort SYS_ABT = 0x6004;
public const ushort SYS_ENT = 0x6005;
public const ushort SYS_TMN = 0x6009;
public const ushort SYS_CNFV = 0x600D;
public const ushort SYS_HEI = 0x600E;
// Function numbers (A register values for CALL 0x6006)
public const byte FN_MNGC_PL = 0x00;
public const byte FN_PRWOW = 0x01;
public const byte FN_RRESB = 0x02;
public const byte FN_PRSRW = 0x03;
public const byte FN_TXTPR = 0x04;
public const byte FN_GADRW = 0x05;
public const byte FN_CURSOR = 0x06;
public const byte FN_CURSER = 0x07;
public const byte FN_YN = 0x08;
public const byte FN_ISTR = 0x09;
public const byte FN_NORK = 0x0A;
public const byte FN_MEZZ = 0x0B;
public const byte FN_SCRLWOW = 0x0C;
public const byte FN_PRSRW_A = 0x0D;
public const byte FN_TURBOPL = 0x0E;
public const byte FN_GEDPL = 0x0F;
// Keyboard: 0x10–0x2F
public const byte FN_KEY_SPACE = 0x10;
public const byte FN_KEY_UP = 0x11;
public const byte FN_KEY_DOWN = 0x12;
public const byte FN_KEY_LEFT = 0x13;
public const byte FN_KEY_RIGHT = 0x14;
public const byte FN_KEY_TAB = 0x15;
public const byte FN_KEY_ENTER = 0x16;
public const byte FN_KEY_ESC = 0x17;
public const byte FN_KEY_BSPC = 0x18;
public const byte FN_KEY_PGUP = 0x19;
public const byte FN_KEY_PGDN = 0x1A;
public const byte FN_KEY_HOME = 0x1B;
public const byte FN_KEY_END = 0x1C;
public const byte FN_KBSCN = 0x1D;
// F1–F10: 0x1E–0x27, ALT=0x28, SHIFT=0x29, CTRL=0x2A
// DEL=0x2B, CAPS=0x2C, ANY=0x2D, WAITREL=0x2E, WAITANY=0x2F
public const byte FN_LOAD512 = 0x30;
public const byte FN_SAVE512 = 0x31;
public const byte FN_GIPAGPL = 0x32;
public const byte FN_TENTRY = 0x33;
public const byte FN_CHTOSEP = 0x34;
public const byte FN_TMRKDFL = 0x35;
public const byte FN_ADIR = 0x36;
public const byte FN_STREAM = 0x37;
public const byte FN_FINDNEXT = 0x38;
public const byte FN_FENTRY = 0x39;
public const byte FN_LOADNONE = 0x3A;
public const byte FN_GFILE = 0x3B;
public const byte FN_GDIR = 0x3C;
public const byte FN_MKFILE = 0x3D;
public const byte FN_MKDIR = 0x3E;
public const byte FN_RENAME = 0x3F;
public const byte FN_DELFL = 0x40;
public const byte FN_MNGCVPL = 0x41;
public const byte FN_MNGV_PL = 0x42;
public const byte FN_GVMOD = 0x43;
public const byte FN_GYOFF = 0x44;
public const byte FN_GXOFF = 0x45;
public const byte FN_GVTM = 0x46;
public const byte FN_GVTL = 0x47;
public const byte FN_GVSGP = 0x48;
public const byte FN_MNG0VPL = 0x49;
public const byte FN_MNG8VPL = 0x4A;
public const byte FN_DMAPL = 0x4B;
public const byte FN_PRM_PL = 0x4C;
public const byte FN_INT_PL = 0x4D;
public const byte FN_MNG0_PL = 0x4E;
public const byte FN_MNG8_PL = 0x4F;
public const byte FN_STRSET = 0x50;
public const byte FN_LOAD256 = 0x51;
public const byte FN_INT_PL_H = 0x52;
public const byte FN_RES_JUMP = 0x53;
public const byte FN_RES_CALL = 0x54;
public const byte FN_KEY_INS = 0x55;
// ── State ─────────────────────────────────────────────────────
private readonly TsMemory _mem;
private CpuUnit _cpu;
// Plugin page offset within the 64-page plugin area (typically 0)
public byte PPGP { get; set; } = 0;
// Emulated file I/O state
private byte[] _fileData; // loaded file data (whole VGM/VGZ)
private int _filePos; // current file read position (byte offset)
public uint FileSize => _fileData != null ? (uint)_fileData.Length : 0;
// Disk root directory (test_data path)
public string DiskRootPath { get; set; }
// WC call log
public List<WcCallRecord> CallLog { get; } = new();
public bool LogCalls { get; set; } = true;
public int MaxCallLog { get; set; } = 10000;
// Saved Win0 page (P8000 saves/restores it on each WC call)
private byte _savedWin0Page;
// Screen height (for gadrw calculations)
public byte ScreenHeight { get; set; } = 36; // 90×36 (TextMode=2)
public byte ScreenWidth { get; set; } = 90;
// WC system page (mapped to Win1 permanently)
public byte SysVarPage { get; set; } = 0x05;
// Callbacks for logging
public Action<string> Log { get; set; }
// ── Constructor ───────────────────────────────────────────────
public WcKernel(TsMemory mem, CpuUnit cpu)
{
_mem = mem;
_cpu = cpu;
}
// ── File I/O emulation ────────────────────────────────────────
/// <summary>Load a file for emulated file I/O (simulates WC file open + stream)</summary>
public void SetFileData(byte[] data)
{
_fileData = data;
_filePos = 0;
}
/// <summary>Load a file from disk root by name</summary>
public bool OpenFile(string name)
{
if (string.IsNullOrEmpty(DiskRootPath)) return false;
string path = Path.Combine(DiskRootPath, name);
if (!File.Exists(path)) return false;
_fileData = File.ReadAllBytes(path);
_filePos = 0;
return true;
}
// ── Setup system variables ────────────────────────────────────
public void InitSysVars()
{
var pg = _mem.GetPage(SysVarPage);
// PG0–PGC (0x6000–0x6003 = offset 0x2000 in Win1)
pg[0x2000] = _mem.GetWindow(0);
pg[0x2001] = _mem.GetWindow(1);
pg[0x2002] = _mem.GetWindow(2);
pg[0x2003] = _mem.GetWindow(3);
// ABT / ENT = 0
pg[0x2004] = 0;
pg[0x2005] = 0;
// JP FUN instruction at 0x6006 (we trap this, but put real bytes for reads)
pg[0x2006] = 0xC3; // JP
pg[0x2007] = 0x00;
pg[0x2008] = 0x00;
// TMN timer
pg[0x2009] = 0;
pg[0x200A] = 0;
// CNFv = 7 (VDAC2/FT812 — TSConfig default)
pg[0x200D] = 7;
// HEI = screen height
pg[0x200E] = ScreenHeight;
}
// ── Update PGC in sys vars after page switch ──────────────────
private void UpdateSysPGC()
{
_mem.GetPage(SysVarPage)[0x2003] = _mem.GetWindow(3);
}
private void UpdateSysPG0()
{
_mem.GetPage(SysVarPage)[0x2000] = _mem.GetWindow(0);
}
private void UpdateSysPG8()
{
_mem.GetPage(SysVarPage)[0x2002] = _mem.GetWindow(2);
}
// ── Main trap handler ─────────────────────────────────────────
/// <summary>
/// Check if CPU is about to execute CALL 0x6006 and intercept it.
/// Call this BEFORE ExecCycle() when PC == address of CALL 0x6006.
/// Returns true if the call was intercepted.
/// </summary>
public bool TryIntercept()
{
ushort pc = _cpu.regs.PC;
// Check: is the instruction at PC a CALL 0x6006?
// CALL nn = CD lo hi → CD 06 60
if (_mem.Read(pc) == 0xCD &&
_mem.Read((ushort)(pc + 1)) == 0x06 &&
_mem.Read((ushort)(pc + 2)) == 0x60)
{
byte funcNum = _cpu.regs.A;
ushort retAddr = (ushort)(pc + 3);
// Log the call
if (LogCalls && CallLog.Count < MaxCallLog)
{
CallLog.Add(new WcCallRecord
{
Tact = _cpu.Tact,
PC = pc,
FuncNum = funcNum,
AF = _cpu.regs.AF,
BC = _cpu.regs.BC,
DE = _cpu.regs.DE,
HL = _cpu.regs.HL,
AF2 = _cpu.regs._AF,
IX = _cpu.regs.IX,
SP = _cpu.regs.SP
});
}
// Emulate P8000 dispatch: save Win0, set Win3=TXT
_savedWin0Page = _mem.GetWindow(0);
// Execute the function
DispatchFunction(funcNum);
// P8000 epilogue: restore Win0 only.
// Do NOT reset Win3 — page management functions (MNGCPL, MNGCVPL)
// explicitly set Win3 and the caller expects it to persist.
_mem.SetWindow(0, _savedWin0Page);
// Update sys vars to reflect actual window state
UpdateSysPGC();
UpdateSysPG0();
// Set PC past the CALL instruction
_cpu.regs.PC = retAddr;
// Advance Tact (approximate: a typical WC call takes ~100-500 T-states)
_cpu.Tact += 100;
return true;
}
return false;
}
// ── Function dispatcher ───────────────────────────────────────
private void DispatchFunction(byte funcNum)
{
switch (funcNum)
{
// ── Page management ──
case FN_MNGC_PL: Fn_MngcPl(); break;
case FN_MNG0_PL: Fn_Mng0Pl(); break;
case FN_MNG8_PL: Fn_Mng8Pl(); break;
case FN_MNGCVPL: Fn_Mngcvpl(); break;
case FN_MNG0VPL: Fn_Mng0vpl(); break;
case FN_MNG8VPL: Fn_Mng8vpl(); break;
// ── UI (stubs) ──
case FN_GEDPL: Fn_Gedpl(); break;
case FN_PRWOW: Fn_Prwow(); break;
case FN_RRESB: Fn_Rresb(); break;
case FN_GADRW: Fn_Gadrw(); break;
case FN_PRSRW: Fn_Stub("PRSRW"); break;
case FN_PRSRW_A: Fn_Stub("PRSRW_A");break;
case FN_TXTPR: Fn_Stub("TXTPR"); break;
case FN_SCRLWOW: Fn_Stub("SCRLWOW"); break;
case FN_CURSOR: Fn_Stub("CURSOR"); break;
case FN_CURSER: Fn_Stub("CURSER"); break;
case FN_YN: Fn_Stub("YN"); break;
case FN_ISTR: Fn_Stub("ISTR"); break;
case FN_NORK: Fn_Stub("NORK"); break;
case FN_MEZZ: Fn_Stub("MEZZ"); break;
// ── Turbo / misc ──
case FN_TURBOPL: Fn_Turbopl(); break;
case FN_PRM_PL: Fn_PrmPl(); break;
case FN_INT_PL: Fn_Stub("INT_PL"); break;
case FN_INT_PL_H: Fn_Stub("INT_PL_H");break;
case FN_STRSET: Fn_Strset(); break;
case FN_DMAPL: Fn_Stub("DMAPL"); break;
// ── Keyboard (all return 0 = not pressed) ──
case >= FN_KEY_SPACE and <= 0x2F:
Fn_KeyStub(funcNum);
break;
// ── File I/O ──
case FN_LOAD512: Fn_Load512(); break;
case FN_LOAD256: Fn_Load256(); break;
case FN_SAVE512: Fn_Stub("SAVE512"); break;
case FN_GIPAGPL: Fn_Gipagpl(); break;
case FN_STREAM: Fn_Stream(); break;
case FN_FENTRY: Fn_Fentry(); break;
case FN_GFILE: Fn_Gfile(); break;
case FN_ADIR: Fn_Stub("ADIR"); break;
case FN_FINDNEXT: Fn_Stub("FINDNEXT");break;
case FN_TENTRY: Fn_Stub("TENTRY"); break;
case FN_GDIR: Fn_Stub("GDIR"); break;
case FN_LOADNONE: Fn_Loadnone(); break;
// ── Graphics (stubs) ──
case FN_MNGV_PL: Fn_Stub("MNGV_PL"); break;
case FN_GVMOD: Fn_Stub("GVMOD"); break;
case FN_GYOFF: Fn_Stub("GYOFF"); break;
case FN_GXOFF: Fn_Stub("GXOFF"); break;
// ── Resident calls ──
case FN_RES_JUMP: Fn_Stub("RES_JUMP"); break;
case FN_RES_CALL: Fn_Stub("RES_CALL"); break;
default:
Log?.Invoke($" WC UNKNOWN function 0x{funcNum:X2} at tact={_cpu.Tact}");
break;
}
}
// ═══════════════════════════════════════════════════════════════
// Page management functions
// ═══════════════════════════════════════════════════════════════
/// <summary>0x00 MNGC_PL: plugin page A' → Win3. phys = A' + PLGPG + PPGP</summary>
private void Fn_MngcPl()
{
byte pageArg = (byte)(_cpu.regs._AF >> 8); // A' register
if (pageArg == 0xFE)
{
// 0xFE = first text screen
_mem.SetWindow(3, MTXPG);
}
else if (pageArg == 0xFF)
{
// 0xFF = font page
_mem.SetWindow(3, 0x01);
}
else if (pageArg < 64)
{
byte phys = (byte)(pageArg + PLGPG + PPGP);
_mem.SetWindow(3, phys);
}
// else >= 64: NOP (return without changing)
UpdateSysPGC();
}
/// <summary>0x4E MNG0_PL: plugin page A' → Win0. phys = A' + PLGPG + PPGP</summary>
private void Fn_Mng0Pl()
{
byte pageArg = (byte)(_cpu.regs._AF >> 8);
if (pageArg < 64)
{
byte phys = (byte)(pageArg + PLGPG + PPGP);
_mem.SetWindow(0, phys);
UpdateSysPG0();
}
}
/// <summary>0x4F MNG8_PL: plugin page A' → Win2. phys = A' + PLGPG + PPGP</summary>
private void Fn_Mng8Pl()
{
byte pageArg = (byte)(_cpu.regs._AF >> 8);
if (pageArg < 64)
{
byte phys = (byte)(pageArg + PLGPG + PPGP);
_mem.SetWindow(2, phys);
UpdateSysPG8();
}
}
/// <summary>0x41 MNGCVPL: video page A' → Win3. phys = A' + TVBPG(0x20)</summary>
private void Fn_Mngcvpl()
{
byte vpage = (byte)(_cpu.regs._AF >> 8);
if (vpage < 64)
{
byte phys = (byte)(vpage + TVBPG);
_mem.SetWindow(3, phys);
}
UpdateSysPGC();
}
/// <summary>0x49 MNG0VPL: video page A' → Win0</summary>
private void Fn_Mng0vpl()
{
byte vpage = (byte)(_cpu.regs._AF >> 8);
if (vpage < 64)
{
byte phys = (byte)(vpage + TVBPG);
_mem.SetWindow(0, phys);
UpdateSysPG0();
}
}
/// <summary>0x4A MNG8VPL: video page A' → Win2</summary>
private void Fn_Mng8vpl()
{
byte vpage = (byte)(_cpu.regs._AF >> 8);
if (vpage < 64)
{
byte phys = (byte)(vpage + TVBPG);
_mem.SetWindow(2, phys);
UpdateSysPG8();
}
}
// ═══════════════════════════════════════════════════════════════
// UI functions (mostly stubs for text-mode UI)
// ═══════════════════════════════════════════════════════════════
/// <summary>0x0F GEDPL: restore WC display (stub — sets Win3 to TXT)</summary>
private void Fn_Gedpl()
{
// Real WC: restores palette, attributes, text mode
// Also trashes 0x1000–0x1447 in Win0 — we emulate that
_mem.SetWindow(3, MTXPG);
UpdateSysPGC();
Log?.Invoke(" WC GEDPL (stub)");
}
/// <summary>0x01 PRWOW: draw window (stub)</summary>
private void Fn_Prwow()
{
// IX = win ptr (from HL in sdcccall)
_mem.SetWindow(3, MTXPG);
UpdateSysPGC();
}
/// <summary>0x02 RRESB: restore window background (stub)</summary>
private void Fn_Rresb()
{
_mem.SetWindow(3, MTXPG);
UpdateSysPGC();
}
/// <summary>0x05 GADRW: get screen address for (y,x) in window</summary>
private void Fn_Gadrw()
{
// In wc_api.s: IX=win, D=y, E=x
// Returns DE = screen address
// Text screen at 0xC000, each row = ScreenWidth bytes
// Window content starts at win.x + win.y * ScreenWidth + 0xC000
// We compute: addr = 0xC000 + (win_y + y) * ScreenWidth + (win_x + x)
ushort winPtr = _cpu.regs.IX;
byte wy = _mem.Read((ushort)(winPtr + 3)); // win.y at +3
byte wx = _mem.Read((ushort)(winPtr + 2)); // win.x at +2
byte y = _cpu.regs.D;
byte x = _cpu.regs.E;
ushort addr = (ushort)(0xC000 + (wy + y) * ScreenWidth + (wx + x));
_cpu.regs.DE = addr;
// Also need to set Win3 to TXT
_mem.SetWindow(3, MTXPG);
UpdateSysPGC();
}
/// <summary>0x0E TURBOPL: set CPU/AY speed via SYSCONF port</summary>
private void Fn_Turbopl()
{
// B = mode (0=CPU, 1=AY, 0xFF=restore), C = param
byte mode = (byte)(_cpu.regs.BC >> 8); // B
byte param = (byte)(_cpu.regs.BC & 0xFF); // C
if (mode == 0x00) // WC_TURBO_CPU: set CPU frequency
{
// param: 0=3.5, 1=7, 2=14, 3=28
// Write turbo bits to SYSCONF port so PluginRunner picks it up
byte sysconf = (byte)(param & 0x03);
_cpu.WRPORT(0x20AF, sysconf);
}
// mode 1 (AY freq) — not relevant for emulation
// mode 0xFF (restore) — restore to 3.5 MHz
else if (mode == 0xFF)
{
_cpu.WRPORT(0x20AF, 0x00); // restore to 3.5 MHz
}
}
/// <summary>0x4C PRM_PL: get INI parameter (stub — return 0xFF = not set)</summary>
private void Fn_PrmPl()
{
_cpu.regs.A = 0xFF;
}
/// <summary>0x50 STRSET: memset(ptr, len, char)</summary>
private void Fn_Strset()
{
// In wc_api.s: HL=dst, DE=len, A'=char
// We actually don't need real WC for this, but the plugin calls it
ushort dst = _cpu.regs.HL;
ushort len = _cpu.regs.DE;
byte ch = (byte)(_cpu.regs._AF >> 8);
for (int i = 0; i < len; i++)
_mem.Write((ushort)(dst + i), ch);
}
// ═══════════════════════════════════════════════════════════════
// Keyboard stubs (all return A=0 = not pressed)
// ═══════════════════════════════════════════════════════════════
private void Fn_KeyStub(byte funcNum)
{
_cpu.regs.A = 0; // not pressed
// Clear Z flag by default (A=0 → Z=1 → key not pressed)
}
// ═══════════════════════════════════════════════════════════════
// File I/O emulation
// ═══════════════════════════════════════════════════════════════
/// <summary>0x30 LOAD512: load blocks×512 bytes from file → memory at dest</summary>
private void Fn_Load512()
{
// wc_api.s _wc_load512: HL=dest, B=blocks (from stack via frame pointer)
// WC API #0x30: HL=dest, B=blocks → A=0 ok / 0x0F EOF
// Returns: A = status, HL = next address after loaded data
ushort loadDest = _cpu.regs.HL;
byte blocks = _cpu.regs.B;
int bytesToLoad = blocks * 512;
if (_fileData == null)
{
Log?.Invoke(" WC LOAD512: no file data!");
_cpu.regs.A = 0xFF; // error
return;
}
int remaining = _fileData.Length - _filePos;
int actual = Math.Min(bytesToLoad, remaining);
bool isEof = (actual < bytesToLoad) || (_filePos + actual >= _fileData.Length);
// Write data into memory at loadDest (using Win3 map)
for (int i = 0; i < actual; i++)
{
_mem.Write((ushort)(loadDest + i), _fileData[_filePos + i]);
}
_filePos += actual;
// Update HL to point past the loaded data
_cpu.regs.HL = (ushort)(loadDest + actual);
_cpu.regs.A = isEof ? (byte)0x0F : (byte)0; // 0x0F = WC_EOF
Log?.Invoke($" LOAD512: dest=0x{loadDest:X4} blocks={blocks} bytes={actual} eof={isEof} filePos={_filePos}/{_fileData.Length}");
}
/// <summary>0x51 LOAD256: load blocks×256 bytes</summary>
private void Fn_Load256()
{
// Same pattern as LOAD512 but 256-byte blocks: HL=dest, B=blocks
ushort loadDest = _cpu.regs.HL;
byte blocks = _cpu.regs.B;
int bytesToLoad = blocks * 256;
if (_fileData == null) { _cpu.regs.A = 0xFF; return; }
int remaining = _fileData.Length - _filePos;
int actual = Math.Min(bytesToLoad, remaining);
bool isEof = actual < bytesToLoad || _filePos + actual >= _fileData.Length;
for (int i = 0; i < actual; i++)
_mem.Write((ushort)(loadDest + i), _fileData[_filePos + i]);
_filePos += actual;
_cpu.regs.HL = (ushort)(loadDest + actual);
_cpu.regs.A = isEof ? (byte)0x0F : (byte)0;
Log?.Invoke($" LOAD256: dest=0x{loadDest:X4} blocks={blocks} bytes={actual} eof={isEof}");
}
/// <summary>0x32 GIPAGPL: seek file to beginning</summary>
private void Fn_Gipagpl()
{
_filePos = 0;
}
/// <summary>0x37 STREAM: open/switch file stream (stub — just log)</summary>
private void Fn_Stream()
{
// D = mode (0xFF=root, 0xFE=clone, 0xFD=wcdir, 0/1=stream#)
Log?.Invoke($" WC STREAM mode=0x{_cpu.regs.D:X2}");
}
/// <summary>0x39 FENTRY: find file by name</summary>
private void Fn_Fentry()
{
// HL = addr of [flag_byte][name_string][0]
ushort namePtr = _cpu.regs.HL;
byte flag = _mem.Read(namePtr);
namePtr++;
// Read filename
var sb = new StringBuilder();
for (int i = 0; i < 255; i++)
{
byte ch = _mem.Read((ushort)(namePtr + i));
if (ch == 0) break;
sb.Append((char)ch);
}
string fileName = sb.ToString();
Log?.Invoke($" WC FENTRY: \"{fileName}\" flag=0x{flag:X2}");
// Try to find the file on disk root
if (!string.IsNullOrEmpty(DiskRootPath))
{
string path = Path.Combine(DiskRootPath, fileName);
if (File.Exists(path))
{
_cpu.regs.A = 1; // found (non-zero)
// Set file size in DE:HL
long fsize = new FileInfo(path).Length;
_cpu.regs.HL = (ushort)(fsize & 0xFFFF);
_cpu.regs.DE = (ushort)((fsize >> 16) & 0xFFFF);
// Store path for subsequent GFILE + LOAD512
_pendingFileName = path;
return;
}
}
_cpu.regs.A = 0; // not found
}
private string _pendingFileName;
/// <summary>0x3B GFILE: open found file for reading</summary>
private void Fn_Gfile()
{
if (!string.IsNullOrEmpty(_pendingFileName) && File.Exists(_pendingFileName))
{
_fileData = File.ReadAllBytes(_pendingFileName);
_filePos = 0;
Log?.Invoke($" WC GFILE: opened \"{_pendingFileName}\" ({_fileData.Length} bytes)");
}
else
{
Log?.Invoke(" WC GFILE: no pending file");
}
}
/// <summary>0x3A LOADNONE: skip N sectors in stream</summary>
private void Fn_Loadnone()
{
byte sectors = (byte)(_cpu.regs._AF >> 8);
int skip = sectors * 512;
if (_fileData != null)
_filePos = Math.Min(_filePos + skip, _fileData.Length);
}
// ═══════════════════════════════════════════════════════════════
// Stub for unimplemented functions
// ═══════════════════════════════════════════════════════════════
private void Fn_Stub(string name)
{
// Most stubs just need Win3 = TXT (already done in P8000 epilogue)
}
// ═══════════════════════════════════════════════════════════════
// Call log
// ═══════════════════════════════════════════════════════════════
public string DumpCallLog(int last = 50)
{
var sb = new StringBuilder();
sb.AppendLine($"=== WC Call Log (last {last} of {CallLog.Count}) ===");
int start = Math.Max(0, CallLog.Count - last);
for (int i = start; i < CallLog.Count; i++)
{
var c = CallLog[i];
sb.AppendFormat("[{0,10}] PC={1:X4} FN=0x{2:X2} ({3}) AF={4:X4} BC={5:X4} DE={6:X4} HL={7:X4} IX={8:X4}",
c.Tact, c.PC, c.FuncNum, GetFuncName(c.FuncNum),
c.AF, c.BC, c.DE, c.HL, c.IX);
sb.AppendLine();
}
return sb.ToString();
}
public static string GetFuncName(byte fn) => fn switch
{
FN_MNGC_PL => "MNGC_PL",
FN_PRWOW => "PRWOW",
FN_RRESB => "RRESB",
FN_PRSRW => "PRSRW",
FN_TXTPR => "TXTPR",
FN_GADRW => "GADRW",
FN_TURBOPL => "TURBOPL",
FN_GEDPL => "GEDPL",
FN_LOAD512 => "LOAD512",
FN_LOAD256 => "LOAD256",
FN_GIPAGPL => "GIPAGPL",
FN_STREAM => "STREAM",
FN_FENTRY => "FENTRY",
FN_GFILE => "GFILE",
FN_MNGCVPL => "MNGCVPL",
FN_MNG0_PL => "MNG0_PL",
FN_MNG8_PL => "MNG8_PL",
FN_MNG0VPL => "MNG0VPL",
FN_MNG8VPL => "MNG8VPL",
FN_STRSET => "STRSET",
FN_PRM_PL => "PRM_PL",
FN_ADIR => "ADIR",
FN_LOADNONE=> "LOADNONE",
FN_SAVE512 => "SAVE512",
FN_DMAPL => "DMAPL",
>= FN_KEY_SPACE and <= 0x2F => $"KEY_{fn - FN_KEY_SPACE:X2}",
_ => $"?{fn:X2}"
};
}
// ── WC call record ────────────────────────────────────────────────
public struct WcCallRecord
{
public long Tact;
public ushort PC;
public byte FuncNum;
public ushort AF, BC, DE, HL;
public ushort AF2;
public ushort IX, SP;
}
}