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 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, _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 Down j : Move cursor down H : Move cursor to the top of screen L : Move cursor to the bottom of screen PageUp : Page up PageDown : Page down Tab : Next screen Shift+Tab : Previous screen 1 F1 h : Help screen (this one) 2 F2 : Songs list 3 F3 : Options screen # Global: -- s : Stop and seek to the beginning p : Pause/resume < : Next track > : Previous track f : Seek forward b : Seek backward Left - : Decrease volume Right + : Increase volume # List screen: -- Enter : Play Ctrl+l : Center l : Jump to current track ]]):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) local tw, th = term.getSize() 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) local _, th = term.getSize() 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) local _, th = term.getSize() 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) local tw, th = term.getSize() 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 _, th = term.getSize() if key == keys.down or key == keys.j then self.screens[2].handleScroll(self, 1) elseif key == keys.up or key == keys.k then self.screens[2].handleScroll(self, -1) elseif key == keys.pageDown then self.screens[2].handleScroll(self, th - 3) elseif key == keys.pageUp then self.screens[2].handleScroll(self, -(th - 3)) 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) local _, th = term.getSize() 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 self.screens[2].textScroll = 0 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) local _, th = term.getSize() 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) local _, th = term.getSize() 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 tw, th = term.getSize() 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() == "PLAYING" then term.setTextColor(mplayer.colors.status) term.write("Playing: ") -- 9 characters -- 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(">") term.setTextColor(mplayer.colors.cursor) term.write(string.rep("-", tw - lw - 1)) -- Statusline text term.setCursorPos(10, th) local w = tw - #timeString - 10 -- "Playing: " plus extra space 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") local tw, th = term.getSize() 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.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 == "term_resize" then tw, th = term.getSize() elseif ev == "tape_removed" then mplayer.songs = {} 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) 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 )