nonogamer9

3C8: A Chip-8 Emulator For ComputerCraft

May 10th, 2025 (edited)
172
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 33.97 KB | Gaming | 0 0
  1. local WIDTH, HEIGHT = 64, 32
  2. local FONT_START = 0x50
  3. local PROGRAM_START = 0x200
  4. local VERSION = "V 1.0.5"
  5. local FONT = {
  6.   0xF0,0x90,0x90,0x90,0xF0, 0x20,0x60,0x20,0x20,0x70, 0xF0,0x10,0xF0,0x80,0xF0, 0xF0,0x10,0xF0,0x10,0xF0,
  7.   0x90,0x90,0xF0,0x10,0x10, 0xF0,0x80,0xF0,0x10,0xF0, 0xF0,0x80,0xF0,0x90,0xF0, 0xF0,0x10,0x20,0x40,0x40,
  8.   0xF0,0x90,0xF0,0x90,0xF0, 0xF0,0x90,0xF0,0x10,0xF0, 0xF0,0x90,0xF0,0x90,0x90, 0xE0,0x90,0xE0,0x90,0xE0,
  9.   0xF0,0x80,0x80,0x80,0xF0, 0xE0,0x90,0x90,0x90,0xE0, 0xF0,0x80,0xF0,0x80,0xF0, 0xF0,0x80,0xF0,0x80,0x80
  10. }
  11. local KEYPAD_MAP = {
  12.   ["1"]=0x1, ["2"]=0x2, ["3"]=0x3, ["4"]=0xC,
  13.   q=0x4, w=0x5, e=0x6, r=0xD,
  14.   a=0x7, s=0x8, d=0x9, f=0xE,
  15.   z=0xA, x=0x0, c=0xB, v=0xF
  16. }
  17. local COLOR_LIST = {colors.white, colors.lightGray, colors.gray, colors.black}
  18. local COLOR_NAMES = {"White", "LightGray", "Gray", "Black"}
  19. for _,c in ipairs{colors.red, colors.orange, colors.yellow, colors.lime, colors.green, colors.cyan, colors.lightBlue, colors.blue, colors.purple, colors.magenta, colors.pink, colors.brown} do
  20.   table.insert(COLOR_LIST, c)
  21. end
  22. for _,n in ipairs{"Red","Orange","Yellow","Lime","Green","Cyan","LightBlue","Blue","Purple","Magenta","Pink","Brown"} do
  23.   table.insert(COLOR_NAMES, n)
  24. end
  25. local function color_index(c)
  26.   for i,v in ipairs(COLOR_LIST) do if v==c then return i end end
  27.   return 1
  28. end
  29. local CC_PALETTE = {
  30.   [colors.white]     = {0xF0, 0xF0, 0xF0},
  31.   [colors.orange]    = {0xF2, 0xB2, 0x2E},
  32.   [colors.magenta]   = {0xC7, 0x4E, 0xBD},
  33.   [colors.lightBlue] = {0x3A, 0xA2, 0xD8},
  34.   [colors.yellow]    = {0xDE, 0xD6, 0x2C},
  35.   [colors.lime]      = {0x41, 0xCD, 0x34},
  36.   [colors.pink]      = {0xD8, 0x4B, 0xA7},
  37.   [colors.gray]      = {0x4F, 0x4F, 0x4F},
  38.   [colors.lightGray] = {0x99, 0x99, 0x99},
  39.   [colors.cyan]      = {0x16, 0x8A, 0x9D},
  40.   [colors.purple]    = {0x7E, 0x3E, 0x9D},
  41.   [colors.blue]      = {0x2E, 0x32, 0x99},
  42.   [colors.brown]     = {0x4F, 0x32, 0x14},
  43.   [colors.green]     = {0x35, 0x6E, 0x19},
  44.   [colors.red]       = {0xB2, 0x1A, 0x21},
  45.   [colors.black]     = {0x1D, 0x1D, 0x21},
  46. }
  47. local SOUND_MODES = {"Off", "Minecraft", "Beeper"}
  48. local function sound_mode_name(idx) return SOUND_MODES[idx] or "Off" end
  49. local SPEEDS = {500, 1000, 2000, 4000, 8000, 10000}
  50. local SPEED_NAMES = {"500Hz", "1000Hz", "2000Hz (Normal)", "4000Hz", "8000Hz", "10000Hz"}
  51. local function speed_name(idx) return SPEED_NAMES[idx] or tostring(SPEEDS[idx]).."Hz" end
  52. local PALETTE_MONO = {
  53.   bg = colors.black, fg = colors.white, box = colors.gray,
  54.   hilite = colors.white, hilitefg = colors.black, btn = colors.gray,
  55. }
  56. local PALETTE_COLOR = {
  57.   bg = colors.blue,
  58.   fg = colors.white,
  59.   box = colors.cyan,
  60.   hilite = colors.cyan,
  61.   hilitefg = colors.black,
  62.   btn = colors.cyan,
  63. }
  64. local function isColor()
  65.   return term.isColor and term.isColor()
  66. end
  67.  
  68. local function isVanillaCC()
  69.     local ok, f = pcall(fs.open, "__cc_vanilla_test__", "rb")
  70.     if ok and f then
  71.         f.close()
  72.         fs.delete("__cc_vanilla_test__")
  73.         return false
  74.     end
  75.     if term.nativePaletteColor then return false end
  76.     if os.version and tostring(os.version()):find("CC:Tweaked") then return false end
  77.     return true
  78. end
  79.  
  80. local PCM_BEEP_FILE = "2obgw2.raw"
  81. local PCM_BEEP_URL = "https://files.catbox.moe/2obgw2.raw"
  82. local pcm_beep_samples = nil
  83. local pcm_beep_pos = 1
  84. local pcm_beep_active = false
  85. local pcm_beep_chunk_size = 800
  86.  
  87. local function ensure_pcm_beep()
  88.     if not fs.exists(PCM_BEEP_FILE) then
  89.         print("Downloading beeper PCM sound...")
  90.         if http and http.get then
  91.             local h = http.get(PCM_BEEP_URL)
  92.             if h then
  93.                 local f = fs.open(PCM_BEEP_FILE, "wb")
  94.                 if f then
  95.                     while true do
  96.                         local b = h.read(32768)
  97.                         if not b then break end
  98.                         f.write(b)
  99.                     end
  100.                     f.close()
  101.                     h.close()
  102.                     print("Downloaded beeper sound.")
  103.                 else
  104.                     print("Failed to open file for writing: "..PCM_BEEP_FILE)
  105.                 end
  106.             else
  107.                 print("Failed to download PCM beep from "..PCM_BEEP_URL)
  108.             end
  109.         else
  110.             print("HTTP API not available! Cannot download PCM beep.")
  111.         end
  112.     end
  113. end
  114.  
  115. local function load_pcm_beep()
  116.     if pcm_beep_samples then return end
  117.     local filename = PCM_BEEP_FILE
  118.     local isUnsigned = true
  119.     local inputSampleRate = 32000
  120.     local outputSampleRate = 48000
  121.     local volume = 1
  122.     local removeDC = false
  123.  
  124.     local file = fs.open(filename, "rb")
  125.     if not file then
  126.         print("Could not open file: " .. filename)
  127.         return
  128.     end
  129.  
  130.     local samples = {}
  131.     local sum, count = 0, 0
  132.  
  133.     while true do
  134.         local byte = file.read()
  135.         if not byte then break end
  136.         if isUnsigned then
  137.             byte = byte - 128
  138.         end
  139.         samples[#samples + 1] = byte
  140.         sum = sum + byte
  141.         count = count + 1
  142.     end
  143.     file.close()
  144.  
  145.     if #samples == 0 then
  146.         print("PCM file is empty or unreadable.")
  147.         return
  148.     end
  149.  
  150.     if removeDC then
  151.         local avg = sum / count
  152.         for i = 1, #samples do
  153.             samples[i] = samples[i] - avg
  154.         end
  155.     end
  156.  
  157.     if inputSampleRate ~= outputSampleRate then
  158.         local resampled = {}
  159.         local ratio = inputSampleRate / outputSampleRate
  160.         for i = 1, math.floor(#samples / ratio) do
  161.             local src = (i - 1) * ratio + 1
  162.             local idx = math.floor(src)
  163.             local frac = src - idx
  164.             local s1 = samples[idx] or 0
  165.             local s2 = samples[idx + 1] or s1
  166.             resampled[i] = s1 + (s2 - s1) * frac
  167.         end
  168.         samples = resampled
  169.     end
  170.  
  171.     for i = 1, #samples do
  172.         local sample = math.floor(samples[i] * volume + 0.5)
  173.         if sample > 127 then sample = 127 end
  174.         if sample < -128 then sample = -128 end
  175.         samples[i] = sample
  176.     end
  177.  
  178.     pcm_beep_samples = samples
  179. end
  180.  
  181. local function start_pcm_beep()
  182.     load_pcm_beep()
  183.     pcm_beep_pos = 1
  184.     pcm_beep_active = true
  185. end
  186.  
  187. local function stop_pcm_beep()
  188.     pcm_beep_active = false
  189. end
  190.  
  191. local function play_pcm_beep_chunk()
  192.     if not pcm_beep_active or not pcm_beep_samples then return end
  193.     local speaker = peripheral.find and peripheral.find("speaker")
  194.     if not speaker then return end
  195.     local chunk = {}
  196.     for i = 1, pcm_beep_chunk_size do
  197.         if pcm_beep_pos > #pcm_beep_samples then
  198.             pcm_beep_active = false
  199.             break
  200.         end
  201.         chunk[i] = pcm_beep_samples[pcm_beep_pos]
  202.         pcm_beep_pos = pcm_beep_pos + 1
  203.     end
  204.     if #chunk > 0 then
  205.         while not speaker.playAudio(chunk) do
  206.             os.pullEvent("speaker_audio_empty")
  207.         end
  208.     end
  209. end
  210.  
  211. local memory, V, I, pc, stack, delay_timer, sound_timer, display, keypad
  212. local function reset_state()
  213.   memory = {}
  214.   for i=0,4095 do memory[i]=0 end
  215.   for i=1,#FONT do memory[FONT_START+i-1]=FONT[i] end
  216.   V = {}
  217.   for i=0,0xF do V[i]=0 end
  218.   I, pc = 0, PROGRAM_START
  219.   stack = {}
  220.   delay_timer, sound_timer = 0, 0
  221.   display = {}
  222.   for y=1,HEIGHT do display[y]={} for x=1,WIDTH do display[y][x]=false end end
  223.   keypad = {}
  224.   for i=0,0xF do keypad[i]=false end
  225. end
  226. local function load_rom(path)
  227.   local f = fs.open(path, "rb")
  228.   if not f then error("ROM not found: "..path) end
  229.   local i=0
  230.   while true do
  231.     local b = f.read()
  232.     if not b then break end
  233.     memory[PROGRAM_START+i] = b
  234.     i = i+1
  235.   end
  236.   f.close()
  237. end
  238. local function get_mosaic_char(p1, p2, p3, p4, p5, p6)
  239.   return string.char(128 + p1 + p2*2 + p3*4 + p4*8 + p5*16 + p6*32)
  240. end
  241. local function draw_rect(x, y, w, h, color)
  242.   term.setBackgroundColor(color)
  243.   for dy=0,h-1 do
  244.     term.setCursorPos(x, y+dy)
  245.     term.write((" "):rep(w))
  246.   end
  247. end
  248. local function draw_text(x, y, text, fg, bg)
  249.   term.setCursorPos(x, y)
  250.   term.setTextColor(fg)
  251.   term.setBackgroundColor(bg)
  252.   term.write(text)
  253. end
  254. local function mouse_in(mx, my, x, y, w, h)
  255.   return mx >= x and mx < x+w and my >= y and my < y+h
  256. end
  257.  
  258. local function rom_menu_ui(rom_dir, use_mono)
  259.   local palette = use_mono and PALETTE_MONO or PALETTE_COLOR
  260.   while true do
  261.     term.setBackgroundColor(palette.bg)
  262.     term.setTextColor(palette.fg)
  263.     term.clear()
  264.     local files = fs.list(rom_dir)
  265.     local roms = {}
  266.     for _,f in ipairs(files) do
  267.       if f:lower():match("%.ch8$") then table.insert(roms, f) end
  268.     end
  269.     table.sort(roms)
  270.     local w, h = term.getSize()
  271.     local menu_w, menu_h = 36, math.min(#roms, 12)+8
  272.     local mx, my = math.floor((w-menu_w)/2)+1, math.floor((h-menu_h)/2)+1
  273.     local sel = 1
  274.     local scroll = 0
  275.     local max_visible = menu_h-8
  276.     local btn_settings = {mx, my+menu_h-5, menu_w, 1}
  277.     local btn_quit = {mx, my+menu_h-4, menu_w, 1}
  278.     local btn_about = {mx, my+menu_h-2, menu_w, 1}
  279.     while true do
  280.       draw_rect(mx, my, menu_w, menu_h, palette.box)
  281.       draw_rect(mx+1, my+1, menu_w-2, menu_h-2, palette.bg)
  282.       draw_text(mx+2, my+1, "CHIP-8 ROMs in: "..rom_dir, palette.fg, palette.bg)
  283.       for i=1,max_visible do
  284.         local idx = i+scroll
  285.         if roms[idx] then
  286.           local fg = (idx==sel) and palette.hilitefg or palette.fg
  287.           local bg = (idx==sel) and palette.hilite or palette.bg
  288.           draw_rect(mx+2, my+1+i, menu_w-4, 1, bg)
  289.           draw_text(mx+3, my+1+i, roms[idx], fg, bg)
  290.         end
  291.       end
  292.       draw_rect(btn_settings[1], btn_settings[2], btn_settings[3], btn_settings[4], palette.btn)
  293.       draw_rect(btn_quit[1], btn_quit[2], btn_quit[3], btn_quit[4], palette.btn)
  294.       draw_rect(btn_about[1], btn_about[2], btn_about[3], btn_about[4], palette.btn)
  295.       draw_text(btn_settings[1]+math.floor((menu_w-11)/2), btn_settings[2], "[ Settings ]", palette.fg, palette.btn)
  296.       draw_text(btn_quit[1]+math.floor((menu_w-7)/2), btn_quit[2], "[ Quit ]", palette.fg, palette.btn)
  297.       draw_text(btn_about[1]+math.floor((menu_w-8)/2), btn_about[2], "[ About ]", palette.fg, palette.btn)
  298.       local e = {os.pullEvent()}
  299.       if e[1] == "mouse_click" then
  300.         local click_x, click_y = e[3], e[4]
  301.         for i=1,max_visible do
  302.           local idx = i+scroll
  303.           if roms[idx] and mouse_in(click_x, click_y, mx+2, my+1+i, menu_w-4, 1) then
  304.             return fs.combine(rom_dir, roms[idx])
  305.           end
  306.         end
  307.         if mouse_in(click_x, click_y, btn_settings[1], btn_settings[2], btn_settings[3], btn_settings[4]) then
  308.           return "settings"
  309.         end
  310.         if mouse_in(click_x, click_y, btn_quit[1], btn_quit[2], btn_quit[3], btn_quit[4]) then
  311.           return nil
  312.         end
  313.         if mouse_in(click_x, click_y, btn_about[1], btn_about[2], btn_about[3], btn_about[4]) then
  314.           return "about"
  315.         end
  316.       elseif e[1] == "key" then
  317.         if e[2] == keys.down then
  318.           sel = math.min(sel+1, #roms)
  319.           if sel > scroll+max_visible then scroll = scroll+1 end
  320.         elseif e[2] == keys.up then
  321.           sel = math.max(sel-1, 1)
  322.           if sel < scroll+1 then scroll = math.max(sel-1, 0) end
  323.         elseif e[2] == keys.enter then
  324.           if roms[sel] then return fs.combine(rom_dir, roms[sel]) end
  325.         elseif e[2] == keys.q then
  326.           return nil
  327.         elseif e[2] == keys.s then
  328.           return "settings"
  329.         elseif e[2] == keys.a then
  330.           return "about"
  331.         end
  332.       end
  333.     end
  334.   end
  335. end
  336.  
  337. local function read_bmp_mosaic(path, pxw, pxh, mono)
  338.   local file = fs.open(path, "rb")
  339.   if not file then return nil end
  340.   local function readu8() return file.read() end
  341.   local function readu16()
  342.     local l = file.read() or 0
  343.     local h = file.read() or 0
  344.     return l + h*256
  345.   end
  346.   local function readu32()
  347.     local l = file.read() or 0
  348.     local h = file.read() or 0
  349.     local i = file.read() or 0
  350.     local j = file.read() or 0
  351.     return l + h*256 + i*65536 + j*16777216
  352.   end
  353.   if file.read() ~= 0x42 or file.read() ~= 0x4D then file.close() return nil end
  354.   file.read(); file.read(); file.read(); file.read()
  355.   file.read(); file.read(); file.read(); file.read()
  356.   local offset = readu32()
  357.   local header_size = readu32()
  358.   local width = readu32()
  359.   local height = readu32()
  360.   file.read(); file.read()
  361.   local bpp = readu16()
  362.   if bpp ~= 24 then file.close() return nil end
  363.   file.read(); file.read(); file.read(); file.read()
  364.   file.read(); file.read(); file.read(); file.read()
  365.   file.read(); file.read(); file.read(); file.read()
  366.   file.read(); file.read(); file.read(); file.read()
  367.   file.read(); file.read(); file.read(); file.read()
  368.   file.seek("set", offset)
  369.   local img = {}
  370.   local pad = (4 - (width*3)%4)%4
  371.   for y=height-1,0,-1 do
  372.     img[y+1] = {}
  373.     for x=1,width do
  374.       local b = readu8() or 0
  375.       local g = readu8() or 0
  376.       local r = readu8() or 0
  377.       img[y+1][x] = {r,g,b}
  378.     end
  379.     for i=1,pad do file.read() end
  380.   end
  381.   file.close()
  382.   local scale_x = width/pxw
  383.   local scale_y = height/pxh
  384.   local work = {}
  385.   for y=1,pxh do
  386.     work[y] = {}
  387.     for x=1,pxw do
  388.       local px = math.floor((x-1)*scale_x)+1
  389.       local py = math.floor((y-1)*scale_y)+1
  390.       local rgb = img[py][px]
  391.       work[y][x] = {rgb[1], rgb[2], rgb[3]}
  392.     end
  393.   end
  394.   local mosaic = {}
  395.   for y=1,pxh do
  396.     mosaic[y] = {}
  397.     for x=1,pxw do
  398.       local old = work[y][x]
  399.       local r, g, b = old[1], old[2], old[3]
  400.       local best, bestc, bestlum = math.huge, colors.white, 255
  401.       if mono then
  402.         local lum = (r+g+b)/3
  403.         bestc = lum > 128 and colors.white or colors.black
  404.         bestlum = bestc == colors.white and 255 or 0
  405.       else
  406.         for _,c in ipairs(COLOR_LIST) do
  407.           local pal = CC_PALETTE[c] or {255,255,255}
  408.           local cr, cg, cb = pal[1], pal[2], pal[3]
  409.           local d = (r-cr)^2 + (g-cg)^2 + (b-cb)^2
  410.           if d < best then best, bestc = d, c; bestlum = (cr+cg+cb)/3 end
  411.         end
  412.       end
  413.       mosaic[y][x] = bestc
  414.       local err_r, err_g, err_b
  415.       if mono then
  416.         local err = ((r+g+b)/3) - bestlum
  417.         err_r, err_g, err_b = err, err, err
  418.       else
  419.         local pal = CC_PALETTE[bestc] or {255,255,255}
  420.         err_r, err_g, err_b = r-pal[1], g-pal[2], b-pal[3]
  421.       end
  422.       local function dither(xo, yo, factor)
  423.         local nx, ny = x+xo, y+yo
  424.         if nx>=1 and nx<=pxw and ny>=1 and ny<=pxh then
  425.           local n = work[ny][nx]
  426.           n[1] = math.max(0, math.min(255, n[1] + err_r * factor))
  427.           n[2] = math.max(0, math.min(255, n[2] + err_g * factor))
  428.           n[3] = math.max(0, math.min(255, n[3] + err_b * factor))
  429.         end
  430.       end
  431.       dither( 1, 0, 7/16)
  432.       dither(-1, 1, 3/16)
  433.       dither( 0, 1, 5/16)
  434.       dither( 1, 1, 1/16)
  435.     end
  436.   end
  437.   return mosaic
  438. end
  439. local function render_bmp_mosaic_highres(path, sx, sy, maxw, maxh, mono)
  440.   local bmp = read_bmp_mosaic(path, maxw*2, maxh*3, mono)
  441.   if not bmp then return end
  442.   for y=1,maxh do
  443.     term.setCursorPos(sx, sy+y-1)
  444.     for x=1,maxw do
  445.       local px = (x-1)*2+1
  446.       local py = (y-1)*3+1
  447.       local p1 = bmp[py]   and bmp[py][px]   or colors.black
  448.       local p2 = bmp[py]   and bmp[py][px+1] or colors.black
  449.       local p3 = bmp[py+1] and bmp[py+1][px] or colors.black
  450.       local p4 = bmp[py+1] and bmp[py+1][px+1] or colors.black
  451.       local p5 = bmp[py+2] and bmp[py+2][px] or colors.black
  452.       local p6 = bmp[py+2] and bmp[py+2][px+1] or colors.black
  453.       if p1==p2 and p1==p3 and p1==p4 and p1==p5 and p1==p6 then
  454.         term.setBackgroundColor(p1)
  455.         term.write(" ")
  456.       else
  457.         local count = {}
  458.         for _,v in ipairs{p1,p2,p3,p4,p5,p6} do count[v]=(count[v] or 0)+1 end
  459.         local bgc, bgc_n = p1, 0
  460.         for c,n in pairs(count) do if n>bgc_n then bgc, bgc_n = c, n end end
  461.         local fgc, fgc_n = nil, 0
  462.         for c,n in pairs(count) do if n<bgc_n and n>fgc_n then fgc, fgc_n = c, n end end
  463.         local fg = fgc or p1
  464.         local bg = bgc
  465.         local bits = {}
  466.         for i,v in ipairs{p1,p2,p3,p4,p5,p6} do bits[i]=(v==fg) and 1 or 0 end
  467.         term.setTextColor(fg)
  468.         term.setBackgroundColor(bg)
  469.         term.write(string.char(128 + bits[1] + bits[2]*2 + bits[3]*4 + bits[4]*8 + bits[5]*16 + bits[6]*32))
  470.       end
  471.     end
  472.   end
  473. end
  474.  
  475. local function about_section(ver, use_mono)
  476.   local palette = use_mono and PALETTE_MONO or PALETTE_COLOR
  477.   term.setBackgroundColor(palette.bg)
  478.   term.setTextColor(palette.fg)
  479.   term.clear()
  480.   local w, h = term.getSize()
  481.   local imgw, imgh = 20, 12
  482.   local logo_path = "logo.bmp"
  483.   local vanilla = isVanillaCC()
  484.   if not vanilla then
  485.     if not fs.exists(logo_path) then
  486.       local url = "https://files.catbox.moe/5k0smq.bmp"
  487.       print("Downloading logo.bmp...")
  488.       local ok = shell.run("wget", url, logo_path)
  489.       if not ok then
  490.         print("Failed to download logo.bmp! (HTTP must be enabled)")
  491.         sleep(2)
  492.       end
  493.     end
  494.   end
  495.   draw_text(3, 2, "Bow to the fecal-lord", palette.fg, palette.bg)
  496.   if not vanilla and fs.exists(logo_path) then
  497.     render_bmp_mosaic_highres(logo_path, 3, 3, imgw, imgh, use_mono)
  498.   elseif vanilla then
  499.     draw_text(3, 3, "[BMP logo disabled on vanilla ComputerCraft]", palette.fg, palette.bg)
  500.   end
  501.   local tx = imgw+6
  502.   local ty = 6
  503.   draw_text(tx, ty, "3C8 Coded By", palette.fg, palette.bg)
  504.   draw_text(tx, ty+2, "nonogamer9 ", palette.fg, palette.bg)
  505.   local ver_color = use_mono and colors.gray or colors.red
  506.   term.setTextColor(ver_color)
  507.   draw_text(tx+11, ty+2, ver, ver_color, palette.bg)
  508.   local btnw = 22
  509.   local btnx, btny = tx, ty+6
  510.   draw_rect(btnx, btny, btnw, 1, palette.btn)
  511.   draw_text(btnx+math.floor((btnw-18)/2), btny, "Thanks Shitman!", palette.fg, palette.btn)
  512.   while true do
  513.     local e = {os.pullEvent()}
  514.     if e[1] == "mouse_click" then
  515.       local mx, my = e[3], e[4]
  516.       if mx >= btnx and mx < btnx+btnw and my == btny then return end
  517.     elseif e[1] == "key" and e[2] == keys.enter then
  518.       return
  519.     end
  520.   end
  521. end
  522.  
  523. local function settings_menu_ui(settings, use_mono)
  524.   local palette = use_mono and PALETTE_MONO or PALETTE_COLOR
  525.   while true do
  526.     term.setBackgroundColor(palette.bg)
  527.     term.setTextColor(palette.fg)
  528.     term.clear()
  529.     local w, h = term.getSize()
  530.     local menu_w, menu_h = 36, 15
  531.     local mx, my = math.floor((w-menu_w)/2)+1, math.floor((h-menu_h)/2)+1
  532.     local btn_rom = {mx+2, my+2, menu_w-4, 1}
  533.     local btn_mono = {mx+2, my+4, menu_w-4, 1}
  534.     local btn_sound = {mx+2, my+6, menu_w-4, 1}
  535.     local btn_speed = {mx+2, my+8, menu_w-4, 1}
  536.     local btn_back = {mx+2, my+12, menu_w-4, 1}
  537.     draw_rect(mx, my, menu_w, menu_h, palette.box)
  538.     draw_rect(mx+1, my+1, menu_w-2, menu_h-2, palette.bg)
  539.     draw_text(mx+2, my+1, "Settings", palette.fg, palette.bg)
  540.     draw_rect(btn_rom[1], btn_rom[2], btn_rom[3], btn_rom[4], palette.btn)
  541.     draw_text(btn_rom[1]+2, btn_rom[2], "ROM directory: "..settings.rom_dir, palette.fg, palette.btn)
  542.     draw_rect(btn_mono[1], btn_mono[2], btn_mono[3], btn_mono[4], palette.btn)
  543.     draw_text(btn_mono[1]+2, btn_mono[2], "Monochrome: "..(settings.monochrome and "ON" or "OFF"), palette.fg, palette.btn)
  544.     draw_rect(btn_sound[1], btn_sound[2], btn_sound[3], btn_sound[4], palette.btn)
  545.     draw_text(btn_sound[1]+2, btn_sound[2], "Sound: "..sound_mode_name(settings.sound_mode or 1), palette.fg, palette.btn)
  546.     draw_rect(btn_speed[1], btn_speed[2], btn_speed[3], btn_speed[4], palette.btn)
  547.     draw_text(btn_speed[1]+2, btn_speed[2], "Speed: "..speed_name(settings.speed or 1), palette.fg, palette.btn)
  548.     draw_rect(btn_back[1], btn_back[2], btn_back[3], btn_back[4], palette.btn)
  549.     draw_text(btn_back[1]+math.floor((btn_back[3]-4)/2), btn_back[2], "[ Back ]", palette.fg, palette.btn)
  550.     local e = {os.pullEvent()}
  551.     if e[1] == "mouse_click" then
  552.       local x, y = e[3], e[4]
  553.       if mouse_in(x, y, btn_rom[1], btn_rom[2], btn_rom[3], btn_rom[4]) then
  554.         term.setCursorPos(mx+2, my+menu_h)
  555.         term.setTextColor(palette.fg)
  556.         term.setBackgroundColor(palette.bg)
  557.         term.clearLine()
  558.         term.write("New ROM dir: ")
  559.         local newdir = read()
  560.         if fs.exists(newdir) and fs.isDir(newdir) then
  561.           settings.rom_dir = newdir
  562.         else
  563.           term.setCursorPos(mx+2, my+menu_h)
  564.           term.write("Invalid directory!"); sleep(1)
  565.         end
  566.       elseif mouse_in(x, y, btn_mono[1], btn_mono[2], btn_mono[3], btn_mono[4]) then
  567.         settings.monochrome = not settings.monochrome
  568.       elseif mouse_in(x, y, btn_sound[1], btn_sound[2], btn_sound[3], btn_sound[4]) then
  569.         settings.sound_mode = ((settings.sound_mode or 1) % #SOUND_MODES) + 1
  570.       elseif mouse_in(x, y, btn_speed[1], btn_speed[2], btn_speed[3], btn_speed[4]) then
  571.         settings.speed = ((settings.speed or 1) % #SPEEDS) + 1
  572.       elseif mouse_in(x, y, btn_back[1], btn_back[2], btn_back[3], btn_back[4]) then
  573.         return
  574.       end
  575.     elseif e[1] == "key" then
  576.       if e[2] == keys.q or e[2] == keys.enter then
  577.         return
  578.       elseif e[2] == keys.m then
  579.         settings.monochrome = not settings.monochrome
  580.       elseif e[2] == keys.n then
  581.         settings.sound_mode = ((settings.sound_mode or 1) % #SOUND_MODES) + 1
  582.       elseif e[2] == keys.h then
  583.         settings.speed = ((settings.speed or 1) % #SPEEDS) + 1
  584.       end
  585.     end
  586.   end
  587. end
  588. local last_timer = os.clock()
  589. local function play_chip8_sound(mode)
  590.   if mode == 2 then
  591.     if commands and commands.execAsync then
  592.       commands.execAsync("playsound block.note_block.pling @a ~ ~ ~ 0.5 2")
  593.     elseif peripheral.find and peripheral.find("speaker") then
  594.       local speaker = peripheral.find("speaker")
  595.       if speaker then speaker.playNote("pling", 2, 1) end
  596.     end
  597.   elseif mode == 3 then
  598.     local speaker = peripheral.find and peripheral.find("speaker")
  599.     if speaker then
  600.       speaker.playSound("minecraft:block.note_block.pling", 1, 2)
  601.     elseif os.beep then
  602.       os.beep(880, 0.05)
  603.     elseif commands and commands.execAsync then
  604.       commands.execAsync("playsound block.note_block.pling @a ~ ~ ~ 1 2")
  605.     end
  606.   end
  607. end
  608.  
  609. local last_timer = os.clock()
  610. local function play_chip8_sound(mode)
  611.   if mode == 2 then
  612.     if commands and commands.execAsync then
  613.       commands.execAsync("playsound block.note_block.pling @a ~ ~ ~ 0.5 2")
  614.     elseif peripheral.find and peripheral.find("speaker") then
  615.       local speaker = peripheral.find("speaker")
  616.       if speaker then speaker.playNote("pling", 2, 1) end
  617.     end
  618.   elseif mode == 3 then
  619.     play_pcm_beep()
  620.   end
  621. end
  622.  
  623. local function update_timers(settings)
  624.   local now = os.clock()
  625.   if now - last_timer >= 1/60 then
  626.     if delay_timer > 0 then delay_timer = delay_timer - 1 end
  627.     if sound_timer > 0 then
  628.       sound_timer = sound_timer - 1
  629.       if settings and settings.sound_mode and settings.sound_mode > 1 then
  630.         play_chip8_sound(settings.sound_mode)
  631.       end
  632.     end
  633.     last_timer = now
  634.   end
  635. end
  636.  
  637. local last_timer = os.clock()
  638. local function play_chip8_sound(mode)
  639.   if mode == 2 then
  640.     if commands and commands.execAsync then
  641.       commands.execAsync("playsound block.note_block.pling @a ~ ~ ~ 0.5 2")
  642.     elseif peripheral.find and peripheral.find("speaker") then
  643.       local speaker = peripheral.find("speaker")
  644.       if speaker then speaker.playNote("pling", 2, 1) end
  645.     end
  646.   elseif mode == 3 then
  647.   end
  648. end
  649.  
  650. local function update_timers(settings)
  651.   local now = os.clock()
  652.   if now - last_timer >= 1/60 then
  653.     if delay_timer > 0 then delay_timer = delay_timer - 1 end
  654.     if sound_timer > 0 then
  655.       if settings and settings.sound_mode and settings.sound_mode == 3 then
  656.         if not pcm_beep_active then
  657.           start_pcm_beep()
  658.         end
  659.         play_pcm_beep_chunk()
  660.       else
  661.         if settings and settings.sound_mode and settings.sound_mode > 1 then
  662.           play_chip8_sound(settings.sound_mode)
  663.         end
  664.       end
  665.       sound_timer = sound_timer - 1
  666.     else
  667.       stop_pcm_beep()
  668.     end
  669.     last_timer = now
  670.   end
  671. end
  672.  
  673. local function draw_display(screenX, screenY, fg, bg)
  674.     for j = 1, 12 do
  675.         term.setCursorPos(screenX, screenY + j - 1)
  676.         for i = 1, 32 do
  677.             local y0 = (j - 1) * 3
  678.             local x0 = (i - 1) * 2
  679.             local p1 = display[y0 + 1] and display[y0 + 1][x0 + 1] and 1 or 0
  680.             local p2 = display[y0 + 1] and display[y0 + 1][x0 + 2] and 1 or 0
  681.             local p3 = display[y0 + 2] and display[y0 + 2][x0 + 1] and 1 or 0
  682.             local p4 = display[y0 + 2] and display[y0 + 2][x0 + 2] and 1 or 0
  683.             local p5 = display[y0 + 3] and display[y0 + 3][x0 + 1] and 1 or 0
  684.             local p6 = display[y0 + 3] and display[y0 + 3][x0 + 2] and 1 or 0
  685.             if p6 == 1 then
  686.                 p1, p2, p3, p4, p5 = 1 - p1, 1 - p2, 1 - p3, 1 - p4, 1 - p5
  687.                 term.setTextColor(bg)
  688.                 term.setBackgroundColor(fg)
  689.                 term.write(get_mosaic_char(p1, p2, p3, p4, p5, 0))
  690.                 term.setTextColor(fg)
  691.                 term.setBackgroundColor(bg)
  692.             else
  693.                 term.write(get_mosaic_char(p1, p2, p3, p4, p5, 0))
  694.             end
  695.         end
  696.     end
  697. end
  698.  
  699. local function emulate(romfile, fg, bg, settings)
  700.   reset_state()
  701.   load_rom(romfile)
  702.   local monochrome = settings.monochrome
  703.   local running, paused = true, false
  704.   local function center()
  705.     local screenWidth, screenHeight = term.getSize()
  706.     return math.floor((screenWidth - 32) / 2) + 1, math.floor((screenHeight - 12) / 2) + 1, screenHeight
  707.   end
  708.   local screenX, screenY, screenHeight = center()
  709.   term.setTextColor(fg)
  710.   term.setBackgroundColor(bg)
  711.   term.clear()
  712.   draw_display(screenX, screenY, fg, bg)
  713.   local draw_flag = true
  714.   local frameTimer = os.startTimer(1/60)
  715.   local fg_idx, bg_idx = color_index(fg), color_index(bg)
  716.   local waiting_key = false
  717.   local waiting_reg = nil
  718.   while running do
  719.     local e = {os.pullEvent()}
  720.     if e[1] == "terminate" then
  721.       return "quit"
  722.     elseif e[1] == "key" then
  723.       local k = keys.getName(e[2])
  724.       if k then
  725.         k = k:lower()
  726.         if KEYPAD_MAP[k] then
  727.           keypad[KEYPAD_MAP[k]] = true
  728.           if waiting_key then
  729.             V[waiting_reg] = KEYPAD_MAP[k]
  730.             waiting_key = false
  731.             waiting_reg = nil
  732.           end
  733.         end
  734.         if not waiting_key then
  735.           if e[2] == keys.enter then
  736.             return "menu"
  737.           elseif e[2] == keys.space then
  738.             paused = not paused
  739.           elseif not monochrome then
  740.             if e[2] == keys.up then
  741.               bg_idx = (bg_idx % #COLOR_LIST) + 1
  742.               bg = COLOR_LIST[bg_idx]
  743.               term.setBackgroundColor(bg)
  744.               draw_flag = true
  745.             elseif e[2] == keys.down then
  746.               bg_idx = (bg_idx - 2) % #COLOR_LIST + 1
  747.               bg = COLOR_LIST[bg_idx]
  748.               term.setBackgroundColor(bg)
  749.               draw_flag = true
  750.             elseif e[2] == keys.right then
  751.               fg_idx = (fg_idx % #COLOR_LIST) + 1
  752.               fg = COLOR_LIST[fg_idx]
  753.               term.setTextColor(fg)
  754.               draw_flag = true
  755.             elseif e[2] == keys.left then
  756.               fg_idx = (fg_idx - 2) % #COLOR_LIST + 1
  757.               fg = COLOR_LIST[fg_idx]
  758.               term.setTextColor(fg)
  759.               draw_flag = true
  760.             end
  761.           end
  762.         end
  763.       end
  764.     elseif e[1] == "key_up" then
  765.       local k = keys.getName(e[2])
  766.       if k and KEYPAD_MAP[k:lower()] then
  767.         keypad[KEYPAD_MAP[k:lower()]] = false
  768.       end
  769.     elseif e[1] == "term_resize" then
  770.       screenX, screenY, screenHeight = center()
  771.       term.clear()
  772.       draw_flag = true
  773.     elseif e[1] == "timer" and e[2] == frameTimer then
  774.       frameTimer = os.startTimer(1/60)
  775.       if not paused then
  776.         local hz = SPEEDS[settings.speed or 1] or 500
  777.         for i=1,math.floor(hz/60) do
  778.           if waiting_key then break end
  779.           local opcode = bit32.lshift(memory[pc],8) + memory[pc+1]
  780.           pc = (pc + 2) % 4096
  781.           local nnn = bit32.band(opcode, 0x0FFF)
  782.           local nn = bit32.band(opcode, 0x00FF)
  783.           local n = bit32.band(opcode, 0x000F)
  784.           local x = bit32.band(bit32.rshift(opcode,8),0xF)
  785.           local y = bit32.band(bit32.rshift(opcode,4),0xF)
  786.           if opcode == 0x00E0 then
  787.             for y1=1,HEIGHT do for x1=1,WIDTH do display[y1][x1]=false end end
  788.             draw_flag = true
  789.           elseif opcode == 0x00EE then
  790.             pc = table.remove(stack)
  791.           elseif bit32.band(opcode,0xF000) == 0x1000 then
  792.             pc = nnn
  793.           elseif bit32.band(opcode,0xF000) == 0x2000 then
  794.             table.insert(stack, pc)
  795.             pc = nnn
  796.           elseif bit32.band(opcode,0xF000) == 0x3000 then
  797.             if V[x]==nn then pc=pc+2 end
  798.           elseif bit32.band(opcode,0xF000) == 0x4000 then
  799.             if V[x]~=nn then pc=pc+2 end
  800.           elseif bit32.band(opcode,0xF00F) == 0x5000 then
  801.             if V[x]==V[y] then pc=pc+2 end
  802.           elseif bit32.band(opcode,0xF000) == 0x6000 then
  803.             V[x]=nn
  804.           elseif bit32.band(opcode,0xF000) == 0x7000 then
  805.             V[x]=(V[x]+nn)%256
  806.           elseif bit32.band(opcode,0xF00F)==0x8000 then
  807.             V[x]=V[y]
  808.           elseif bit32.band(opcode,0xF00F)==0x8001 then
  809.             V[x]=bit32.bor(V[x],V[y])
  810.           elseif bit32.band(opcode,0xF00F)==0x8002 then
  811.             V[x]=bit32.band(V[x],V[y])
  812.           elseif bit32.band(opcode,0xF00F)==0x8003 then
  813.             V[x]=bit32.bxor(V[x],V[y])
  814.           elseif bit32.band(opcode,0xF00F)==0x8004 then
  815.             local sum = V[x]+V[y]
  816.             V[0xF] = sum>255 and 1 or 0
  817.             V[x]=sum%256
  818.           elseif bit32.band(opcode,0xF00F)==0x8005 then
  819.             V[0xF] = (V[x] >= V[y]) and 1 or 0
  820.             V[x]=(V[x]-V[y])%256
  821.           elseif bit32.band(opcode,0xF00F)==0x8006 then
  822.             V[0xF] = bit32.band(V[x],1)
  823.             V[x]=bit32.rshift(V[x],1)
  824.           elseif bit32.band(opcode,0xF00F)==0x8007 then
  825.             V[0xF] = (V[y] >= V[x]) and 1 or 0
  826.             V[x]=(V[y]-V[x])%256
  827.           elseif bit32.band(opcode,0xF00F)==0x800E then
  828.             V[0xF] = bit32.band(bit32.rshift(V[x],7),1)
  829.             V[x]=(bit32.lshift(V[x],1)%256)
  830.           elseif bit32.band(opcode,0xF00F)==0x9000 then
  831.             if V[x]~=V[y] then pc=pc+2 end
  832.           elseif bit32.band(opcode,0xF000)==0xA000 then
  833.             I = nnn
  834.           elseif bit32.band(opcode,0xF000)==0xB000 then
  835.             pc = nnn + V[0]
  836.           elseif bit32.band(opcode,0xF000)==0xC000 then
  837.             V[x]=bit32.band(math.random(0,255),nn)
  838.           elseif bit32.band(opcode,0xF000)==0xD000 then
  839.             local vx, vy = V[x]%WIDTH, V[y]%HEIGHT
  840.             V[0xF]=0
  841.             for row=0,n-1 do
  842.               if vy+row>=HEIGHT then break end
  843.               local sprite = memory[I+row]
  844.               for col=0,7 do
  845.                 if vx+col>=WIDTH then break end
  846.                 local bit = bit32.band(sprite, bit32.rshift(0x80,col)) ~= 0
  847.                 if bit then
  848.                   if display[vy+row+1][vx+col+1] then
  849.                     V[0xF]=1
  850.                   end
  851.                   display[vy+row+1][vx+col+1] = bit32.bxor(display[vy+row+1][vx+col+1] and 1 or 0, 1)==1
  852.                 end
  853.               end
  854.             end
  855.             draw_flag = true
  856.           elseif bit32.band(opcode,0xF0FF)==0xE09E then
  857.             if keypad[V[x]] then pc=pc+2 end
  858.           elseif bit32.band(opcode,0xF0FF)==0xE0A1 then
  859.             if not keypad[V[x]] then pc=pc+2 end
  860.           elseif bit32.band(opcode,0xF0FF)==0xF007 then
  861.             V[x]=delay_timer
  862.           elseif bit32.band(opcode,0xF0FF)==0xF00A then
  863.             waiting_key = true
  864.             waiting_reg = x
  865.           elseif bit32.band(opcode,0xF0FF)==0xF015 then
  866.             delay_timer=V[x]
  867.           elseif bit32.band(opcode,0xF0FF)==0xF018 then
  868.             sound_timer=V[x]
  869.           elseif bit32.band(opcode,0xF0FF)==0xF01E then
  870.             I = (I+V[x])%4096
  871.           elseif bit32.band(opcode,0xF0FF)==0xF029 then
  872.             I = FONT_START + (V[x]%16)*5
  873.           elseif bit32.band(opcode,0xF0FF)==0xF033 then
  874.             local val = V[x]
  875.             memory[I] = math.floor(val/100)
  876.             memory[I+1] = math.floor((val%100)/10)
  877.             memory[I+2] = val%10
  878.           elseif bit32.band(opcode,0xF0FF)==0xF055 then
  879.             for idx=0,x do memory[I+idx]=V[idx] end
  880.           elseif bit32.band(opcode,0xF0FF)==0xF065 then
  881.             for idx=0,x do V[idx]=memory[I+idx] end
  882.           end
  883.         end
  884.         update_timers(settings)
  885.         if draw_flag then
  886.           draw_display(screenX, screenY, fg, bg)
  887.           draw_flag = false
  888.         end
  889.       end
  890.       term.setCursorPos(1, screenHeight)
  891.       term.clearLine()
  892.       local status =
  893.         "Space: Pause  Enter: Menu      " ..
  894.         (monochrome and "[Monochrome]" or ("FG: "..COLOR_NAMES[fg_idx].." BG: "..COLOR_NAMES[bg_idx])) ..
  895.         "  Sound: "..sound_mode_name(settings.sound_mode or 1) ..
  896.         "  Speed: "..speed_name(settings.speed or 1)
  897.       local w = term.getSize()
  898.       if #status > w then
  899.         status = status:sub(1, w-1) .. "…"
  900.       end
  901.       term.write(status)
  902.     end
  903.   end
  904. end
  905.  
  906. local function main()
  907.   ensure_pcm_beep()
  908.   local settings = { rom_dir = "roms", monochrome = false, sound_mode = 1, speed = 3 }
  909.   local color_capable = isColor()
  910.   while true do
  911.     local romfile
  912.     while not romfile do
  913.       local use_mono = settings.monochrome or not color_capable
  914.       local sel = rom_menu_ui(settings.rom_dir, use_mono)
  915.       if sel == "settings" then
  916.         settings_menu_ui(settings, use_mono)
  917.       elseif sel == "about" then
  918.         about_section(VERSION, use_mono)
  919.       elseif sel == nil then
  920.         term.clear()
  921.         term.setCursorPos(1,1)
  922.         print("Goodbye!")
  923.         return
  924.       else
  925.         romfile = sel
  926.       end
  927.     end
  928.     local fg, bg = colors.white, colors.black
  929.     local result = emulate(romfile, fg, bg, settings)
  930.     if result == "quit" then return end
  931.     if result == "menu" then romfile = nil end
  932.   end
  933. end
  934. main()
Add Comment
Please, Sign In to add comment