Files

780 lines
31 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}
}