--[[============================================================ 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