Files
4cade/tests/lib/apple2.lua
T

1291 lines
38 KiB
Lua

--[[============================================================
Generic utilities for Apple II
============================================================]]
local apple2 = {
SCREEN_HEIGHT = 192,
SCREEN_WIDTH = 560,
SCREEN_COLUMNS = 80,
}
local util = require("util")
local machine = manager.machine
--------------------------------------------------
-- System Abstractions
--------------------------------------------------
local mouse
local auxram
-- Defaults (Apple IIe, Apple IIc)
local keyboard = {
["Return"] = { port = ":X6", field = "Return" },
["Delete"] = { port = ":X7", field = "Delete" },
["Escape"] = { port = ":X0", field = "Esc" },
["Tab"] = { port = ":X1", field = "Tab" },
["Up Arrow"] = { port = ":X6", field = "↑" },
["Left Arrow"] = { port = ":X7", field = "←" },
["Right Arrow"] = { port = ":X7", field = "→" },
["Down Arrow"] = { port = ":X7", field = "↓" },
-- modifiers
["Control"] = { port = ":keyb_special", field = "Control" },
["Shift"] = { port = ":keyb_special", field = "Left Shift" },
["Open Apple"] = { port = ":keyb_special", field = "Open Apple" },
["Solid Apple"] = { port = ":keyb_special", field = "Solid Apple" },
-- Other
["Reset"] = { port = ":keyb_special", field = "RESET" },
["Caps Lock"] = { port = ":keyb_special", field = "Caps Lock", bits = 0x01 },
}
local function get_device(pattern)
for name,dev in pairs(machine.devices) do
if name:match(pattern) then return dev end
end
error("Failed to find device " .. pattern)
end
-- System class is one of: "apple2", "apple2e", "apple2c", "apple2gs"
local system_class = tostring(manager.machine.system.parent)
if system_class == "0" then
system_class = manager.machine.system.name
end
local scan_for_mouse = false
if machine.system.name:match("^apple2e")
or machine.system.name:match("^tk3000") then
-- Apple IIe
-- * mouse card required (if mouse is used)
-- * many possible aux memory devices
scan_for_mouse = true
if emu.subst_env("$CHECKAUXMEMORY") == "true" then
auxram = emu.item(get_device("^:aux:").items["0/m_ram"])
end
elseif machine.system.name:match("^apple2c") then
-- Apple IIc / Apple IIc Plus
-- * built-in mouse port
-- * aux memory is just exposed as RAM > 0xFFFF
mouse = {
x = { port = ":mse_x", field = "Mouse X" },
y = { port = ":mse_y", field = "Mouse Y" },
b = { port = ":mse_button", field = "Mouse Button" },
}
elseif machine.system.name:match("^las.*128") then
-- Laser 128
-- * built-in mouse port
-- * aux memory is just exposed as RAM > 0xFFFF
mouse = {
x = { port = ":mse_x", field = "Mouse X" },
y = { port = ":mse_y", field = "Mouse Y" },
b = { port = ":mse_button", field = "Mouse Button" },
}
keyboard["Open Apple"].field = "Open Triangle"
keyboard["Solid Apple"].field = "Solid Triangle"
elseif machine.system.name:match("^apple2gs") then
-- Apple IIgs
-- * built-in mouse port (ADB)
-- * aux memory is just exposed as RAM > 0xFFFF
mouse = {
x = { port = ":macadb:MOUSE1", field = "Mouse X" },
y = { port = ":macadb:MOUSE2", field = "Mouse Y" },
b = { port = ":macadb:MOUSE0", field = "Mouse Button 0" },
}
keyboard = {
["Return"] = { port = ":macadb:KEY2", field = "Return" },
["Delete"] = { port = ":macadb:KEY3", field = "Backspace" },
["Escape"] = { port = ":macadb:KEY3", field = "Esc" },
["Tab"] = { port = ":macadb:KEY3", field = "Tab" },
["Up Arrow"] = { port = ":macadb:KEY3", field = "Up Arrow" },
["Left Arrow"] = { port = ":macadb:KEY3", field = "Left Arrow" },
["Right Arrow"] = { port = ":macadb:KEY3", field = "Right Arrow" },
["Down Arrow"] = { port = ":macadb:KEY3", field = "Down Arrow" },
-- modifiers
["Control"] = { port = ":macadb:KEY3", field = "Control" },
["Shift"] = { port = ":macadb:KEY3", field = "Shift" },
-- Ample MAME builds modify port names for OA/SA, so support both forms
-- https://github.com/ksherlock/mame/blob/9069cdfc14f0abc9634540bf3d2ebc1d21951a50/src/mame/apple/macadb.cpp#L157-L169
["Open Apple"] = { port = ":macadb:KEY3", field = { "Command / Open Apple" , "Command" } },
["Solid Apple"] = { port = ":macadb:KEY3", field = { "Option / Solid Apple" , "Option" } },
-- Other
["Reset"] = { port = ":macadb:KEY5", field = "Reset / Power" },
["Caps Lock"] = { port = ":macadb:KEY3", field = "Caps Lock", bits = 0x0200 },
}
elseif machine.system.name:match("^ace500") then
-- Franklin ACE 500
-- * has built-in mouse port
-- * built-in aux memory device
auxram = emu.item(get_device("^:aux$").items["0/m_ram"])
mouse = {
x = { port = ":mse_x", field = "Mouse X" },
y = { port = ":mse_y", field = "Mouse Y" },
b = { port = ":mse_button", field = "Mouse Button" },
}
keyboard["Open Apple"].field = "Open F"
keyboard["Solid Apple"].field = "Solid F"
keyboard["Reset"].field = "RESET"
elseif machine.system.name:match("^ace2200") then
-- Franklin ACE 2200
-- * mouse card required
-- * built-in aux memory device
auxram = emu.item(get_device("^:aux$").items["0/m_ram"])
scan_for_mouse = true
keyboard["Open Apple"].field = "Open F"
keyboard["Solid Apple"].field = "Solid F"
keyboard["Reset"].field = "RESET"
elseif machine.system.name:match("^prav8c") then
-- Pravetz 8C
-- * mouse card required
-- * many possible aux memory devices
scan_for_mouse = true
if emu.subst_env("$CHECKAUXMEMORY") == "true" then
auxram = emu.item(get_device("^:aux:").items["0/m_ram"])
end
keyboard = {
["Return"] = { port = ":kbd:KEY8", field = "Return" },
["Delete"] = { port = ":kbd:KEY8", field = "Del" },
["Escape"] = { port = ":kbd:KEY4", field = "Esc" },
["Tab"] = { port = ":kbd:KEY4", field = "Tab" },
["Up Arrow"] = { port = ":kbd:KEY9", field = "Cursor Up" },
["Left Arrow"] = { port = ":kbd:KEY7", field = "Cursor Left" },
["Right Arrow"] = { port = ":kbd:KEY8", field = "Cursor Right" },
["Down Arrow"] = { port = ":kbd:KEY9", field = "Cursor Down" },
-- modifiers
["Control"] = { port = ":kbd:KEY4", field = "Ctrl" },
["Shift"] = { port = ":kbd:KEY5", field = "Shift (Left)" },
["Open Apple"] = { port = ":kbd:FN", field = "F1" },
["Solid Apple"] = { port = ":kbd:FN", field = "F2" },
-- Other
["Reset"] = { port = ":kbd:KEY9", field = "Reset" },
["Caps Lock"] = { port = ":kbd:KEY6", field = "Caps Lock", bits = 0x01 }, -- TODO: bits?
}
elseif machine.system.name:match("^apple2p") or machine.system.name:match("^apple2$") then
-- minimal support for launcher testing
if machine.ioport.ports[":keyb_special"] then
-- MAME through 0.283
keyboard = {
["Control"] = { port = ":keyb_special", field = "Control" },
["Shift"] = { port = ":keyb_special", field = "Left Shift" },
["Reset"] = { port = ":keyb_special", field = "Reset" },
["Left Arrow"] = { port = ":X2", field = "←" },
["Right Arrow"] = { port = ":X2", field = "→" },
["Return"] = { port = ":X4", field = "Return" },
["Escape"] = { port = ":X4", field = "Esc" },
}
elseif machine.ioport.ports[":kbd:nkbd:keyb_special"] then
-- MAME from 0.284
keyboard = {
["Control"] = { port = ":kbd:nkbd:keyb_special", field = "Ctrl" },
["Shift"] = { port = ":kbd:nkbd:keyb_special", field = "Left Shift" },
["Reset"] = { port = ":kbd:nkbd:keyb_special", field = "Reset" },
["Left Arrow"] = { port = ":kbd:nkbd:X2", field = "Cursor Left" },
["Right Arrow"] = { port = ":kbd:nkbd:X2", field = "Cursor Right" },
["Return"] = { port = ":kbd:nkbd:X4", field = "Return" },
["Escape"] = { port = ":kbd:nkbd:X4", field = "Esc" },
}
else
error(string.format(
"Unable to identify keyboard port for %s - was MAME updated?",
machine.system.name))
end
else
error("Unknown model: " .. machine.system.name)
end
local function EnsureMouse()
if scan_for_mouse then
local mousedev = get_device("^:sl.:mouse$").tag
mouse = {
x = { port = mousedev .. ":a2mse_x", field = "Mouse X" },
y = { port = mousedev .. ":a2mse_y", field = "Mouse Y" },
b = { port = mousedev .. ":a2mse_button", field = "Mouse Button" },
}
scan_for_mouse = false
end
end
--------------------------------------------------
-- General System Configuration
--------------------------------------------------
local function get_port(port_name)
local port = machine.ioport.ports[port_name]
if port == nil then
error("No such port: " .. port_name)
end
return port
end
--[[
`field_name` can be an array, in which case each name is tried
in turn. This can be used to work around field name changes in MAME, etc.
]]
local function get_field(port_name, field_name)
local port = get_port(port_name)
local field
if type(field_name) == "table" then
for i, v in ipairs(field_name) do
field = port.fields[v]
if field ~= nil then
break
end
end
else
field = port.fields[field_name]
end
if field == nil then
error("No field \"" .. field_name .. "\" for port: " .. port_name)
end
return field
end
function apple2.SetSystemConfig(port_name, field_name, mask, value)
local port = get_port(port_name)
local field = get_field(port_name, field_name)
local initial = port:read()
while (port:read() & mask) ~= value do
-- each field toggle advances to the next setting (or wraps)
field:clear_value()
emu.wait(2/60)
field:set_value(1) -- anything
emu.wait(2/60)
if port:read() == initial then
error("Cycled field \"" .. field_name .. "\" on port \"" .. port_name .. "\" without hitting target value")
end
end
end
-- TODO: Come up with better API for this?
--[[
https://github.com/mamedev/mame/blob/master/src/mame/apple/apple2e.cpp
":a2_config"
IIe: bit 4 = "CPU type": 0="Standard", 1="4MHz Zip Chip"
IIe: bit 5 = "Bootup speed": 0="Standard", 1="4MHz"
Laser 128: bit 3 = "Printer Type", 0="Serial", 1="Parallel"
Laser 128: bit 6 = "40/80 Columns", 0="80 columns", 1="40 columns"
Laser 128: ":kbd_lang_select", mask=$FF="Keyboard", 0="QWERTY", 1="DVORAK"
IIc: bit 6 = "40/80 Columns", 0="80 columns", 1="40 columns"
IIc: bit 4 = "CPU type": 0="Standard", 1="4MHz Zip Chip"
IIc: bit 5 = "Bootup speed": 0="Standard", 1="4MHz"
IIgs: bits 0-7 = "CPU type": 0="Standard", 1="7MHz ZipGS", 3="8MHz ZipGS", 5="12MHz ZipGS", 7="16MHz ZipGS"
]]
--------------------------------------------------
-- System Configuration: Video
--------------------------------------------------
-- https://github.com/mamedev/mame/blob/master/src/mame/apple/apple2video.cpp
--[[
For the non-IIgs models:
":a2video:a2_video_config"
field mask
----------------- -----------
"Monitor type" 00_00000111
"Color algorithm" 00_01110000
"Lores artifacts" 00_10000000
"Text color" 11_00000000
]]
local function SetVideoConfig(field_name, value, mask, shift)
apple2.SetSystemConfig(":a2video:a2_video_config", field_name, mask << shift, value << shift)
emu.wait(2/60)
end
for k,v in pairs({
MONITOR_TYPE_COLOR = 0,
MONITOR_TYPE_VIDEO7 = 3, -- not available on IIc
MONITOR_TYPE_BW = 4,
MONITOR_TYPE_GREEN = 5,
MONITOR_TYPE_AMBER = 6,
MONITOR_TYPE_BWNTSC = 7,
ALGORITHM_BWBIAS = 0,
ALGORITHM_COLORBIAS = 1,
ALGORITHM_BOXFILTER = 2,
TEXTCOLOR_MODE_OFF = 0,
TEXTCOLOR_MODE_40COL = 1,
TEXTCOLOR_MODE_ON = 3,
LORES_ARTIFACTS_OFF = 0,
LORES_ARTIFACTS_ON = 1,
}) do apple2[k] = v end
local MONITOR_TYPE_MASK = 0x07
local MONITOR_TYPE_SHIFT = 0
local ALGORITHM_MASK = 0x70
local ALGORITHM_SHIFT = 4
local TEXTCOLOR_MODE_MASK = 0x300
local TEXTCOLOR_MODE_SHIFT = 8
local LORES_ARTIFACTS_MASK = 1
local LORES_ARTIFACTS_SHIFT = 7
function apple2.SetMonitorType(mode)
SetVideoConfig("Monitor type", mode, MONITOR_TYPE_MASK, MONITOR_TYPE_SHIFT)
end
function apple2.SetColorAlgorithm(mode)
SetVideoConfig("Color algorithm", mode, ALGORITHM_MASK, ALGORITHM_SHIFT)
end
function apple2.SetLoresArtifacts(mode)
SetVideoConfig("Lores artifacts", mode, LORES_ARTIFACTS_MASK, LORES_ARTIFACTS_SHIFT)
end
function apple2.SetTextColorMode(mode)
SetVideoConfig("Text color", mode, TEXTCOLOR_MODE_MASK, TEXTCOLOR_MODE_SHIFT)
end
--------------------------------------------------
-- Keyboard Input
--------------------------------------------------
local kbd = machine.natkeyboard
local function wait_for_kbd_strobe_clear()
while apple2.ReadSSW("KBD") > 127 do
emu.wait(1/60)
end
end
-- https://docs.mamedev.org/luascript/ref-input.html#natural-keyboard-manager
function apple2.Type(sequence)
for i=1,sequence:len() do
kbd:post(sequence:sub(i,i))
while kbd.is_posting do
emu.wait(1/60)
end
wait_for_kbd_strobe_clear()
end
end
function apple2.TypeLine(sequence)
apple2.Type(sequence)
apple2.ReturnKey()
end
local function press(k)
if keyboard[k] == nil then
error(string.format("No key binding for %q on system %q", k, machine.system.name))
end
get_field(keyboard[k].port, keyboard[k].field):set_value(1)
end
local function release(k)
if keyboard[k] == nil then
error(string.format("No key binding for %q on system %q", k, machine.system.name))
end
get_field(keyboard[k].port, keyboard[k].field):clear_value()
end
local function press_and_release(k)
press(k)
emu.wait(2/60)
release(k)
emu.wait(2/60)
end
function apple2.IsCapsLockOn()
local key = keyboard["Caps Lock"]
if key == nil then
return false -- Apple ][+ compat
end
local bits = get_port(key.port):read()
return (bits & key.bits) ~= 0
end
function apple2.ToggleCapsLock()
press_and_release("Caps Lock")
end
function apple2.CapsLockOn()
if not apple2.IsCapsLockOn() then
apple2.ToggleCapsLock()
end
end
function apple2.CapsLockOff()
if apple2.IsCapsLockOn() then
apple2.ToggleCapsLock()
end
end
function apple2.ControlReset()
apple2.PressControl()
press("Reset")
emu.wait(2/60)
release("Reset")
apple2.ReleaseControl()
emu.wait(1/60)
end
function apple2.ControlOAReset()
press("Open Apple")
apple2.ControlReset()
release("Open Apple")
end
function apple2.ControlKey(key)
apple2.PressControl()
apple2.Type(key)
apple2.ReleaseControl()
end
function apple2.SpaceKey()
apple2.Type(" ")
end
function apple2.TabKey()
press_and_release("Tab")
wait_for_kbd_strobe_clear()
end
function apple2.ReturnKey()
press_and_release("Return")
wait_for_kbd_strobe_clear()
end
function apple2.DeleteKey()
press_and_release("Delete")
wait_for_kbd_strobe_clear()
end
function apple2.EscapeKey()
press_and_release("Escape")
wait_for_kbd_strobe_clear()
end
function apple2.LeftArrowKey()
press_and_release("Left Arrow")
wait_for_kbd_strobe_clear()
end
function apple2.RightArrowKey()
press_and_release("Right Arrow")
wait_for_kbd_strobe_clear()
end
function apple2.UpArrowKey()
press_and_release("Up Arrow")
wait_for_kbd_strobe_clear()
end
function apple2.DownArrowKey()
press_and_release("Down Arrow")
wait_for_kbd_strobe_clear()
end
function apple2.PressOA()
press("Open Apple")
emu.wait(1/60)
end
function apple2.ReleaseOA()
release("Open Apple")
emu.wait(1/60)
end
function apple2.PressSA()
press("Solid Apple")
emu.wait(1/60)
end
function apple2.ReleaseSA()
release("Solid Apple")
emu.wait(1/60)
end
function apple2.PressControl()
press("Control")
emu.wait(1/60)
end
function apple2.ReleaseControl()
release("Control")
emu.wait(1/60)
end
function apple2.PressShift()
press("Shift")
emu.wait(1/60)
end
function apple2.ReleaseShift()
release("Shift")
emu.wait(1/60)
end
function apple2.OAKey(key)
apple2.PressOA()
apple2.Type(key)
emu.wait(1/60)
apple2.ReleaseOA()
end
function apple2.SAKey(key)
apple2.PressSA()
apple2.Type(key)
emu.wait(1/60)
apple2.ReleaseSA()
end
function apple2.OASAKey(key)
apple2.PressOA()
apple2.PressSA()
apple2.Type(key)
emu.wait(1/60)
apple2.ReleaseSA()
apple2.ReleaseOA()
end
--------------------------------------------------
-- Joystick/Paddles
--------------------------------------------------
-- NOTE: On `apple2e` (etc) need `-gameio joy` or equivalent to enable
function apple2.PressJoyButton1()
local buttons = get_port(":gameio:joy:joystick_buttons")
if not buttons then
error("No joystick ports present")
end
buttons.fields["P1 Button 1"]:set_value(1)
emu.wait(1/10)
end
function apple2.ReleaseJoyButton1()
local buttons = get_port(":gameio:joy:joystick_buttons")
if not buttons then
error("No joystick ports present")
end
buttons.fields["P1 Button 1"]:clear_value()
emu.wait(1/10)
end
function apple2.SetJoy1(x,y)
local xport = machine.ioport.ports[":gameio:joy:joystick_1_x"]
local yport = machine.ioport.ports[":gameio:joy:joystick_1_y"]
if not xport or not yport then
error("No joystick ports present")
end
xport.fields["P1 Joystick X"]:set_value(x)
yport.fields["P1 Joystick Y"]:set_value(y)
emu.wait(1/10)
end
function apple2.SetJoy2(x,y)
local xport = machine.ioport.ports[":gameio:joy:joystick_2_x"]
local yport = machine.ioport.ports[":gameio:joy:joystick_2_y"]
if not xport or not yport then
error("No joystick ports present")
end
xport.fields["P2 Joystick X"]:set_value(x)
yport.fields["P2 Joystick Y"]:set_value(y)
emu.wait(1/10)
end
--------------------------------------------------
-- Mouse
--------------------------------------------------
local function clamp(n, min, max)
if n < min then
return min
elseif n > max then
return max
else
return n
end
end
-- Can't move mouse more than 127 at a time, so track current position
local mouse_x = 0
local mouse_y = 0
-- Just updates our notion of where the mouse is
function apple2.ResetMouse()
mouse_x = 0
mouse_y = 0
end
function apple2.PressMouseButton()
EnsureMouse()
local field = get_field(mouse.b.port, mouse.b.field)
field:set_value(1)
emu.wait(1/60)
end
function apple2.ReleaseMouseButton()
EnsureMouse()
local field = get_field(mouse.b.port, mouse.b.field)
field:clear_value()
emu.wait(1/60)
end
function apple2.ClickMouseButton()
apple2.PressMouseButton()
apple2.ReleaseMouseButton()
end
function apple2.DoubleClickMouseButton()
apple2.ClickMouseButton()
emu.wait(10/60)
apple2.ClickMouseButton()
end
function apple2.MoveMouse(x, y)
--[[
reading:
# maxes out at 256
machine.ioport.ports[mouse.x.port]:read()
machine.ioport.ports[mouse.y.port]:read()
# this is low level hardware... need to snoop at higher level
# maybe screen holes?
# but there's also scaling. Ugh.
]]
EnsureMouse()
local MAX_DELTA = 127
repeat
local delta_x = clamp(x - mouse_x, -MAX_DELTA, MAX_DELTA)
if delta_x ~= 0 then
mouse_x = mouse_x + delta_x
get_field(mouse.x.port, mouse.x.field):set_value(mouse_x)
emu.wait(10/60)
-- print(machine.ioport.ports[mouse.x.port]:read())
end
until delta_x == 0
emu.wait(1)
repeat
local delta_y = clamp(y - mouse_y, -MAX_DELTA, MAX_DELTA)
if delta_y ~= 0 then
mouse_y = mouse_y + delta_y
get_field(mouse.y.port, mouse.y.field):set_value(mouse_y)
emu.wait(10/60)
-- print(machine.ioport.ports[mouse.y.port]:read())
end
until delta_y == 0
emu.wait(1)
end
--------------------------------------------------
-- Memory
--------------------------------------------------
-- This is a "hardware" view of memory; i.e. the MMU/softswitch states
-- are ignored, which is useful as the banking configuration changes
-- constantly. Instead, call with explicit bank selection in the
-- address.
-- Note that internally MAME models LC memory on the IIe as:
-- * $D000/1 is presented at $C000
-- * $D000/2 is presented at $D000
-- * $E000 is presented at $F000
-- * $F000 is presented at $E000
-- For Apple IIe devices, Auxiliary RAM is a separate device. For
-- others, it just exists at 0x10000 and up.
--
-- The IIgs swaps E/F, but not the two D banks!
-- The ReadRAMDevice/WriteRAMDevice calls normalize this; call with
-- 0x0nnnn to access Main RAM and 0x1nnnn to acces Aux RAM. For
-- LCBANK2 use 0xbCnnnn instead of 0xDnnnn.
-- RAM should be device tag ":ram" item name "0/m_pointer" but
-- MAME 0.284 introduced https://github.com/mamedev/mame/issues/14762
-- so hunt for it using pattern matching.
local ram
for i,_ in pairs(machine.devices[":ram"].items) do
if i:match("0/.*m_pointer") then
ram = emu.item(machine.devices[":ram"].items[i])
break
end
end
local function swizzle(addr)
-- Assume LCBANK1 is desired
if machine.system.name:match("^apple2gs") then
if (addr & 0xE000) == 0xE000 then
return addr ~ 0x1000
end
elseif (addr & 0xC000) == 0xC000 then
return addr ~ 0x1000
end
--print("not swizzled")
return addr
end
function apple2.ReadRAMDevice(addr)
addr = swizzle(addr)
-- Apple IIe exposes Aux RAM as a separate device
if addr >= 0x10000 and auxram ~= nil then
return auxram:read(addr - 0x10000)
else
return ram:read(addr)
end
end
function apple2.WriteRAMDevice(addr, value)
addr = swizzle(addr)
-- Apple IIe exposes Aux RAM as a separate device
if addr >= 0x10000 and auxram ~= nil then
auxram:write(addr - 0x10000, value)
else
ram:write(addr, value)
end
end
function apple2.GetRAMDeviceProxy()
local function read_u8(addr)
return apple2.ReadRAMDevice(addr)
end
local function write_u8(addr, value)
return apple2.WriteRAMDevice(addr, value)
end
local function read_u16(addr)
return read_u8(addr) | (read_u8(addr+1) << 8)
end
local function read_s16(addr)
local v = read_u16(addr)
if v & 0x8000 == 0 then
return v
else
return v - 0x10000
end
end
return {
read_u8 = read_u8,
read_u16 = read_u16,
read_s16 = read_s16,
write_u8 = write_u8,
}
end
--------------------------------------------------
-- Machine State
--------------------------------------------------
-- This is a "software" view of memory; i.e. the MMU/softswitch states
-- determines which banks (Main vs. Aux, LCBANK1 vs. LCBANK2 vs. ROM)
-- are accessed
-- Most useful for reading/writing softswitches
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
function apple2.ReadMemory(addr)
return mem:read_u8(addr)
end
function apple2.WriteMemory(addr, value)
return mem:write_u8(addr, value)
end
--[[
Softswitches, based on system_class (O = apple2, E = apple2e, C = apple2c, G = apple2gs)
]]
local ssw_definitions = {
{ "KBD" , "OECG", 0xC000 }, -- (R) keyboard
{ "CLR80STORE" , ".ECG", 0xC000 }, -- (W) restore normal PAGE2 control
{ "SET80STORE" , ".ECG", 0xC001 }, -- (W) cause PAGE2 to bank display memory
{ "RAMRDOFF" , ".ECG", 0xC002 }, -- (W) Read from main 48K RAM ($200-$BFFF)
{ "RAMRDON" , ".ECG", 0xC003 }, -- (W) Read from auxiliary 48K RAM ($200-$BFFF)
{ "RAMWRTOFF" , ".ECG", 0xC004 }, -- (W) Write to main 48K RAM ($200-$BFFF)
{ "RAMWRTON" , ".ECG", 0xC005 }, -- (W) Write to auxiliary 48K RAM ($200-$BFFF)
{ "SETSLOTCXROM" , ".E.G", 0xC006 }, -- (W) Bank in slot ROM in $C100-$CFFF (IIe/IIgs)
{ "SETINTCXROM" , ".E.G", 0xC007 }, -- (W) Bank in internal ROM in $C100-$CFFF (IIe/IIgs)
{ "ALTZPOFF" , ".ECG", 0xC008 }, -- (W) Use main zero page/stack/LC
{ "ALTZPON" , ".ECG", 0xC009 }, -- (W) Use aux zero page/stack/LC
{ "SETINTC3ROM" , ".E.G", 0xC006 }, -- (W) ROM in Slot 3 (IIe/IIgs)
{ "SETSLOTC3ROM" , ".E.G", 0xC007 }, -- (W) ROM in Aux Slot (IIe/IIgs)
{ "CLR80VID" , ".ECG", 0xC00C }, -- (W) Disable 80-column hardware
{ "SET80VID" , ".ECG", 0xC00D }, -- (W) Enable 80-column hardware
{ "CLRALTCHAR" , ".ECG", 0xC00E }, -- (W) Primary character set
{ "SETALTCHAR" , ".ECG", 0xC00F }, -- (W) Alternate character set (MouseText)
{ "KBDSTRB" , "OECG", 0xC010 }, -- (W) clear keyboard strobe
{ "AKD" , ".ECG", 0xC010 }, -- (R7) Any key down / clear strobe
{ "RDLCBNK2" , ".ECG", 0xC011 }, -- (R7) bit 7=1 if LCBANK2 enabled
{ "RDLCRAM" , ".ECG", 0xC012 }, -- (R7) bit 7=1 if LC RAM (0=ROM)
{ "RDRAMRD" , ".ECG", 0xC013 }, -- (R7) bit 7=1 if reading auxiliary RAM
{ "RDRAMWRT" , ".ECG", 0xC014 }, -- (R7) bit 7=1 if writing auxiliary RAM
{ "RDCXROM" , ".E.G", 0xC015 }, -- (R7) (IIe/IIgs)
{ "RSTXINT" , "..C.", 0xC015 }, -- (R) Reset Mouse X0 Interrupt (IIc)
{ "RDALTZP" , ".ECG", 0xC016 }, -- (R7) bit 7=1 if auxiliary ZP/stack/LC enabled
{ "RDC3ROM" , ".E.G", 0XC017 }, -- (R7) (IIe/IIgs)
{ "RSTYINT" , "..C.", 0XC017 }, -- (R) Reset Mouse Y0 Interrupt (IIc)
{ "RD80STORE" , ".ECG", 0xC018 }, -- (R7) bit 7=1 if 80STORE enabled
{ "RDVBL" , ".E.G", 0xC019 }, -- (R7) Vertical blanking
{ "RSTVBL" , "..C.", 0xC019 }, -- (R7) See if VBlInt off; reset it
{ "RDTEXT" , ".ECG", 0xC01A }, -- (R7) bit 7=1 if text
{ "RDMIXED" , ".ECG", 0xC01B }, -- (R7) bit 7=1 if mixed
{ "RDPAGE2" , ".ECG", 0xC01C }, -- (R7) bit 7=1 if PAGE2 on
{ "RDHIRES" , ".ECG", 0xC01D }, -- (R7) bit 7=1 if high-resolution on
{ "RDALTCHAR" , ".ECG", 0xC01E }, -- (R7) bit 7=1 if ALTCHAR on
{ "RD80VID" , ".ECG", 0xC01F },
{ "MONOCOLOR" , "...G", 0xC021 }, -- IIgs - bit 7=1 switches composite to mono
{ "TBCOLOR" , "...G", 0xC022 }, -- IIgs - text foreground/background colors
{ "KEYMODREG" , "...G", 0xC025 }, -- IIgs - keyboard modifiers
{ "NEWVIDEO" , "...G", 0xC029 }, -- IIgs - new video modes
{ "MACIIE" , "....", 0xC02B }, -- Macintosh IIe Option Card
{ "SPKR" , "OECG", 0xC030 },
{ "CLOCKCTL" , "...G", 0xC034 }, -- IIgs
{ "SHADOW" , "...G", 0xC035 }, -- IIgs
{ "TXTCLR" , "OECG", 0xC050 }, -- (R/W) Graphics
{ "TXTSET" , "OECG", 0xC051 }, -- (R/W) Text
{ "MIXCLR" , "OECG", 0xC052 }, -- (R/W) Fullscreen
{ "MIXSET" , "OECG", 0xC053 }, -- (R/W) Mixed screen
{ "PAGE2OFF" , "OECG", 0xC054 }, -- (R/W) Page 1
{ "PAGE2ON" , "OECG", 0xC055 }, -- (R/W) Page 2
{ "HIRESOFF" , "OECG", 0xC056 }, -- (R/W) High resolution graphics
{ "HIRESON" , "OECG", 0xC057 }, -- (R/W) Low resolution graphics
{ "AN0_OFF" , "OE.G", 0xC058 }, -- (W)
{ "AN0_ON" , "OE.G", 0xC059 }, -- (W)
{ "AN1_OFF" , "OE.G", 0xC05A }, -- (W)
{ "DISVBL" , "..C.", 0xC05A }, -- (W) Disable VBL interrupts (IIc)
{ "AN1_ON" , "OE.G", 0xC05B }, -- (W)
{ "ENVBL" , "..C.", 0xC05B }, -- (W) Enable VBL interrupts (IIc)
{ "AN2_OFF" , "OE.G", 0xC05C }, -- (W)
{ "AN2_ON" , "OE.G", 0xC05D }, -- (W)
{ "AN3_OFF" , "OE.G", 0xC05E }, -- (W)
{ "AN3_ON" , "OE.G", 0xC05F }, -- (W)
{ "DHIRESON" , ".ECG", 0xC05E }, -- (W) Double high resolution graphics on
{ "DHIRESOFF" , ".ECG", 0xC05F }, -- (W) Double high resolution graphics off
{ "RD80SW" , "..C.", 0xC060 }, -- (R7) See if 80/40 switch down (IIc)
{ "BUTN0" , "OECG", 0xC061 }, -- (R7)
{ "BUTN1" , "OECG", 0xC062 }, -- (R7)
{ "BUTN2" , "OE.G", 0xC063 }, -- (R7)
{ "PDL0" , "OECG", 0xC064 }, -- (R)
{ "PDL1" , "OECG", 0xC065 }, -- (R)
{ "PDL2" , "OE.G", 0xC066 }, -- (R)
{ "PDL3" , "OE.G", 0xC067 }, -- (R)
{ "STATEREG" , "...G", 0xC068 }, -- Mega II chip (IIgs, etc)
{ "PTRIG" , "OECG", 0xC070 },
{ "RAMWORKS_BANK" , ".EC.", 0xC073 },
{ "LASER128EX_CFG" , "....", 0xC074 }, -- high two bits control speed
{ "IOUDISON" , "..C.", 0xC07E }, -- (W) Disable IOU access (IIc)
{ "RDIOUDIS" , "..C.", 0xC07E }, -- (R7) Read IOUDIS switch (1=on) (IIc)
{ "IOUDISOFFF" , "..C.", 0xC07F }, -- (W) Enable IOU access (IIc)
{ "RDDHIRES" , "..C.", 0xC07F }, -- (R7) Read DHIRES switch (0=on) (IIc)
{ "ROMIN2" , "OECG", 0xC082 }, -- (W) Read ROM; no write
}
local ssw = {}
for _, entry in ipairs(ssw_definitions) do
local name, systems, value = table.unpack(entry)
if system_class == "apple2" and systems:match("O") or
system_class == "apple2e" and systems:match("E") or
system_class == "apple2c" and systems:match("C") or
system_class == "apple2gs" and systems:match("G") then
ssw[name] = value
end
end
function apple2.ReadSSW(symbol)
if ssw[symbol] == nil then
error(string.format("No softswitch %q on system %q", symbol, machine.system.name))
end
return apple2.ReadMemory(ssw[symbol])
end
function apple2.WriteSSW(symbol, value)
if ssw[symbol] == nil then
error(string.format("No softswitch %q on system %q", symbol, machine.system.name))
end
apple2.WriteMemory(ssw[symbol], value)
end
--------------------------------------------------
-- Misc Utilities
--------------------------------------------------
-- Address must include bank offset
function apple2.GetPascalString(addr)
-- NOTE: Doesn't handle non-ASCII encodings
local len = apple2.ReadRAMDevice(addr)
local str = ""
for i = 1,len do
str = str .. string.char(apple2.ReadRAMDevice(addr+i))
end
return str
end
local function GetDHRByteAddress(row, col)
local bank = col % 2
col = col >> 1
local aa = (row & 0xC0) >> 6
local bbb = (row & 0x38) >> 3
local ccc = (row & 0x07)
return bank, 0x2000 + (aa * 0x28) + (bbb * 0x80) + (ccc * 0x400) + col
end
function apple2.GetDHRByte(row, col, ram)
ram = ram or apple2.GetRAMDeviceProxy()
local bank, addr = GetDHRByteAddress(row, col)
return ram.read_u8(addr + 0x10000 * (1-bank)) & 0x7F
end
function apple2.SetDHRByte(row, col, value)
local bank, addr = GetDHRByteAddress(row, col)
apple2.WriteRAMDevice(addr + 0x10000 * (1-bank), value)
end
local function IterateTextScreen(char_cb, row_cb)
local is80 = system_class ~= "apple2" and apple2.ReadSSW("RD80VID") > 127
local isAlt = system_class ~= "apple2" and apple2.ReadSSW("RDALTCHAR") > 127
for row = 0,23 do
local base = 0x400 + (row - math.floor(row/8) * 8) * 0x80 + 40 * math.floor(row/8)
for col = 0,39 do
if is80 then
char_cb(apple2.ReadRAMDevice(0x10000 + base + col), isAlt)
end
char_cb(apple2.ReadRAMDevice(base + col), isAlt)
end
row_cb()
end
end
function apple2.GrabTextScreen()
-- Apple IIe and later, non-MouseText
-- 0x00-0x1F: Inverse @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
-- 0x20-0x3F: Inverse !"#$%&'()*+,-./0123456789:;<=>?@
-- 0x40-0x5F: Flashing @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ (ALTCHAR - MouseText)
-- 0x60-0x7F: Flashing !"#$%&'()*+,-./0123456789:;<=>?@ (ALTCHAR - inverse lower)
-- 0x80-0x9F: Normal @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
-- 0xA0-0xBF: Normal !"#$%&'()*+,-./0123456789:;<=>?@
-- 0xC0-0xDF: Normal @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
-- 0xE0-0xFF: Normal `abcdefghijklmnopqrstuvwxyz{|}~ `
local screen = ""
IterateTextScreen(
function(byte, isAlt)
if byte < 0x20 then -- 0x00-0x1F: inverse upper
byte = byte + 0x40
elseif byte < 0x40 then -- 0x20-0x3F: inverse punctuation
byte = byte
elseif byte < 0x60 then -- 0x40-0x5F: flashing upper / MouseText
if not isAlt then
byte = byte
else
byte = 0x20 -- TODO: MouseText mapping
end
elseif byte < 0x80 then -- 0x60-0x7F: flashing punctuation / inverse lower
if not isAlt then
byte = byte - 0x40
else
byte = byte
end
elseif byte < 0xA0 then -- 0x80-0x9F: normal upper
byte = byte - 0x40
else -- 0xA0-0xFF: normal punctuation / upper / lower
byte = byte - 0x80
end
screen = screen .. string.format("%c", byte)
end,
function()
screen = screen .. "\n"
end)
return screen
end
function apple2.GrabInverseText()
local str = ""
IterateTextScreen(
function(byte, isAlt)
if byte < 0x20 then -- 0x00-0x1F: inverse upper
byte = byte + 0x40
elseif byte < 0x40 then -- 0x20-0x3F: inverse punctuation
byte = byte
elseif byte < 0x60 then -- 0x40-0x5F: flashing upper / alt: MouseText
return
elseif byte < 0x80 then -- 0x60-0x7F: flashing punctuation / alt: inverse lower
if not isAlt then
return
else
byte = byte
end
elseif byte < 0xA0 then -- normal upper
return
else -- normal punctuation / upper / lower
return
end
str = str .. string.format("%c", byte)
end,
function()
end)
return str
end
function apple2.SnapshotDHR()
local ram = apple2.GetRAMDeviceProxy()
local bytes = {}
for row = 0,apple2.SCREEN_HEIGHT-1 do
for col = 0,apple2.SCREEN_COLUMNS-1 do
bytes[row*apple2.SCREEN_COLUMNS+col] = apple2.GetDHRByte(row, col, ram)
end
end
return bytes
end
--------------------------------------------------
function apple2.IsCrashedToMonitor()
local cpu = manager.machine.devices[":maincpu"]
local sp = cpu.state.SP.value
if sp == 0x1FE and apple2.ReadMemory(sp) == 0x4E and apple2.ReadMemory(sp+1) == 0xEB then
return true
else
return false
end
end
--------------------------------------------------
function apple2.WaitForBitsy(options)
util.WaitFor(
"Bitsy Bye",
function()
local text = apple2.GrabTextScreen()
return text:match("BITSY") and text:match("^([^:]+):(/[^\n ]+)")
end,
options)
end
function apple2.GetBitsyLocation()
local slot_drive, path = apple2.GrabTextScreen():match("^([^:]+):(/[^\n ]+)")
return slot_drive, path
end
function apple2.BitsySelectSlotDrive(sd)
local slot_drive, path = apple2.GetBitsyLocation()
while path:match("^/.*/") do
apple2.EscapeKey()
emu.wait(5)
slot_drive, path = apple2.GetBitsyLocation()
end
while slot_drive ~= sd do
apple2.ControlKey("I") -- for Apple ][+ compat
emu.wait(5)
slot_drive, path = apple2.GetBitsyLocation()
end
end
function apple2.BitsySelectVolume(name)
local slot_drive, path = apple2.GetBitsyLocation()
while path:match("^/.*/") do
apple2.EscapeKey()
emu.wait(5)
slot_drive, path = apple2.GetBitsyLocation()
end
while path ~= "/" .. name do
apple2.ControlKey("I") -- for Apple ][+ compat
emu.wait(5)
slot_drive, path = apple2.GetBitsyLocation()
end
end
function apple2.BitsyInvokePath(path)
local segments = {}
for segment in path:gmatch("([^/]+)") do
table.insert(segments, segment)
end
-- volume
apple2.BitsySelectVolume(segments[1])
-- directories
for i = 2, #segments-1 do
apple2.BitsyInvokeFile("/" .. segments[i])
end
-- file
apple2.BitsyInvokeFile(segments[#segments])
end
function apple2.BitsyInvokeFile(name)
while apple2.GrabInverseText() ~= name do
apple2.RightArrowKey() -- for Apple ][+ compat
emu.wait(1)
end
apple2.ReturnKey()
end
--------------------------------------------------
function apple2.WaitForBasicSystem(options)
util.WaitFor(
"Basic System",
function()
return apple2.GrabTextScreen():match("PRODOS BASIC")
end,
options)
end
--------------------------------------------------
function apple2.IsMono()
--[[
IIgs border is 72 (left/right) x 40 (top/bottom) ... but the edge
pixels are blurred. So make the border black.
TODO: Something less hacky than this
]]
if manager.machine.system.name:match("^apple2gs") then
apple2.WriteSSW("CLOCKCTL", 0)
emu.wait(2/60)
end
emu.wait_next_frame()
-- https://docs.mamedev.org/luascript/ref-core.html#video-manager
local bytes = manager.machine.video:snapshot_pixels()
local width, height = manager.machine.video:snapshot_size()
function pixel(x,y)
local a = string.byte(bytes, (x + y * width) * 4 + 0)
local b = string.byte(bytes, (x + y * width) * 4 + 1)
local g = string.byte(bytes, (x + y * width) * 4 + 2)
local r = string.byte(bytes, (x + y * width) * 4 + 3)
return r,g,b,a
end
local fr,fg,fb = pixel(0,0)
for y = 0, height-1 do
for x = 0, width-1 do
local r,g,b,a = pixel(x,y)
if r ~= g or r ~=b then
return false
end
end
end
return true
end
function apple2.IsColor()
return not apple2.IsMono()
end
--------------------------------------------------
-- ProDOS Date/Time
--------------------------------------------------
local DATELO, DATEHI, TIMELO, TIMEHI = 0xBF90, 0xBF91, 0xBF92, 0xBF93
function apple2.SetProDOSDate(y,m,d)
local hi = (y % 100) << 1 | (m >> 3)
local lo = (m << 5) | d
local ram = apple2.GetRAMDeviceProxy()
ram.write_u8(DATELO, lo)
ram.write_u8(DATEHI, hi)
end
function apple2.GetProDOSDate()
local word = apple2.GetRAMDeviceProxy().read_u16(DATELO)
local y = (word >> 9) & 0x7F
local m = (word >> 5) & 0xF
local d = word & 0x1F
-- https://prodos8.com/docs/technote/28/
if y < 40 then
y = y + 2000
else
y = y + 1900
end
return y,m,d
end
function apple2.SetProDOSTime(h, m)
local ram = apple2.GetRAMDeviceProxy()
apple2.WriteRAMDevice(TIMELO, m)
apple2.WriteRAMDevice(TIMEHI, h)
end
function apple2.GetProDOSTime()
local m = apple2.ReadRAMDevice(TIMELO) & 0x3F
local h = apple2.ReadRAMDevice(TIMEHI) & 0x1F
return h,m
end
--------------------------------------------------
-- ProDOS Devices
--------------------------------------------------
function apple2.GetProDOSDeviceList()
local DEVCNT = 0xBF31
local DEVLST = 0xBF32
local count = apple2.ReadRAMDevice(DEVCNT) + 1
local list = {}
for i = 0, count-1 do
local unit = apple2.ReadRAMDevice(DEVLST + i)
local slot = (unit & 0x70) >> 4
local drive = ((unit & 0x80) >> 7) + 1
table.insert(list, {slot=slot, drive=drive})
end
return list
end
--------------------------------------------------
return apple2