settings.define("mplayer.colors.bg", { description = "Background color in media player", default = 0x131313, -- #131313 type = number }) settings.define("mplayer.colors.fg", { description = "Text color in media player", default = 0xEFEFEF, -- #EFEFEF type = number }) settings.define("mplayer.colors.cursor", { description = "Color of the cursor", default = 0x8080EF, -- #8080EF type = number }) settings.define("mplayer.colors.current", { description = "Color of the currently playing song", default = 0xEFEF80, -- #EFEF80 type = number }) settings.define("mplayer.colors.status", { description = "Color of the statusbar", default = 0x80EF80, -- #80EF80 type = number }) local tw, th = term.getSize() os.queueEvent("dummy") while tw < 25 or th < 10 do local ev = { os.pullEvent() } term.clear() term.setCursorPos(1, 1) printError("Too small: " .. tw .. "x" .. th .. " < 25x10") printError("Q to exit") printError("Y to ignore") local setTextScale = term.current().setTextScale if setTextScale ~= nil then printError("S to try rescaling") end printError("I'll wait while you're adding more") if ev[1] == "term_resize" then tw, th = term.getSize() elseif ev[1] == "key" and ev[2] == keys.s and setTextScale ~= nil then setTextScale(0.5) elseif ev[1] == "key" and ev[2] == keys.y then break elseif ev[1] == "key" and ev[2] == keys.q then return end end local drive = peripheral.find("tape_drive") if not drive then printError("No drive found, starting in dummy mode") local fp = io.open("noita.dfpwm", "rb") if fp == nil then printError("No sample file found, are you running it on a real computer without a tape drive?") return end local size = fp:seek("end", 0) fp:seek("set", 0) drive = { _pos = 0, _fp = fp, _state = "STOPPED", isDummy = true, seek = function(howMuch) drive._pos = math.min(drive.getSize(), math.max(0, drive._pos + howMuch)) end, getPosition = function() return drive._pos end, read = function(n) local out = { drive._fp:read(n) } drive.seek(n or 1) return table.unpack(out) end, getSize = function() return size end, getState = function() return drive._state end, play = function() drive._state = "PLAYING" end, stop = function() drive._state = "STOPPED" end, isReady = function() return true end, getLabel = function() return "Dummy drive tape" end, _tick = function() if drive._state == "PLAYING" then drive.read(600) end end, } os.sleep(1) end local function time2str(ti) ti = math.floor(ti) local m, s = math.floor(ti / 60), ti % 60 return string.format("%02d:%02d", m, s) end local function read32() local v = 0 for i = 1, 4 do local b = string.byte(drive.read(1), 1) v = bit32.bor(bit32.lshift(v, 8), b) end return v end local help = {} for line in ([[# Movement: -- Up k : Move cursor up Dn j : Move cursor down H : Jump to top L : Jump down PgUp : Page up PgDn : Page down Tab : Next screen S+Tab : Prev. screen 1 F1 : Help screen 2 F2 : Songs list 3 F3 : Settings # Global: -- s : Stop, jump to start p : Pause/resume < : Next track > : Previous track f : Seek forward b : Seek backward \x1b - : Lower volume \x1a + : Higher volume # List screen: -- Enter : Play Ctrl+l : Center l : Jump to current ]]):gsub("\\x(..)", function(m) return string.char(tonumber(m, 16)) end):gmatch("[^\n]+") do table.insert(help, line) end local mplayer = { colors = { bg = colors.black, fg = colors.white, cursor = colors.blue, current = colors.yellow, status = colors.lime }, heldKeys = {}, screens = { { title = "Help", scroll = 0, render = function(self) for i = 1, th - 3 do local line = help[i + self.screens[1].scroll] or "~" term.setCursorPos(1, i + 1) term.clearLine() if line:sub(1, 1) == "~" then term.setTextColor(self.colors.cursor) elseif line:sub(1, 1) == "#" then term.setTextColor(self.colors.current) elseif line:sub(1, 2) == "--" then term.setTextColor(self.colors.status) term.write(("-"):rep(tw - 2)) else term.setTextColor(self.colors.fg) end term.write(line) end end, handleKey = function(self, key, repeating) if key == keys.down or key == keys.j then self.screens[1].handleScroll(self, 1) elseif key == keys.up or key == keys.k then self.screens[1].handleScroll(self, -1) elseif key == keys.pageDown then self.screens[1].handleScroll(self, th - 3) elseif key == keys.pageUp then self.screens[1].handleScroll(self, -(th - 3)) end end, handleScroll = function(self, direction, x, y) self.screens[1].scroll = math.max(0, math.min(th - 1, self.screens[1].scroll + direction)) end, }, { title = "List", scroll = 0, cursor = 1, textScroll = 0, render = function(self) for i = 1, th - 3 do local song = self.songs[i + self.screens[2].scroll] local isCurrent = (i + self.screens[2].scroll) == self.currentSong local isHovered = (i + self.screens[2].scroll) == self.screens[2].cursor term.setCursorPos(1, i + 1) local bg, fg = self.colors.bg, (isCurrent and self.colors.current or self.colors.fg) if isHovered then bg, fg = fg, bg end term.setBackgroundColor(bg) term.setTextColor(fg) term.clearLine() if song then local timeString = " ["..time2str(song.length / 6000) .. "]" local w = tw - #timeString if #song.title <= w then term.write(song.title) else local off = isHovered and ((self.screens[2].textScroll % (#song.title + 5)) + 1) or 1 local txt = song.title .. " ::: " .. song.title term.write(txt:sub(off, off + w - 1)) end term.setCursorPos(tw - #timeString + 1, i + 1) term.write(timeString) end end end, handleKey = function(self, key, repeating) local shiftHeld = self.heldKeys[keys.leftShift] ~= nil or self.heldKeys[keys.rightShift] ~= nil local ctrlHeld = self.heldKeys[keys.leftCtrl] ~= nil or self.heldKeys[keys.rightCtrl] ~= nil if key == keys.down or key == keys.j then self.screens[2].handleScroll(self, 1, 1, 1) elseif key == keys.up or key == keys.k then self.screens[2].handleScroll(self, -1, 1, 1) elseif key == keys.pageDown then self.screens[2].handleScroll(self, th - 3, 1, 1) elseif key == keys.pageUp then self.screens[2].handleScroll(self, -(th - 3), 1, 1) elseif key == keys.h and shiftHeld then self.screens[2].handleScroll(self, -#self.songs, 1, 1) elseif key == keys.l and shiftHeld then self.screens[2].handleScroll(self, #self.songs, 1, 1) elseif key == keys.l then self.screens[2].cursor = self.currentSong or 1 if ctrlHeld then self.screens[2].scroll = self.screens[2].scroll - math.floor((th - 3) / 2) end self.screens[2].handleScroll(self, 0, 1, 1) elseif key == keys.enter then drive.seek(-drive.getSize()) drive.seek(self.songs[self.screens[2].cursor].offset) drive.play() self.currentSong = self.screens[2].cursor end end, handleScroll = function(self, direction, x, y) self.screens[2].cursor = math.max(1, math.min(#self.songs, self.screens[2].cursor + direction)) if self.screens[2].scroll + 1 > self.screens[2].cursor then self.screens[2].scroll = self.screens[2].cursor - 1 end local maxi = self.screens[2].scroll + th - 3 if self.screens[2].cursor > maxi then self.screens[2].scroll = self.screens[2].cursor - (th - 3) end if self.screens[2].scroll > #self.songs - (th - 3) then self.screens[2].scroll = #self.songs - (th - 3) end if self.screens[2].scroll < 0 then self.screens[2].scroll = 0 end self.screens[2].textScroll = 0 end, handleClick = function(self, x, y) local i = self.screens[2].scroll + y - 1 if i == self.screens[2].cursor then self.screens[2].handleKey(self, keys.enter, false) elseif i <= #self.songs then self.screens[2].cursor = i end end, }, { title = "Settings", scroll = 0, cursor = 1, render = function(self) term.clear() term.write(string.format("opt = %d, scroll = %d", self.screens[3].cursor, self.screens[3].scroll)) end, handleKey = function(self, key, repeating) if key == keys.down or key == keys.j then self.screens[3].handleScroll(self, 1) elseif key == keys.up or key == keys.k then self.screens[3].handleScroll(self, -1) elseif key == keys.pageDown then self.screens[3].handleScroll(self, th - 3) elseif key == keys.pageUp then self.screens[3].handleScroll(self, -(th - 3)) end end, handleScroll = function(self, direction, x, y) self.screens[3].cursor = math.max(1, math.min(20, self.screens[3].cursor + direction)) if self.screens[3].scroll + 1 > self.screens[3].cursor then self.screens[3].scroll = self.screens[3].cursor - 1 end local maxi = self.screens[3].scroll + th - 3 if self.screens[3].cursor > maxi then self.screens[3].scroll = self.screens[3].cursor - (th - 3) end end } }, songs = {}, currentSong = 0, statusLineScroll = 0, currentScreen = 2, } for k, v in pairs(mplayer.colors) do term.setPaletteColor(v, settings.get("mplayer.colors." .. k)) end term.setCursorPos(1, 1) term.setBackgroundColor(mplayer.colors.bg) term.setTextColor(mplayer.colors.fg) term.clear() local spinner = { "\x81", "\x83", "\x82", "\x8a", "\x88", "\x8c", "\x84", "\x85" } parallel.waitForAny( function() while true do -- Current screen term.setCursorPos(1, 2) mplayer.screens[mplayer.currentScreen].render(mplayer) -- Top bar term.setCursorPos(1, 1) for i, screen in ipairs(mplayer.screens) do term.setTextColor(mplayer.colors[i == mplayer.currentScreen and "bg" or "fg"]) term.setBackgroundColor(mplayer.colors[i == mplayer.currentScreen and "fg" or "bg"]) term.write(" "..i..":"..screen.title.." ") end term.setBackgroundColor(mplayer.colors.bg) local title, time, duration = "Whatever is on the tape", drive.getPosition() / 6000, drive.getSize() / 6000 if mplayer.currentSong ~= 0 then local song = mplayer.songs[mplayer.currentSong] time = (drive.getPosition() - song.offset) / 6000 duration = song.length / 6000 title = song.title end -- Statusline term.setCursorPos(1, th) term.clearLine() local timeString = string.format("[%s:%s]", time2str(time), time2str(duration)) if drive.getState() ~= "STOPPED" then term.setTextColor(mplayer.colors.status) local speen = spinner[(math.floor(drive.getPosition() / 3000) % #spinner) + 1] local action = "" if drive.getState() == "PLAYING" then action = "Playing:" elseif drive.getState() == "REWINDING" then action = "Rewinding" elseif drive.getState() == "FORWARDING" then action = "Forwarding" else printError("Unknown drive state: "..tostring(drive.getState())) return end action = speen .. " " .. action term.write(action) -- Progressbar local lw = math.floor(tw * time / duration) term.setCursorPos(1, th - 1) term.setTextColor(mplayer.colors.current) term.clearLine() term.write(string.rep("=", lw)) term.write("\x10") term.setTextColor(mplayer.colors.cursor) term.write(string.rep("\xad", tw - lw - 1)) -- Statusline text term.setCursorPos(#action + 2, th) local w = tw - #timeString - #action - 2 -- "Playing: ", spinner plus spacing term.setTextColor(mplayer.colors.current) if #title <= w then term.write(title) else local off = (mplayer.statusLineScroll % (#title + 5)) + 1 local txt = title .. " ::: " .. title term.write(txt:sub(off, off + w - 1)) end end term.setTextColor(mplayer.colors.status) term.setCursorPos(tw - #timeString + 1, th) term.write(timeString) os.sleep(0.1) end end, function() local pretty = require("cc.pretty") while true do local _evd = { os.pullEvent() } local ev, evd = table.remove(_evd, 1), _evd if ev == "key" then mplayer.heldKeys[evd[1]] = evd[2] elseif ev == "key_up" then mplayer.heldKeys[evd[1]] = nil end if ev == "key_up" and evd[1] == keys.q then break elseif ev == "key" and (evd[1] == keys.one or evd[1] == keys.f1) then mplayer.currentScreen = 1 elseif ev == "key" and (evd[1] == keys.two or evd[1] == keys.f2) then mplayer.currentScreen = 2 elseif ev == "key" and (evd[1] == keys.three or evd[1] == keys.f3) then mplayer.currentScreen = 3 elseif ev == "key" and evd[1] == keys.f then drive.seek(3000) elseif ev == "key" and evd[1] == keys.b then drive.seek(-3000) elseif ev == "key" and evd[1] == keys.s then drive.stop() drive.seek(-drive.getSize()) drive.seek(6000) elseif ev == "key" and (evd[1] == keys.p or evd[1] == keys.space) then if drive.getState() ~= "STOPPED" then drive.stop() else drive.play() end elseif ev == "key" and evd[1] == keys.tab then local dir = ((mplayer.heldKeys[keys.leftShift] ~= nil) or (mplayer.heldKeys[keys.rightShift] ~= nil)) and -1 or 1 mplayer.currentScreen = ((mplayer.currentScreen - 1 + #mplayer.screens + dir)) % #mplayer.screens + 1 elseif ev == "key" then mplayer.screens[mplayer.currentScreen].handleKey(mplayer, evd[1], evd[2]) elseif ev == "mouse_scroll" then local dir, x, y = table.unpack(evd) mplayer.screens[mplayer.currentScreen].handleScroll(mplayer, dir, x, y) elseif ev == "song_change" then mplayer.statusLineScroll = 0 elseif (ev == "mouse_click" or ev == "mouse_drag") and evd[3] == th - 1 then local p = (evd[2] - 1) / tw local song = mplayer.songs[mplayer.currentSong] drive.seek(-drive.getSize()) if song ~= nil then drive.seek(song.offset + math.floor(song.length * p)) else drive.seek(math.floor(p * drive.getSize())) end elseif (ev == "mouse_click" or ev == "mouse_drag") and evd[3] == 1 then local cx, x = 1, evd[2] for i, screen in ipairs(mplayer.screens) do local caption = " "..i..""..screen.title.." " if x >= cx and x <= (cx + #caption) then mplayer.currentScreen = i break end cx = cx + #caption end elseif ev == "mouse_click" or ev == "mouse_drag" then if mplayer.screens[mplayer.currentScreen].handleClick then local butt, x, y = table.unpack(evd) mplayer.screens[mplayer.currentScreen].handleClick(mplayer, x, y) end elseif ev == "term_resize" then tw, th = term.getSize() elseif ev == "tape_removed" then mplayer.songs = {} mplayer.currentSong = 0 elseif ev == "tape_inserted" then local seekToAndPlay = nil if drive.getState() == "PLAYING" then seekToAndPlay = drive.getPosition() end drive.stop() drive.seek(-drive.getSize()) for i = 1, 48 do local offset = read32() local length = read32() local title = drive.read(117) if offset > drive.getSize() or length > drive.getSize() then mplayer.songs = { { title = drive.getLabel(), offset = 0, length = drive.getSize() - 10 }, { title = "NOTE: It's just a regular tape", offset = drive.getSize() - 10, length = 10 } } for t = 1, drive.getSize(), 6000 * 60 * 5 do table.insert(mplayer.songs, 2, { title = "Skip to " .. time2str(t), offset = t, length = 6000 * 60 * 5 }) end break end title = title:sub(1, title:find("\x00")) if length > 0 and offset > 0 then mplayer.songs[i] = { title = title, offset = offset, length = length } end end if seekToAndPlay ~= nil then drive.seek(-drive.getSize()) drive.seek(seekToAndPlay) drive.play() end elseif ev ~= "timer" then if drive.isDummy then local m = term.redirect(peripheral.wrap("right")) io.write(ev .. " ") pretty.print(pretty.pretty(evd)) term.redirect(m) end end end end, function() while true do mplayer.statusLineScroll = mplayer.statusLineScroll + 1 mplayer.screens[2].textScroll = mplayer.screens[2].textScroll + 1 os.sleep(0.25) end end, function() local oldSong = nil local oldDriveState = nil local oldTapeState = nil while true do mplayer.currentSong = 0 if drive._tick then drive._tick() end -- dummy mode local tapeState = drive.isReady() if tapeState ~= oldTapeState then os.queueEvent(tapeState and "tape_inserted" or "tape_removed") oldTapeState = tapeState end local driveState = drive.getState() if driveState ~= oldDriveState then os.queueEvent("drive_state", driveState) oldDriveState = driveState end local pos = drive.getPosition() for i, song in ipairs(mplayer.songs) do if pos >= song.offset and pos < (song.offset + song.length) then mplayer.currentSong = i end end if oldSong ~= mplayer.currentSong then os.queueEvent("song_change") oldSong = mplayer.currentSong end os.sleep(0.1) end end ) -- cleanup term.clear() term.setCursorPos(1, 1) for i = 1, 16 do local c = math.pow(2, i - 1) term.setPaletteColor(c, term.nativePaletteColor(c)) end term.setBackgroundColor(colors.black) term.setTextColor(colors.white) if term.current().setTextScale then term.current().setTextScale(1.0) end print(" Goodbye!")