--[[ * * Singe 2 * Copyright (C) 2006-2024 Scott Duensing * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. * * --]] dofile("Singe/Framework.singe") local lfs = require("lfs") -- Audio delay calibration: a click every second and a flash scheduled the engine's measured -- device queue plus the candidate value after it. The click and the disc audio share one mixer -- device, so when the player sees and hears them as one event the candidate is the machine's delay. function calibrationBegin() CALIBRATING = true CAL_ORIGINAL = singeGetAudioCalibration() CAL_VALUE = CAL_ORIGINAL CAL_LATENCY = singeGetAudioLatency() CAL_BEAT = singeGetTicks() - CAL_PERIOD CAL_CLICK_DONE = true CAL_FLASH_DONE = true if VIDEO_ATTRACT then videoPause(VIDEO_ATTRACT) end end function calibrationEnd(save) if save then singeSetAudioCalibration(CAL_VALUE) end CALIBRATING = false if VIDEO_ATTRACT then videoPlay(VIDEO_ATTRACT) end end function calibrationInput(what) local delta = 0 if what == SWITCH_LEFT then delta = -CAL_STEP_COARSE elseif what == SWITCH_RIGHT then delta = CAL_STEP_COARSE elseif what == SWITCH_UP then delta = CAL_STEP_FINE elseif what == SWITCH_DOWN then delta = -CAL_STEP_FINE elseif what == SWITCH_START1 or what == SWITCH_START2 then CAL_VALUE = 0 elseif what == SWITCH_BUTTON1 then calibrationEnd(true) elseif what == SWITCH_BUTTON2 or what == SWITCH_SERVICE then calibrationEnd(false) end if delta ~= 0 then CAL_VALUE = math.max(-CAL_LIMIT, math.min(CAL_LIMIT, CAL_VALUE + delta)) end end function calibrationUpdate() local now = singeGetTicks() local gap = CAL_LATENCY + CAL_VALUE -- flash this long after the click; negative means before local flash = false local y = MARGIN_Y -- Start a new beat? Whichever of click and flash comes first goes on the beat. if now - CAL_BEAT >= CAL_PERIOD then CAL_BEAT = now CAL_CLICK_AT = now + math.max(0, -gap) CAL_FLASH_AT = now + math.max(0, gap) CAL_CLICK_DONE = false CAL_FLASH_DONE = false end if not CAL_CLICK_DONE and now >= CAL_CLICK_AT then soundPlay(SND_CLICK) CAL_CLICK_DONE = true end if not CAL_FLASH_DONE and now >= CAL_FLASH_AT then flash = true CAL_FLASH_DONE = true end if flash then colorBackground(255, 255, 255, 255) colorForeground(0, 0, 0, 255) else colorBackground(0, 0, 0, 255) colorForeground(255, 255, 255, 255) end overlayClear() fontPrint(MARGIN_X, y, "AUDIO DELAY CALIBRATION") y = y + CAL_LINE_HEIGHT * 2 fontPrint(MARGIN_X, y, "Adjust until the flash and the click happen together.") y = y + CAL_LINE_HEIGHT * 2 fontPrint(MARGIN_X, y, "Delay: " .. CAL_VALUE .. " ms") y = y + CAL_LINE_HEIGHT fontPrint(MARGIN_X, y, "Measured device queue: " .. CAL_LATENCY .. " ms") y = y + CAL_LINE_HEIGHT * 2 fontPrint(MARGIN_X, y, "Left / Right: 10 ms Up / Down: 1 ms Start: reset to 0") y = y + CAL_LINE_HEIGHT fontPrint(MARGIN_X, y, "Button 1: save Button 2: cancel") end function cleanTitle(a) local output = string.lower(a) output = string.gsub(output, '%p', ' ') output = string.gsub(output, '^ +', '') output = string.gsub(output, '^the ', '') output = string.gsub(output, '^a ', '') output = string.gsub(output, '^an ', '') return output end function compareTitles(a, b) return cleanTitle(a.TITLE) < cleanTitle(b.TITLE) end function loadGameAssets() SPRITE_CABINET = spriteLoad(GAME_LIST[GAME_SELECTED].CABINET) SPRITE_MARQUEE = spriteLoad(GAME_LIST[GAME_SELECTED].MARQUEE) VIDEO_ATTRACT = videoLoad(GAME_LIST[GAME_SELECTED].ATTRACT) if GAME_LIST[GAME_SELECTED].AUDIO_TRACK then videoSetAudioTrack(VIDEO_ATTRACT, GAME_LIST[GAME_SELECTED].AUDIO_TRACK) end videoPlay(VIDEO_ATTRACT) videoSeek(VIDEO_ATTRACT, GAME_LIST[GAME_SELECTED].ATTRACT_START) -- videoSetVolume(VIDEO_ATTRACT, 0, 0) -- Build text sprites local textBox = GAME_LIST[GAME_SELECTED].DESCRIPTION .. WRAP_BREAK .. WRAP_BREAK .. "Year: " .. GAME_LIST[GAME_SELECTED].YEAR .. WRAP_BREAK .. "Genre: " .. GAME_LIST[GAME_SELECTED].GENRE .. WRAP_BREAK .. "Platform: " .. GAME_LIST[GAME_SELECTED].PLATFORM .. WRAP_BREAK .. "Developer: " .. GAME_LIST[GAME_SELECTED].DEVELOPER .. WRAP_BREAK .. "Publisher: " .. GAME_LIST[GAME_SELECTED].PUBLISHER .. WRAP_BREAK .. WRAP_BREAK .. "Singe Port: " .. GAME_LIST[GAME_SELECTED].CREATOR .. WRAP_BREAK .. "Source: " .. GAME_LIST[GAME_SELECTED].SOURCE wrapText(textBox, TEXT_W) TEXT_LINE_COUNT = utilGetTableSize(TEXT_SPRITE_LIST) TEXT_LINE_TOP = 1 end function onInputPressed(what) if CALIBRATING then calibrationInput(what) return end -- Are we displaying the grid background? if (discGetFrame() >= DISC_GRID_START) then if what == SWITCH_SERVICE then calibrationBegin() return end if what == SWITCH_UP then if TEXT_LINE_TOP > 1 then TEXT_LINE_TOP = TEXT_LINE_TOP - 1 end end if what == SWITCH_DOWN then if TEXT_LINE_TOP < TEXT_LINE_COUNT - TEXT_LINE_LIMIT + 1 then TEXT_LINE_TOP = TEXT_LINE_TOP + 1 end end if what == SWITCH_LEFT then GAME_SELECTED = GAME_SELECTED - 1 if GAME_SELECTED < 1 then GAME_SELECTED = GAME_COUNT end unloadGameAssets() loadGameAssets() end if what == SWITCH_RIGHT then GAME_SELECTED = GAME_SELECTED + 1 if GAME_SELECTED > GAME_COUNT then GAME_SELECTED = 1 end unloadGameAssets() loadGameAssets() end if what == SWITCH_START1 or what == SWITCH_START2 or what == SWITCH_BUTTON1 or what == SWITCH_BUTTON2 or what == SWITCH_BUTTON3 or what == SWITCH_BUTTON4 then -- Start next game SHUTDOWN_FROM_PUSH = true scriptPush(GAME_LIST[GAME_SELECTED]) end end end function onOverlayUpdate() local x = 0 local y = 0 local c = 0 local t = 0 -- Loop video? if (discGetFrame() >= DISC_LAST_FRAME) then discSkipToFrame(DISC_GRID_START) end if CALIBRATING then calibrationUpdate() return(OVERLAY_UPDATED) end colorBackground(0, 0, 0, 0) overlayClear() -- Are we displaying the grid background? if (discGetFrame() >= DISC_GRID_START) then -- Cabinet image x = CABINET_X + (CABINET_W - spriteGetWidth(SPRITE_CABINET)) * 0.5 y = CABINET_Y + (CABINET_H - spriteGetHeight(SPRITE_CABINET)) * 0.5 spriteDraw(SPRITE_CABINET, x, y) -- Marquee Image x = MARQUEE_X + (MARQUEE_W - spriteGetWidth(SPRITE_MARQUEE)) * 0.5 y = MARQUEE_Y + (MARQUEE_H - spriteGetHeight(SPRITE_MARQUEE)) * 0.5 spriteDraw(SPRITE_MARQUEE, x, y) -- Attract Mode Video videoDraw(VIDEO_ATTRACT, VIDEO_X, VIDEO_Y, VIDEO_X + VIDEO_W, VIDEO_Y + VIDEO_H) if videoGetFrame(VIDEO_ATTRACT) > GAME_LIST[GAME_SELECTED].ATTRACT_END then videoSeek(VIDEO_ATTRACT, GAME_LIST[GAME_SELECTED].ATTRACT_START) end -- Game Description colorForeground(255, 255, 255, 255) y = TEXT_Y c = 0 t = 1 for _, handle in ipairs(TEXT_SPRITE_LIST) do -- Find height of font and number of lines that fit if TEXT_LINE_HEIGHT == 0 then if (handle >= 0) then TEXT_LINE_HEIGHT = spriteGetHeight(handle) TEXT_LINE_LIMIT = math.floor(TEXT_H / TEXT_LINE_HEIGHT) end end -- Only display what is visible in the window if (t >= TEXT_LINE_TOP) then if (c < TEXT_LINE_LIMIT) then if (handle >= 0) then spriteDraw(handle, TEXT_X, y) end y = y + TEXT_LINE_HEIGHT + 1 c = c + 1 end end t = t + 1 end end return(OVERLAY_UPDATED) end function onShutdown() unloadGameAssets() saveConfig(not SHUTDOWN_FROM_PUSH) if freeSans18 then fontUnload(freeSans18) end if SND_CLICK then soundUnload(SND_CLICK) end end function saveConfig(showIntro) if GAME_COUNT > 0 then -- Save what game we're currently viewing local cfg = io.open(CONFIG_FILE, "w") if cfg then cfg:write("GAME_SELECTED = " .. GAME_SELECTED .. "\n") cfg:write("SHOW_INTRO = " .. tostring(showIntro) .. "\n") cfg:close() else debugPrint("Unable to write " .. CONFIG_FILE) end end end function unloadGameAssets() -- Nothing is loaded when there are no games. if SPRITE_CABINET then spriteUnload(SPRITE_CABINET) SPRITE_CABINET = nil end if SPRITE_MARQUEE then spriteUnload(SPRITE_MARQUEE) SPRITE_MARQUEE = nil end if VIDEO_ATTRACT then videoUnload(VIDEO_ATTRACT) VIDEO_ATTRACT = nil end for _, handle in ipairs(TEXT_SPRITE_LIST or {}) do if handle >= 0 then spriteUnload(handle) end end TEXT_SPRITE_LIST = {} end function wrapText(text, maxWidth) local words = {} local toWrap = "" local line = "" local lastLine = "" local fragment = "" local doBreak = false local spriteTemp = -1 local trimBreak = utilTrim(WRAP_BREAK) -- Break input into words toWrap = text .. WRAP_BREAK for w in toWrap:gmatch("%S+") do words[#words+1] = w end -- Iterate over words and try to fit them in the space given for _, word in ipairs(words) do -- Is this an intentional break? if word == trimBreak then doBreak = true else -- Add this word to the line line = utilTrim(line .. " " .. word) -- Do we even have text? if string.len(line) > 0 then -- Is the line too long to fit now? spriteTemp = fontToSprite(line) if spriteGetWidth(spriteTemp) > maxWidth then doBreak = true fragment = word line = lastLine else fragment = "" end spriteUnload(spriteTemp) end end lastLine = line -- Did we find a break? if doBreak then -- Did we get anything to print? if string.len(line) > 0 then table.insert(TEXT_SPRITE_LIST, fontToSprite(line)) else -- Blank line table.insert(TEXT_SPRITE_LIST, -1) end -- Reset for next line line = fragment fragment = "" doBreak = false end end end -- Search for games.dat files in subdirectories GAME_LIST = {} GAME_COUNT = 0 for dir in lfs.dir(".") do if dir ~= "." and dir ~= ".." then local dirattr = lfs.attributes(dir) if dirattr.mode == "directory" then for file in lfs.dir(dir .. "/.") do if file == "games.dat" then -- Load games.dat GAMES = {} dofile(dir .. "/games.dat") for _, value in pairs(GAMES or {}) do -- Since 3.00 a laserdisc game must say DISC = true; refuse the ambiguous cases here -- with a message rather than letting the engine stop the menu when it is picked. local title = tostring(value.TITLE or value.SCRIPT or "?") if value.VIDEO and not value.DISC then debugPrint(dir .. "/games.dat: \"" .. title .. "\" names a VIDEO without DISC = true; add DISC = true to the entry. Skipped.") elseif value.DISC and not value.VIDEO then debugPrint(dir .. "/games.dat: \"" .. title .. "\" says DISC = true but has no VIDEO. Skipped.") else table.insert(GAME_LIST, value) GAME_COUNT = GAME_COUNT + 1 end end GAMES = {} end end end end end table.sort(GAME_LIST, compareTitles) if GAME_COUNT == 0 then debugPrint("No games found! Exiting.") singeQuit() else overlaySetResolution(discGetWidth(), discGetHeight()) freeSans18 = fontLoad("Singe/FreeSansBold.ttf", 18) fontQuality(FONT_QUALITY_BLENDED) fontSelect(freeSans18) DISC_GRID_START = 180 DISC_LAST_FRAME = 359 MARGIN_X = 25 MARGIN_Y = 25 VIDEO_W = 320 VIDEO_H = 200 MARQUEE_H = 75 MARQUEE_W = VIDEO_W MARQUEE_X = overlayGetWidth() - MARGIN_X - MARQUEE_W MARQUEE_Y = MARGIN_Y VIDEO_X = overlayGetWidth() - MARGIN_X - VIDEO_W VIDEO_Y = MARGIN_Y + MARQUEE_H + MARGIN_Y TEXT_W = VIDEO_W TEXT_X = overlayGetWidth() - MARGIN_X - TEXT_W TEXT_Y = VIDEO_Y + VIDEO_H + MARGIN_Y TEXT_H = overlayGetHeight() - MARGIN_Y - TEXT_Y CABINET_X = MARGIN_X CABINET_Y = MARGIN_Y CABINET_W = VIDEO_X - MARGIN_X - CABINET_X CABINET_H = overlayGetHeight() - MARGIN_Y - CABINET_Y --[[ Cabinet is 325x430 Marquee is 320x75 Video is 320x200 Text is 320x105 --]] --[[ debugPrint("Cabinet is " .. CABINET_W .. "x" .. CABINET_H) debugPrint("Marquee is " .. MARQUEE_W .. "x" .. MARQUEE_H) debugPrint(" Video is " .. VIDEO_W .. "x" .. VIDEO_H) debugPrint(" Text is " .. TEXT_W .. "x" .. TEXT_H) --]] WRAP_BREAK = " [!wb!] " TEXT_LINE_TOP = 1 TEXT_LINE_LIMIT = 0 TEXT_LINE_COUNT = 0 TEXT_LINE_HEIGHT = 0 TEXT_SPRITE_LIST = {} SHUTDOWN_FROM_PUSH = false -- Audio delay calibration screen (SWITCH_SERVICE) CALIBRATING = false CAL_PERIOD = 1000 CAL_STEP_COARSE = 10 CAL_STEP_FINE = 1 CAL_LIMIT = 1000 CAL_LINE_HEIGHT = 24 SND_CLICK = soundLoad("Singe/click.wav") -- Load configuration SHOW_INTRO = true CONFIG_FILE = singeGetDataPath() .. "menu.dat" local confattr = lfs.attributes(CONFIG_FILE) if confattr then dofile(CONFIG_FILE) -- A damaged menu.dat must not take the menu down with it. GAME_SELECTED = tonumber(GAME_SELECTED) or 1 if GAME_SELECTED > GAME_COUNT then GAME_SELECTED = GAME_COUNT end else GAME_SELECTED = 1 end discPlay() if (not SHOW_INTRO) then discSkipToFrame(DISC_GRID_START) end -- Prime the pump loadGameAssets() end