Tools and menu work.
This commit is contained in:
parent
421ef75bdc
commit
f9f67201e3
14 changed files with 1062 additions and 953 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -22,4 +22,3 @@ docs/.asciidoctor/
|
||||||
/Menu.bat
|
/Menu.bat
|
||||||
/data/
|
/data/
|
||||||
screenshots/
|
screenshots/
|
||||||
screenshots-hw/
|
|
||||||
|
|
|
||||||
36
CHANGELOG
36
CHANGELOG
|
|
@ -354,10 +354,10 @@ API Changes
|
||||||
a scrolling list on the left, up and down to select, left and right to
|
a scrolling list on the left, up and down to select, left and right to
|
||||||
page, Start or any button to play, and a mouse works too (click a row
|
page, Start or any button to play, and a mouse works too (click a row
|
||||||
to select it, click it again or the Start button to play, wheel to
|
to select it, click it again or the Start button to play, wheel to
|
||||||
scroll the description). The audio delay calibration screen is a page
|
scroll the description). The service tools are pages of the same
|
||||||
of the same document. The previous overlay-drawn menu is kept for one
|
document. A machine without a GPU device, where guiNew fails, gets the
|
||||||
release as Singe/MenuClassic.singe for a machine without a GPU device,
|
same menu drawn into the overlay instead; see "One menu, two renderers"
|
||||||
where guiNew fails; name it in Menu.sh or Menu.bat to use it.
|
below.
|
||||||
|
|
||||||
- Physics debug drawing. physicsSetDebug(DEBUG_*) draws the bodies in
|
- Physics debug drawing. physicsSetDebug(DEBUG_*) draws the bodies in
|
||||||
wireframe (coloured by motion state), joints and their limits, contact
|
wireframe (coloured by motion state), joints and their limits, contact
|
||||||
|
|
@ -862,14 +862,36 @@ API Changes
|
||||||
things that must not pass through each other -- which is what the
|
things that must not pass through each other -- which is what the
|
||||||
collide calls deliberately do not do.
|
collide calls deliberately do not do.
|
||||||
|
|
||||||
|
- One menu, two renderers. Singe/MenuClassic.singe is gone: it was the
|
||||||
|
whole menu written a second time, and the two copies duplicated their
|
||||||
|
game list, their selection, their menu.dat handling and their games.dat
|
||||||
|
walk almost line for line. Singe/Menu.singe is now everything the menu
|
||||||
|
is and draws nothing, and it picks one of two renderers at startup from
|
||||||
|
the new singeHasGpu(): Singe/MenuDocument.singe for a machine with a
|
||||||
|
GPU device, Singe/MenuOverlay.singe for one without. A machine with no
|
||||||
|
Vulkan, Direct3D 12 or Metal driver therefore gets the menu -- and all
|
||||||
|
ten service tools -- without anyone editing a launcher, which is what
|
||||||
|
editing Menu.sh used to be for.
|
||||||
|
|
||||||
|
Both renderers take the same keys, so the overlay one now moves the
|
||||||
|
selection with up and down and pages with left and right, as the
|
||||||
|
document one does; it shows one game at a time rather than a list, and
|
||||||
|
clips a description too long for its column rather than scrolling it.
|
||||||
|
MENU_OVERLAY = true in menu.dat forces it on a machine that has a GPU.
|
||||||
|
|
||||||
|
singeHasGpu() answers whether the GPU device came up. guiNew and the
|
||||||
|
scene calls end the script rather than returning false, so a game that
|
||||||
|
can do without one had no way to ask.
|
||||||
|
|
||||||
- The service tools. Pressing the SERVICE key on the bundled menu now
|
- The service tools. Pressing the SERVICE key on the bundled menu now
|
||||||
opens a list of ten screens rather than the audio delay screen alone:
|
opens a list of ten screens rather than the audio delay screen alone:
|
||||||
Audio Delay, Input Test, System Information, Light Gun, Sound Test,
|
Audio Delay, Input Test, System Information, Light Gun, Sound Test,
|
||||||
Display, Disc Test, Saved Data, MIDI Ports and Frame Statistics.
|
Display, Disc Test, Saved Data, MIDI Ports and Frame Statistics.
|
||||||
Button 2 backs out of a tool, the service key leaves. They live in
|
Button 2 backs out of a tool, the service key leaves. They live in
|
||||||
Singe/Tools.singe, which the engine extracts beside Menu.singe, and
|
Singe/Tools.singe, which the engine extracts beside Menu.singe. Nothing
|
||||||
each is a page of Singe/Menu.rml, so a cabinet builder can add one.
|
there draws: a tool builds a list of rows and the menu draws them, so
|
||||||
MenuClassic keeps the audio delay screen and nothing else.
|
the same ten run on both renderers and a cabinet builder adding one
|
||||||
|
writes no drawing code at all.
|
||||||
|
|
||||||
Four calls were added for them, and are worth having on their own:
|
Four calls were added for them, and are worth having on their own:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,8 @@ singeEmbed(${CMAKE_SOURCE_DIR}/assets/controls.cfg ${GENERATED_DIR}/controls_cfg
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/settings.cfg ${GENERATED_DIR}/settings_cfg.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/settings.cfg ${GENERATED_DIR}/settings_cfg.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Tools.singe ${GENERATED_DIR}/Tools_singe.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Tools.singe ${GENERATED_DIR}/Tools_singe.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuClassic.singe ${GENERATED_DIR}/MenuClassic_singe.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuDocument.singe ${GENERATED_DIR}/MenuDocument_singe.h "")
|
||||||
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuOverlay.singe ${GENERATED_DIR}/MenuOverlay_singe.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.rml ${GENERATED_DIR}/Menu_rml.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.rml ${GENERATED_DIR}/Menu_rml.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/menu.rcss ${GENERATED_DIR}/menu_rcss.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/menu.rcss ${GENERATED_DIR}/menu_rcss.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/gui.rcss ${GENERATED_DIR}/gui_rcss.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/gui.rcss ${GENERATED_DIR}/gui_rcss.h "")
|
||||||
|
|
|
||||||
|
|
@ -32,68 +32,11 @@
|
||||||
<button id="start">Start</button>
|
<button id="start">Start</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tools" class="tool hidden">
|
<!-- Every service tool draws through this one page: Singe/Tools.singe hands the menu a list of
|
||||||
<h1>Service Tools</h1>
|
rows and Singe/MenuDocument.singe turns them into the divs below it. -->
|
||||||
<div id="toolList"></div>
|
<div id="tool" class="tool hidden">
|
||||||
<p class="value" id="toolHelp"></p>
|
<h1 id="toolTitle"></h1>
|
||||||
<p class="keys">Up / Down: choose Button 1: open Service: back to the games</p>
|
<div id="toolBody"></div>
|
||||||
</div>
|
|
||||||
<div id="toolAudio" class="tool hidden">
|
|
||||||
<h1>Audio Delay</h1>
|
|
||||||
<p>Adjust until the flash and the click happen together.</p>
|
|
||||||
<p class="value">Delay: <span id="calValue"></span> ms</p>
|
|
||||||
<p class="value">Measured device queue: <span id="calLatency"></span> ms</p>
|
|
||||||
<p class="keys">Left / Right: 10 ms Up / Down: 1 ms Start: reset to 0</p>
|
|
||||||
<p class="keys">Button 1: save Button 2: back</p>
|
|
||||||
</div>
|
|
||||||
<div id="toolInput" class="tool hidden">
|
|
||||||
<h1>Input Test</h1>
|
|
||||||
<div id="inputRows"></div>
|
|
||||||
<p class="keys">Press anything. Button 2: back</p>
|
|
||||||
</div>
|
|
||||||
<div id="toolSystem" class="tool hidden">
|
|
||||||
<h1>System Information</h1>
|
|
||||||
<div id="systemRows"></div>
|
|
||||||
<p class="keys">Button 2: back</p>
|
|
||||||
</div>
|
|
||||||
<div id="toolGun" class="tool clear hidden">
|
|
||||||
<h1>Light Gun</h1>
|
|
||||||
<p class="value">Last shot: <span id="gunLast"></span> shots shown: <span id="gunCount"></span></p>
|
|
||||||
<p class="keys">Button 1: fire Start: clear Button 2: back</p>
|
|
||||||
</div>
|
|
||||||
<div id="toolSound" class="tool hidden">
|
|
||||||
<h1>Sound Test</h1>
|
|
||||||
<p class="value">Playing: <span id="soundWhich"></span></p>
|
|
||||||
<p class="value">Disc audio tracks: <span id="soundTracks"></span></p>
|
|
||||||
<p class="keys">Button 1: left, right, both Left / Right: disc track Button 2: back</p>
|
|
||||||
</div>
|
|
||||||
<div id="toolDisplay" class="tool clear hidden">
|
|
||||||
<h1>Display</h1>
|
|
||||||
<p class="value" id="dispValues"></p>
|
|
||||||
<p class="value" id="dispSaved"></p>
|
|
||||||
<p class="keys">Arrows: move Button 3 / 4: size Coin 1: rotate Start: reset</p>
|
|
||||||
<p class="keys">Button 1: keep for this machine Button 2: back</p>
|
|
||||||
</div>
|
|
||||||
<div id="toolDisc" class="tool hidden">
|
|
||||||
<h1>Disc Test</h1>
|
|
||||||
<div id="discRows"></div>
|
|
||||||
<p class="keys">Button 1: run Button 2: back</p>
|
|
||||||
</div>
|
|
||||||
<div id="toolSaved" class="tool hidden">
|
|
||||||
<h1>Saved Data</h1>
|
|
||||||
<div id="savedRows"></div>
|
|
||||||
<p class="keys">Up / Down: choose Button 3: delete this game's save Button 2: back</p>
|
|
||||||
</div>
|
|
||||||
<div id="toolMidi" class="tool hidden">
|
|
||||||
<h1>MIDI Ports</h1>
|
|
||||||
<div id="midiRows"></div>
|
|
||||||
<p class="keys">Button 1: send a note Button 3: look again Button 2: back</p>
|
|
||||||
</div>
|
|
||||||
<div id="toolStats" class="tool hidden">
|
|
||||||
<h1>Frame Statistics</h1>
|
|
||||||
<p class="value">The overlay is <span id="statsState"></span>.</p>
|
|
||||||
<p>It stays on while you play, in the top left corner.</p>
|
|
||||||
<p class="keys">Button 1: turn it on or off Button 2: back</p>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</rml>
|
</rml>
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,18 @@
|
||||||
--]]
|
--]]
|
||||||
|
|
||||||
|
|
||||||
-- The bundled game menu: an RmlUi document (Singe/Menu.rml styled by Singe/menu.rcss) over the
|
|
||||||
-- disc's neon grid, filled and driven from here. The script keeps the selection, places every
|
-- The bundled game menu. Everything the menu *is* lives here: the game list read from every
|
||||||
-- region of the document from the overlay size (so the attract video it draws into the overlay
|
-- games.dat, the selection and how it moves, the attract video, launching a game and coming back,
|
||||||
-- lands in the hole the document leaves for it) and launches games with scriptPush.
|
-- and the service tools. Nothing here draws.
|
||||||
-- Singe/MenuClassic.singe is the previous, sprite drawn menu, kept for one release.
|
--
|
||||||
|
-- Drawing is a renderer, chosen once at startup by whether this machine has a GPU device:
|
||||||
|
--
|
||||||
|
-- Singe/MenuDocument.singe an RmlUi document, which needs the GPU device the 3D scene uses
|
||||||
|
-- Singe/MenuOverlay.singe sprites and fontPrint straight into the overlay, which needs nothing
|
||||||
|
--
|
||||||
|
-- The two draw the same menu and take the same keys. A renderer provides the MENU_RENDER table;
|
||||||
|
-- see the top of either file for what is in it.
|
||||||
|
|
||||||
dofile("Singe/Framework.singe")
|
dofile("Singe/Framework.singe")
|
||||||
dofile("Singe/Tools.singe")
|
dofile("Singe/Tools.singe")
|
||||||
|
|
@ -50,18 +57,15 @@ function compareTitles(a, b)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-- Makes a game the selection, wrapping at either end of the list, and shows its assets.
|
-- Makes a game the selection, wrapping at either end of the list, and shows it.
|
||||||
function gameSelect(index)
|
function gameSelect(index)
|
||||||
if index < 1 then
|
if index < 1 then
|
||||||
index = GAME_COUNT
|
index = GAME_COUNT
|
||||||
elseif index > GAME_COUNT then
|
elseif index > GAME_COUNT then
|
||||||
index = 1
|
index = 1
|
||||||
end
|
end
|
||||||
menuRow(GAME_SELECTED):SetClass("selected", false)
|
|
||||||
GAME_SELECTED = index
|
|
||||||
menuRow(GAME_SELECTED):SetClass("selected", true)
|
|
||||||
LIST_SCROLL_PENDING = true
|
|
||||||
unloadGameAssets()
|
unloadGameAssets()
|
||||||
|
GAME_SELECTED = index
|
||||||
loadGameAssets()
|
loadGameAssets()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -82,17 +86,7 @@ function loadGameAssets()
|
||||||
end
|
end
|
||||||
videoPlay(VIDEO_ATTRACT)
|
videoPlay(VIDEO_ATTRACT)
|
||||||
videoSeek(VIDEO_ATTRACT, game.ATTRACT_START)
|
videoSeek(VIDEO_ATTRACT, game.ATTRACT_START)
|
||||||
|
MENU_RENDER.showGame(game)
|
||||||
-- The document wraps the text itself.
|
|
||||||
menuArt("poster", game.CABINET)
|
|
||||||
menuArt("marquee", game.MARQUEE)
|
|
||||||
guiSetValue(GUI, DOCUMENT, "title", menuEscape(game.TITLE))
|
|
||||||
guiSetValue(GUI, DOCUMENT, "credits", menuLabel("Year", game.YEAR) .. menuLabel("Genre", game.GENRE) .. menuLabel("Platform", game.PLATFORM))
|
|
||||||
guiSetValue(GUI, DOCUMENT, "developer", menuLabel("Developer", game.DEVELOPER))
|
|
||||||
guiSetValue(GUI, DOCUMENT, "publisher", menuLabel("Publisher", game.PUBLISHER))
|
|
||||||
guiSetValue(GUI, DOCUMENT, "port", menuLabel("Singe port", game.CREATOR) .. menuLabel("Source", game.SOURCE))
|
|
||||||
guiSetValue(GUI, DOCUMENT, "description", menuEscape(game.DESCRIPTION))
|
|
||||||
menuElement("description").scroll_top = 0
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -102,152 +96,6 @@ function menuActive()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-- Fits a game's picture inside one of the art regions. The document lives in Singe/, so a game's
|
|
||||||
-- own file is reached from there through "../"; the engine's files are named as they are.
|
|
||||||
function menuArt(id, file)
|
|
||||||
local path = tostring(file or "")
|
|
||||||
|
|
||||||
if path:sub(1, 6):lower() ~= "singe/" then
|
|
||||||
path = "../" .. path
|
|
||||||
end
|
|
||||||
menuElement(id).style.decorator = 'image("' .. path .. '" contain)'
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- Fills the game list with one row per game and routes their clicks here.
|
|
||||||
function menuBuildList()
|
|
||||||
local rows = {}
|
|
||||||
|
|
||||||
for index, game in ipairs(GAME_LIST) do
|
|
||||||
rows[index] = '<div id="game' .. index .. '">' .. menuEscape(game.TITLE) .. '</div>'
|
|
||||||
end
|
|
||||||
menuElement("list").inner_rml = table.concat(rows)
|
|
||||||
for index = 1, GAME_COUNT do
|
|
||||||
guiSetHandler(GUI, DOCUMENT, "game" .. index, "click", menuRowClick)
|
|
||||||
end
|
|
||||||
guiSetHandler(GUI, DOCUMENT, "start", "click", menuStartClick)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function menuElement(id)
|
|
||||||
return rmlui.contexts["gui" .. GUI].documents["menu"]:GetElementById(id)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- Text on its way into the document, with RML's special characters made harmless.
|
|
||||||
function menuEscape(text)
|
|
||||||
local output = tostring(text or "")
|
|
||||||
|
|
||||||
output = string.gsub(output, "&", "&")
|
|
||||||
output = string.gsub(output, "<", "<")
|
|
||||||
output = string.gsub(output, ">", ">")
|
|
||||||
return output
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- One "Label: value" fragment of a details line.
|
|
||||||
function menuLabel(label, value)
|
|
||||||
return '<span class="label">' .. label .. ':</span> ' .. menuEscape(value) .. ' '
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- Lays the document out from the overlay size. The list fills the left, the poster stands beside
|
|
||||||
-- it, the marquee sits over the attract video on the right, the details span both under them and
|
|
||||||
-- the key hints run along the bottom. The video rectangle is the same one videoDraw is given.
|
|
||||||
function menuLayout()
|
|
||||||
local width = overlayGetWidth()
|
|
||||||
local height = overlayGetHeight()
|
|
||||||
local sideX = MARGIN + LIST_W + GAP + POSTER_W + GAP
|
|
||||||
local sideW = width - MARGIN - sideX
|
|
||||||
local listH = height - MARGIN * 2 - FOOTER_H - GAP
|
|
||||||
local posterX = MARGIN + LIST_W + GAP
|
|
||||||
local footerY = height - MARGIN - FOOTER_H
|
|
||||||
local infoY = 0
|
|
||||||
|
|
||||||
MARQUEE_H = math.floor(sideW * MARQUEE_ASPECT_H / MARQUEE_ASPECT_W)
|
|
||||||
VIDEO_X = sideX
|
|
||||||
VIDEO_Y = MARGIN + MARQUEE_H + GAP
|
|
||||||
VIDEO_W = sideW
|
|
||||||
VIDEO_H = math.floor(sideW * VIDEO_ASPECT_H / VIDEO_ASPECT_W)
|
|
||||||
infoY = VIDEO_Y + VIDEO_H + GAP
|
|
||||||
|
|
||||||
menuPlace("list", MARGIN, MARGIN, LIST_W, listH)
|
|
||||||
menuPlace("poster", posterX, MARGIN, POSTER_W, VIDEO_Y + VIDEO_H - MARGIN)
|
|
||||||
menuPlace("marquee", sideX, MARGIN, sideW, MARQUEE_H)
|
|
||||||
menuPlace("video", VIDEO_X, VIDEO_Y, VIDEO_W, VIDEO_H)
|
|
||||||
menuPlace("info", posterX, infoY, width - MARGIN - posterX, MARGIN + listH - infoY)
|
|
||||||
menuPlace("footer", MARGIN, footerY, width - MARGIN * 2, FOOTER_H)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- Rows that fit in the list at once: what left and right page by.
|
|
||||||
function menuPageSize()
|
|
||||||
local list = menuElement("list")
|
|
||||||
local row = menuRow(GAME_SELECTED)
|
|
||||||
|
|
||||||
if row.offset_height <= 0 then
|
|
||||||
return 1
|
|
||||||
end
|
|
||||||
return math.max(1, math.floor(list.client_height / row.offset_height))
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function menuPlace(id, x, y, w, h)
|
|
||||||
local element = menuElement(id)
|
|
||||||
|
|
||||||
element:SetClass("placed", true)
|
|
||||||
element.style.left = x .. "px"
|
|
||||||
element.style.top = y .. "px"
|
|
||||||
element.style.width = w .. "px"
|
|
||||||
element.style.height = h .. "px"
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function menuRow(index)
|
|
||||||
return menuElement("game" .. index)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- A click on a row selects its game; a click on the selected row starts it.
|
|
||||||
function menuRowClick(gui, document, id, event, value)
|
|
||||||
local index = tonumber(id:sub(5))
|
|
||||||
|
|
||||||
if not menuActive() then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if index == GAME_SELECTED then
|
|
||||||
gameStart()
|
|
||||||
else
|
|
||||||
gameSelect(index)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- Brings the selected row into view once the document has been laid out.
|
|
||||||
function menuScrollToSelection()
|
|
||||||
local list = menuElement("list")
|
|
||||||
local row = menuRow(GAME_SELECTED)
|
|
||||||
local top = row.offset_top - list.scroll_top
|
|
||||||
|
|
||||||
if list.client_height <= 0 then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if top < 0 then
|
|
||||||
row:ScrollIntoView(true)
|
|
||||||
elseif top + row.offset_height > list.client_height then
|
|
||||||
row:ScrollIntoView(false)
|
|
||||||
end
|
|
||||||
LIST_SCROLL_PENDING = false
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function menuStartClick(gui, document, id, event, value)
|
|
||||||
if menuActive() then
|
|
||||||
gameStart()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function onInputPressed(what)
|
function onInputPressed(what)
|
||||||
|
|
||||||
-- The service tools take every switch while they are up, including the one that closes them.
|
-- The service tools take every switch while they are up, including the one that closes them.
|
||||||
|
|
@ -270,13 +118,13 @@ function onInputPressed(what)
|
||||||
if GAME_SELECTED == 1 then
|
if GAME_SELECTED == 1 then
|
||||||
gameSelect(GAME_COUNT)
|
gameSelect(GAME_COUNT)
|
||||||
else
|
else
|
||||||
gameSelect(math.max(1, GAME_SELECTED - menuPageSize()))
|
gameSelect(math.max(1, GAME_SELECTED - MENU_RENDER.pageSize()))
|
||||||
end
|
end
|
||||||
elseif what == SWITCH_RIGHT then
|
elseif what == SWITCH_RIGHT then
|
||||||
if GAME_SELECTED == GAME_COUNT then
|
if GAME_SELECTED == GAME_COUNT then
|
||||||
gameSelect(1)
|
gameSelect(1)
|
||||||
else
|
else
|
||||||
gameSelect(math.min(GAME_COUNT, GAME_SELECTED + menuPageSize()))
|
gameSelect(math.min(GAME_COUNT, GAME_SELECTED + MENU_RENDER.pageSize()))
|
||||||
end
|
end
|
||||||
elseif what == SWITCH_START1 or what == SWITCH_START2 or what == SWITCH_BUTTON1 or what == SWITCH_BUTTON2 or what == SWITCH_BUTTON3 or what == SWITCH_BUTTON4 then
|
elseif what == SWITCH_START1 or what == SWITCH_START2 or what == SWITCH_BUTTON1 or what == SWITCH_BUTTON2 or what == SWITCH_BUTTON3 or what == SWITCH_BUTTON4 then
|
||||||
gameStart()
|
gameStart()
|
||||||
|
|
@ -303,23 +151,17 @@ function onOverlayUpdate()
|
||||||
|
|
||||||
if toolsActive() then
|
if toolsActive() then
|
||||||
toolsUpdate()
|
toolsUpdate()
|
||||||
guiDraw(GUI)
|
MENU_RENDER.tools()
|
||||||
return(OVERLAY_UPDATED)
|
return(OVERLAY_UPDATED)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Are we displaying the grid background?
|
-- Are we displaying the grid background?
|
||||||
if menuActive() then
|
if menuActive() then
|
||||||
|
-- The attract clip runs between the two frames the game's entry names.
|
||||||
-- Attract Mode Video, under the hole the document leaves for it
|
|
||||||
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
|
if videoGetFrame(VIDEO_ATTRACT) > GAME_LIST[GAME_SELECTED].ATTRACT_END then
|
||||||
videoSeek(VIDEO_ATTRACT, GAME_LIST[GAME_SELECTED].ATTRACT_START)
|
videoSeek(VIDEO_ATTRACT, GAME_LIST[GAME_SELECTED].ATTRACT_START)
|
||||||
end
|
end
|
||||||
|
MENU_RENDER.frame()
|
||||||
if LIST_SCROLL_PENDING then
|
|
||||||
menuScrollToSelection()
|
|
||||||
end
|
|
||||||
guiDraw(GUI)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return(OVERLAY_UPDATED)
|
return(OVERLAY_UPDATED)
|
||||||
|
|
@ -330,9 +172,7 @@ end
|
||||||
function onShutdown()
|
function onShutdown()
|
||||||
unloadGameAssets()
|
unloadGameAssets()
|
||||||
saveConfig(not SHUTDOWN_FROM_PUSH)
|
saveConfig(not SHUTDOWN_FROM_PUSH)
|
||||||
if GUI then
|
MENU_RENDER.finish()
|
||||||
guiDelete(GUI)
|
|
||||||
end
|
|
||||||
if SND_CLICK then
|
if SND_CLICK then
|
||||||
soundUnload(SND_CLICK)
|
soundUnload(SND_CLICK)
|
||||||
end
|
end
|
||||||
|
|
@ -346,6 +186,11 @@ function saveConfig(showIntro)
|
||||||
if cfg then
|
if cfg then
|
||||||
cfg:write("GAME_SELECTED = " .. GAME_SELECTED .. "\n")
|
cfg:write("GAME_SELECTED = " .. GAME_SELECTED .. "\n")
|
||||||
cfg:write("SHOW_INTRO = " .. tostring(showIntro) .. "\n")
|
cfg:write("SHOW_INTRO = " .. tostring(showIntro) .. "\n")
|
||||||
|
-- The file is written whole, so a renderer forced here has to be written back or it
|
||||||
|
-- would last exactly one run.
|
||||||
|
if MENU_OVERLAY then
|
||||||
|
cfg:write("MENU_OVERLAY = true\n")
|
||||||
|
end
|
||||||
cfg:close()
|
cfg:close()
|
||||||
else
|
else
|
||||||
debugPrint("Unable to write " .. CONFIG_FILE)
|
debugPrint("Unable to write " .. CONFIG_FILE)
|
||||||
|
|
@ -360,6 +205,7 @@ function unloadGameAssets()
|
||||||
videoUnload(VIDEO_ATTRACT)
|
videoUnload(VIDEO_ATTRACT)
|
||||||
VIDEO_ATTRACT = nil
|
VIDEO_ATTRACT = nil
|
||||||
end
|
end
|
||||||
|
MENU_RENDER.hideGame()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -416,24 +262,10 @@ else
|
||||||
DISC_GRID_START = 180
|
DISC_GRID_START = 180
|
||||||
DISC_LAST_FRAME = 359
|
DISC_LAST_FRAME = 359
|
||||||
|
|
||||||
-- Layout, in overlay pixels: the columns' widths and the space around them; everything else
|
|
||||||
-- follows from the overlay size in menuLayout. The marquee and video keep the classic menu's
|
|
||||||
-- 320x75 and 320x200 proportions.
|
|
||||||
MARGIN = 10
|
|
||||||
GAP = 8
|
|
||||||
LIST_W = 200
|
|
||||||
POSTER_W = 180
|
|
||||||
FOOTER_H = 24
|
|
||||||
MARQUEE_ASPECT_W = 320
|
|
||||||
MARQUEE_ASPECT_H = 75
|
|
||||||
VIDEO_ASPECT_W = 320
|
|
||||||
VIDEO_ASPECT_H = 200
|
|
||||||
|
|
||||||
INTRO_DONE = false
|
INTRO_DONE = false
|
||||||
LIST_SCROLL_PENDING = false
|
|
||||||
SHUTDOWN_FROM_PUSH = false
|
SHUTDOWN_FROM_PUSH = false
|
||||||
|
|
||||||
-- The service tools (SWITCH_SERVICE) share this click with the audio delay screen.
|
-- The service tools share this click with the audio delay tool.
|
||||||
SND_CLICK = soundLoad("Singe/click.wav")
|
SND_CLICK = soundLoad("Singe/click.wav")
|
||||||
|
|
||||||
-- Load configuration
|
-- Load configuration
|
||||||
|
|
@ -451,13 +283,19 @@ else
|
||||||
GAME_SELECTED = 1
|
GAME_SELECTED = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The document: the size of the overlay, composited over it every frame, and first in line
|
-- The renderer. A GUI document is drawn through the GPU device the 3D scene uses, so without
|
||||||
-- for the mouse and the switches so its rows and Start button can be clicked.
|
-- one there is no document to build and guiNew would end the script rather than answer false;
|
||||||
GUI = guiNew(overlayGetWidth(), overlayGetHeight())
|
-- the overlay renderer needs nothing but the overlay every game already draws into. Set
|
||||||
DOCUMENT = guiLoad(GUI, "Singe/Menu.rml")
|
-- MENU_OVERLAY = true in menu.dat to use it on a machine that has a GPU, which is how the
|
||||||
guiSetInput(GUI, true)
|
-- overlay path gets tested.
|
||||||
menuLayout()
|
if singeHasGpu() and not MENU_OVERLAY then
|
||||||
menuBuildList()
|
dofile("Singe/MenuDocument.singe")
|
||||||
|
else
|
||||||
|
dofile("Singe/MenuOverlay.singe")
|
||||||
|
end
|
||||||
|
MENU_RENDER.begin()
|
||||||
|
MENU_RENDER.layout()
|
||||||
|
MENU_RENDER.buildList()
|
||||||
|
|
||||||
discPlay()
|
discPlay()
|
||||||
if (not SHOW_INTRO) then
|
if (not SHOW_INTRO) then
|
||||||
|
|
|
||||||
|
|
@ -1,547 +0,0 @@
|
||||||
--[[
|
|
||||||
*
|
|
||||||
* Singe 3
|
|
||||||
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
|
|
||||||
|
|
||||||
-- Adds the entries of one games.dat. container is the game database the file came from, or nil.
|
|
||||||
local function loadGamesDat(source, container)
|
|
||||||
GAMES = {}
|
|
||||||
local ok, err = pcall(dofile, source)
|
|
||||||
if not ok then
|
|
||||||
debugPrint(source .. ": " .. tostring(err) .. " Skipped.")
|
|
||||||
GAMES = {}
|
|
||||||
return
|
|
||||||
end
|
|
||||||
for _, value in pairs(GAMES or {}) do
|
|
||||||
if container then
|
|
||||||
-- Packed game: the engine opens its files through the database, and so does the menu.
|
|
||||||
value.CONTAINER = container
|
|
||||||
for _, key in ipairs({ "CABINET", "MARQUEE", "ATTRACT" }) do
|
|
||||||
if value[key] and value[key]:sub(1, 6):lower() ~= "singe/" then
|
|
||||||
value[key] = container .. "/" .. value[key]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.insert(GAME_LIST, value)
|
|
||||||
GAME_COUNT = GAME_COUNT + 1
|
|
||||||
end
|
|
||||||
GAMES = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Search for games.dat files in subdirectories and inside .game databases
|
|
||||||
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
|
|
||||||
loadGamesDat(dir .. "/games.dat", nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif dirattr.mode == "file" and dir:sub(-5):lower() == ".game" then
|
|
||||||
loadGamesDat(dir .. "/games.dat", dir)
|
|
||||||
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
|
|
||||||
301
assets/MenuDocument.singe
Normal file
301
assets/MenuDocument.singe
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
--[[
|
||||||
|
*
|
||||||
|
* Singe 3
|
||||||
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
--]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- The menu drawn as an RmlUi document: Singe/Menu.rml styled by Singe/menu.rcss, composited over
|
||||||
|
-- the overlay, with the attract video drawn into the overlay underneath the hole the document
|
||||||
|
-- leaves for it. This needs the GPU device the 3D scene uses; Singe/MenuOverlay.singe is the one
|
||||||
|
-- for a machine without one. Singe/Menu.singe loads exactly one of the two and calls it through
|
||||||
|
-- MENU_RENDER:
|
||||||
|
--
|
||||||
|
-- begin() build what drawing needs
|
||||||
|
-- finish() give it back
|
||||||
|
-- layout() place the regions, from the overlay size
|
||||||
|
-- buildList() the whole game list is known
|
||||||
|
-- showGame(game) this is the selection now
|
||||||
|
-- hideGame() let go of the last selection
|
||||||
|
-- pageSize() how many rows left and right should move by
|
||||||
|
-- frame() draw the game page
|
||||||
|
-- tools() draw the service tools page
|
||||||
|
|
||||||
|
|
||||||
|
-- Layout, in overlay pixels: the columns' widths and the space around them; everything else
|
||||||
|
-- follows from the overlay size in layout(). The marquee and video keep the overlay menu's
|
||||||
|
-- 320x75 and 320x200 proportions.
|
||||||
|
local MARGIN = 10
|
||||||
|
local GAP = 8
|
||||||
|
local LIST_W = 200
|
||||||
|
local POSTER_W = 180
|
||||||
|
local FOOTER_H = 24
|
||||||
|
local MARQUEE_ASPECT_W = 320
|
||||||
|
local MARQUEE_ASPECT_H = 75
|
||||||
|
local VIDEO_ASPECT_W = 320
|
||||||
|
local VIDEO_ASPECT_H = 200
|
||||||
|
|
||||||
|
local videoX, videoY, videoW, videoH = 0, 0, 0, 0
|
||||||
|
local scrollPending = false
|
||||||
|
|
||||||
|
|
||||||
|
function menuElement(id)
|
||||||
|
return rmlui.contexts["gui" .. GUI].documents["menu"]:GetElementById(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Text on its way into the document, with RML's special characters made harmless.
|
||||||
|
function menuEscape(text)
|
||||||
|
local output = tostring(text or "")
|
||||||
|
|
||||||
|
output = string.gsub(output, "&", "&")
|
||||||
|
output = string.gsub(output, "<", "<")
|
||||||
|
output = string.gsub(output, ">", ">")
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Fits a game's picture inside one of the art regions. The document lives in Singe/, so a game's
|
||||||
|
-- own file is reached from there through "../"; the engine's files are named as they are.
|
||||||
|
local function menuArt(id, file)
|
||||||
|
local path = tostring(file or "")
|
||||||
|
|
||||||
|
if path:sub(1, 6):lower() ~= "singe/" then
|
||||||
|
path = "../" .. path
|
||||||
|
end
|
||||||
|
menuElement(id).style.decorator = 'image("' .. path .. '" contain)'
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- One "Label: value" fragment of a details line.
|
||||||
|
local function menuLabel(label, value)
|
||||||
|
return '<span class="label">' .. label .. ':</span> ' .. menuEscape(value) .. ' '
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function menuPlace(id, x, y, w, h)
|
||||||
|
local element = menuElement(id)
|
||||||
|
|
||||||
|
element:SetClass("placed", true)
|
||||||
|
element.style.left = x .. "px"
|
||||||
|
element.style.top = y .. "px"
|
||||||
|
element.style.width = w .. "px"
|
||||||
|
element.style.height = h .. "px"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function menuRow(index)
|
||||||
|
return menuElement("game" .. index)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A click on a row selects its game; a click on the selected row starts it.
|
||||||
|
local function menuRowClick(gui, document, id, event, value)
|
||||||
|
local index = tonumber(id:sub(5))
|
||||||
|
|
||||||
|
if not menuActive() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if index == GAME_SELECTED then
|
||||||
|
gameStart()
|
||||||
|
else
|
||||||
|
gameSelect(index)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function menuStartClick(gui, document, id, event, value)
|
||||||
|
if menuActive() then
|
||||||
|
gameStart()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Brings the selected row into view once the document has been laid out.
|
||||||
|
local function menuScrollToSelection()
|
||||||
|
local list = menuElement("list")
|
||||||
|
local row = menuRow(GAME_SELECTED)
|
||||||
|
local top = row.offset_top - list.scroll_top
|
||||||
|
|
||||||
|
if list.client_height <= 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if top < 0 then
|
||||||
|
row:ScrollIntoView(true)
|
||||||
|
elseif top + row.offset_height > list.client_height then
|
||||||
|
row:ScrollIntoView(false)
|
||||||
|
end
|
||||||
|
scrollPending = false
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A tool lines its columns up with runs of spaces, which RML would collapse to one, so they are
|
||||||
|
-- made non-breaking on the way in. Only the tools do this: a game's description is meant to wrap.
|
||||||
|
local function menuSpaces(text)
|
||||||
|
return (string.gsub(menuEscape(text), " +", function(run) return string.rep(" ", #run) end))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- One row of a service tool, as RML. The kinds are the ones Singe/Tools.singe builds.
|
||||||
|
local function toolRow(row)
|
||||||
|
if row.kind == "pair" then
|
||||||
|
return string.format("<div class='pair'><span class='label'>%s</span><span class='detail'>%s</span></div>",
|
||||||
|
menuEscape(row.label), menuSpaces(row.value))
|
||||||
|
elseif row.kind == "sub" then
|
||||||
|
return "<div class='sub'>" .. menuSpaces(row.text) .. "</div>"
|
||||||
|
elseif row.kind == "keys" then
|
||||||
|
return "<p class='keys'>" .. menuSpaces(row.text) .. "</p>"
|
||||||
|
elseif row.kind == "item" then
|
||||||
|
return string.format("<div class='%s'>%s</div>", row.selected and "item selected" or "item", menuSpaces(row.text))
|
||||||
|
elseif row.kind == "blank" then
|
||||||
|
return "<div class='blank'></div>"
|
||||||
|
end
|
||||||
|
return "<div class='row'>" .. menuSpaces(row.text) .. "</div>"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER = {}
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.begin = function()
|
||||||
|
-- The document: the size of the overlay, composited over it every frame, and first in line
|
||||||
|
-- for the mouse and the switches so its rows and Start button can be clicked.
|
||||||
|
GUI = guiNew(overlayGetWidth(), overlayGetHeight())
|
||||||
|
DOCUMENT = guiLoad(GUI, "Singe/Menu.rml")
|
||||||
|
guiSetInput(GUI, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.finish = function()
|
||||||
|
if GUI then
|
||||||
|
guiDelete(GUI)
|
||||||
|
GUI = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Lays the document out from the overlay size. The list fills the left, the poster stands beside
|
||||||
|
-- it, the marquee sits over the attract video on the right, the details span both under them and
|
||||||
|
-- the key hints run along the bottom. The video rectangle is the same one videoDraw is given.
|
||||||
|
MENU_RENDER.layout = function()
|
||||||
|
local width = overlayGetWidth()
|
||||||
|
local height = overlayGetHeight()
|
||||||
|
local sideX = MARGIN + LIST_W + GAP + POSTER_W + GAP
|
||||||
|
local sideW = width - MARGIN - sideX
|
||||||
|
local listH = height - MARGIN * 2 - FOOTER_H - GAP
|
||||||
|
local posterX = MARGIN + LIST_W + GAP
|
||||||
|
local footerY = height - MARGIN - FOOTER_H
|
||||||
|
local marqueeH = math.floor(sideW * MARQUEE_ASPECT_H / MARQUEE_ASPECT_W)
|
||||||
|
local infoY = 0
|
||||||
|
|
||||||
|
videoX = sideX
|
||||||
|
videoY = MARGIN + marqueeH + GAP
|
||||||
|
videoW = sideW
|
||||||
|
videoH = math.floor(sideW * VIDEO_ASPECT_H / VIDEO_ASPECT_W)
|
||||||
|
infoY = videoY + videoH + GAP
|
||||||
|
|
||||||
|
menuPlace("list", MARGIN, MARGIN, LIST_W, listH)
|
||||||
|
menuPlace("poster", posterX, MARGIN, POSTER_W, videoY + videoH - MARGIN)
|
||||||
|
menuPlace("marquee", sideX, MARGIN, sideW, marqueeH)
|
||||||
|
menuPlace("video", videoX, videoY, videoW, videoH)
|
||||||
|
menuPlace("info", posterX, infoY, width - MARGIN - posterX, MARGIN + listH - infoY)
|
||||||
|
menuPlace("footer", MARGIN, footerY, width - MARGIN * 2, FOOTER_H)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Fills the game list with one row per game and routes their clicks here.
|
||||||
|
MENU_RENDER.buildList = function()
|
||||||
|
local rows = {}
|
||||||
|
|
||||||
|
for index, game in ipairs(GAME_LIST) do
|
||||||
|
rows[index] = '<div id="game' .. index .. '">' .. menuEscape(game.TITLE) .. '</div>'
|
||||||
|
end
|
||||||
|
menuElement("list").inner_rml = table.concat(rows)
|
||||||
|
for index = 1, GAME_COUNT do
|
||||||
|
guiSetHandler(GUI, DOCUMENT, "game" .. index, "click", menuRowClick)
|
||||||
|
end
|
||||||
|
guiSetHandler(GUI, DOCUMENT, "start", "click", menuStartClick)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.showGame = function(game)
|
||||||
|
menuRow(GAME_SELECTED):SetClass("selected", true)
|
||||||
|
scrollPending = true
|
||||||
|
menuArt("poster", game.CABINET)
|
||||||
|
menuArt("marquee", game.MARQUEE)
|
||||||
|
guiSetValue(GUI, DOCUMENT, "title", menuEscape(game.TITLE))
|
||||||
|
guiSetValue(GUI, DOCUMENT, "credits", menuLabel("Year", game.YEAR) .. menuLabel("Genre", game.GENRE) .. menuLabel("Platform", game.PLATFORM))
|
||||||
|
guiSetValue(GUI, DOCUMENT, "developer", menuLabel("Developer", game.DEVELOPER))
|
||||||
|
guiSetValue(GUI, DOCUMENT, "publisher", menuLabel("Publisher", game.PUBLISHER))
|
||||||
|
guiSetValue(GUI, DOCUMENT, "port", menuLabel("Singe port", game.CREATOR) .. menuLabel("Source", game.SOURCE))
|
||||||
|
guiSetValue(GUI, DOCUMENT, "description", menuEscape(game.DESCRIPTION))
|
||||||
|
menuElement("description").scroll_top = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.hideGame = function()
|
||||||
|
-- The document keeps nothing of its own per game; only the highlight has to come off.
|
||||||
|
if GAME_SELECTED then
|
||||||
|
menuRow(GAME_SELECTED):SetClass("selected", false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Rows that fit in the list at once: what left and right page by.
|
||||||
|
MENU_RENDER.pageSize = function()
|
||||||
|
local list = menuElement("list")
|
||||||
|
local row = menuRow(GAME_SELECTED)
|
||||||
|
|
||||||
|
if row.offset_height <= 0 then
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
return math.max(1, math.floor(list.client_height / row.offset_height))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.frame = function()
|
||||||
|
-- Attract Mode Video, under the hole the document leaves for it
|
||||||
|
videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
|
||||||
|
if scrollPending then
|
||||||
|
menuScrollToSelection()
|
||||||
|
end
|
||||||
|
guiDraw(GUI)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.tools = function()
|
||||||
|
local rows = {}
|
||||||
|
|
||||||
|
-- The tools redraw only when they say something changed; the document keeps what it was given.
|
||||||
|
if TOOL_DIRTY then
|
||||||
|
TOOL_DIRTY = false
|
||||||
|
for _, row in ipairs(TOOL_ROWS) do
|
||||||
|
rows[#rows + 1] = toolRow(row)
|
||||||
|
end
|
||||||
|
guiSetValue(GUI, DOCUMENT, "toolTitle", menuEscape(TOOL_TITLE))
|
||||||
|
guiSetValue(GUI, DOCUMENT, "toolBody", table.concat(rows))
|
||||||
|
menuElement("games"):SetClass("hidden", toolsActive())
|
||||||
|
menuElement("tool"):SetClass("hidden", not toolsActive())
|
||||||
|
menuElement("tool"):SetClass("clear", TOOL_CLEAR)
|
||||||
|
menuElement("tool"):SetClass("flash", TOOL_FLASH)
|
||||||
|
end
|
||||||
|
guiDraw(GUI)
|
||||||
|
end
|
||||||
349
assets/MenuOverlay.singe
Normal file
349
assets/MenuOverlay.singe
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
--[[
|
||||||
|
*
|
||||||
|
* Singe 3
|
||||||
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
--]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- The menu drawn with sprites and fontPrint straight into the overlay: the look Singe 2 had, and
|
||||||
|
-- the one that works on a machine whose GPU device never came up, where there is no GUI document to
|
||||||
|
-- build. Singe/MenuDocument.singe is the other renderer; Singe/Menu.singe loads exactly one of the
|
||||||
|
-- two and calls it through MENU_RENDER. See the top of MenuDocument.singe for what is in it.
|
||||||
|
--
|
||||||
|
-- One game is on screen at a time, as it always was here, so there is no list to page through
|
||||||
|
-- visually: up and down move one game, left and right move ten, and the line under the marquee
|
||||||
|
-- says where in the library the selection is. A description too long for its column is clipped
|
||||||
|
-- rather than scrolled, because up and down now belong to the list.
|
||||||
|
|
||||||
|
|
||||||
|
local MARGIN_X = 25
|
||||||
|
local MARGIN_Y = 25
|
||||||
|
local VIDEO_W = 320
|
||||||
|
local VIDEO_H = 200
|
||||||
|
local MARQUEE_H = 75
|
||||||
|
local LINE_HEIGHT = 24
|
||||||
|
local LABEL_W = 150
|
||||||
|
local SUB_INDENT = 20
|
||||||
|
local PAGE_ROWS = 10
|
||||||
|
local WRAP_BREAK = " [!wb!] "
|
||||||
|
|
||||||
|
local font = nil
|
||||||
|
local videoX, videoY, videoW, videoH = 0, 0, 0, 0
|
||||||
|
local marqueeX, marqueeY, marqueeW = 0, 0, 0
|
||||||
|
local textX, textY, textW, textH = 0, 0, 0, 0
|
||||||
|
local cabinetX, cabinetY, cabinetW, cabinetH = 0, 0, 0, 0
|
||||||
|
local spriteCabinet = nil
|
||||||
|
local spriteMarquee = nil
|
||||||
|
local textSprites = {}
|
||||||
|
local textHeight = 0
|
||||||
|
local textLimit = 0
|
||||||
|
|
||||||
|
|
||||||
|
-- A games.dat entry need not carry every field, and one that is missing must not take the menu
|
||||||
|
-- down; the document renderer is tolerant the same way, through menuEscape.
|
||||||
|
local function field(value)
|
||||||
|
return tostring(value or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Splits text into the lines that fit the width given, measuring with the selected font. The only
|
||||||
|
-- way to measure a string is to render it, so this is slow enough to be worth doing once when
|
||||||
|
-- something changes rather than once a frame. WRAP_BREAK is a word the caller puts in to force a
|
||||||
|
-- line ending.
|
||||||
|
--
|
||||||
|
-- The space between words is kept as it was written rather than squeezed to one, because a tool
|
||||||
|
-- lines its columns up with runs of spaces and there is no other way here to say "a wide gap".
|
||||||
|
local function fitLines(text, maxWidth)
|
||||||
|
local lines = {}
|
||||||
|
local line = ""
|
||||||
|
local pending = ""
|
||||||
|
local trimBreak = utilTrim(WRAP_BREAK)
|
||||||
|
|
||||||
|
for word, space in (text .. WRAP_BREAK):gmatch("(%S+)(%s*)") do
|
||||||
|
if word == trimBreak then
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
line = ""
|
||||||
|
pending = ""
|
||||||
|
else
|
||||||
|
local candidate = line .. pending .. word
|
||||||
|
local measured = fontToSprite(candidate)
|
||||||
|
local tooWide = spriteGetWidth(measured) > maxWidth
|
||||||
|
spriteUnload(measured)
|
||||||
|
if tooWide and line ~= "" then
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
line = word
|
||||||
|
else
|
||||||
|
line = candidate
|
||||||
|
end
|
||||||
|
pending = space
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if line ~= "" then
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
end
|
||||||
|
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The service tools, laid out. Each entry is one line of the page and each line is a list of
|
||||||
|
-- pieces: where the piece starts, what it says and which colour it wants. The colours are roles
|
||||||
|
-- rather than values because the audio delay tool inverts the whole page for a frame, and fontPrint
|
||||||
|
-- takes the colour at the moment it draws -- unlike fontToSprite, which bakes it in.
|
||||||
|
local toolLines = {}
|
||||||
|
|
||||||
|
|
||||||
|
local function toolLine(pieces)
|
||||||
|
toolLines[#toolLines + 1] = pieces
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Text too long for the page wraps under itself rather than running off the edge.
|
||||||
|
local function toolWrapped(text, x, role)
|
||||||
|
for _, line in ipairs(fitLines(text, overlayGetWidth() - MARGIN_X - x)) do
|
||||||
|
toolLine({ { x = x, text = line, role = role } })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function toolsLayout()
|
||||||
|
local valueX = MARGIN_X + LABEL_W
|
||||||
|
local wasKeys = false
|
||||||
|
|
||||||
|
toolLines = {}
|
||||||
|
for _, row in ipairs(TOOL_ROWS) do
|
||||||
|
-- The key hints stand away from whatever the tool put above them, as they do in the
|
||||||
|
-- document, but sit together when a tool needs more than one line of them.
|
||||||
|
if row.kind == "keys" and not wasKeys then
|
||||||
|
toolLine({})
|
||||||
|
end
|
||||||
|
wasKeys = (row.kind == "keys")
|
||||||
|
|
||||||
|
if row.kind == "blank" then
|
||||||
|
toolLine({})
|
||||||
|
elseif row.kind == "pair" then
|
||||||
|
-- The label and the first line of the value share a line; the rest of the value wraps
|
||||||
|
-- under itself, in its own column.
|
||||||
|
local lines = fitLines(row.value, overlayGetWidth() - MARGIN_X - valueX)
|
||||||
|
toolLine({ { x = MARGIN_X, text = row.label, role = "quiet" },
|
||||||
|
{ x = valueX, text = lines[1] or "", role = "ink" } })
|
||||||
|
for index = 2, #lines do
|
||||||
|
toolLine({ { x = valueX, text = lines[index], role = "ink" } })
|
||||||
|
end
|
||||||
|
elseif row.kind == "sub" then
|
||||||
|
toolWrapped(row.text, MARGIN_X + SUB_INDENT, "quiet")
|
||||||
|
elseif row.kind == "keys" then
|
||||||
|
toolWrapped(row.text, MARGIN_X, "quiet")
|
||||||
|
elseif row.kind == "item" then
|
||||||
|
toolLine({ { x = MARGIN_X, text = (row.selected and "> " or " ") .. row.text,
|
||||||
|
role = row.selected and "pick" or "ink" } })
|
||||||
|
else
|
||||||
|
toolWrapped(row.text, MARGIN_X, "ink")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER = {}
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.begin = function()
|
||||||
|
font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||||
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
|
fontSelect(font)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.finish = function()
|
||||||
|
if font then
|
||||||
|
fontUnload(font)
|
||||||
|
font = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The cabinet fills the left; the marquee, the attract video and the details stack down the right.
|
||||||
|
MENU_RENDER.layout = function()
|
||||||
|
local width = overlayGetWidth()
|
||||||
|
local height = overlayGetHeight()
|
||||||
|
|
||||||
|
marqueeW = VIDEO_W
|
||||||
|
marqueeX = width - MARGIN_X - marqueeW
|
||||||
|
marqueeY = MARGIN_Y
|
||||||
|
|
||||||
|
videoW = VIDEO_W
|
||||||
|
videoH = VIDEO_H
|
||||||
|
videoX = width - MARGIN_X - videoW
|
||||||
|
videoY = MARGIN_Y + MARQUEE_H + MARGIN_Y
|
||||||
|
|
||||||
|
textW = VIDEO_W
|
||||||
|
textX = width - MARGIN_X - textW
|
||||||
|
textY = videoY + videoH + MARGIN_Y
|
||||||
|
textH = height - MARGIN_Y - textY
|
||||||
|
|
||||||
|
cabinetX = MARGIN_X
|
||||||
|
cabinetY = MARGIN_Y
|
||||||
|
cabinetW = videoX - MARGIN_X - cabinetX
|
||||||
|
cabinetH = height - MARGIN_Y - cabinetY
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.buildList = function()
|
||||||
|
-- One game is on screen at a time; there is no list to build.
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.showGame = function(game)
|
||||||
|
spriteCabinet = spriteLoad(game.CABINET)
|
||||||
|
spriteMarquee = spriteLoad(game.MARQUEE)
|
||||||
|
|
||||||
|
-- The details follow the description in one wrapped block, the way this menu has always shown
|
||||||
|
-- them. The document menu gives them their own panel instead.
|
||||||
|
local textBox = field(game.DESCRIPTION) .. WRAP_BREAK .. WRAP_BREAK ..
|
||||||
|
"Year: " .. field(game.YEAR) .. WRAP_BREAK ..
|
||||||
|
"Genre: " .. field(game.GENRE) .. WRAP_BREAK ..
|
||||||
|
"Platform: " .. field(game.PLATFORM) .. WRAP_BREAK ..
|
||||||
|
"Developer: " .. field(game.DEVELOPER) .. WRAP_BREAK ..
|
||||||
|
"Publisher: " .. field(game.PUBLISHER) .. WRAP_BREAK .. WRAP_BREAK ..
|
||||||
|
"Singe Port: " .. field(game.CREATOR) .. WRAP_BREAK ..
|
||||||
|
"Source: " .. field(game.SOURCE)
|
||||||
|
-- fontToSprite bakes the colour in, so it is set before the sprites are made rather than
|
||||||
|
-- before they are drawn.
|
||||||
|
colorForeground(255, 255, 255, 255)
|
||||||
|
for _, line in ipairs(fitLines(textBox, textW)) do
|
||||||
|
textSprites[#textSprites + 1] = (string.len(line) > 0) and fontToSprite(line) or -1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.hideGame = function()
|
||||||
|
if spriteCabinet then
|
||||||
|
spriteUnload(spriteCabinet)
|
||||||
|
spriteCabinet = nil
|
||||||
|
end
|
||||||
|
if spriteMarquee then
|
||||||
|
spriteUnload(spriteMarquee)
|
||||||
|
spriteMarquee = nil
|
||||||
|
end
|
||||||
|
for _, handle in ipairs(textSprites) do
|
||||||
|
if handle >= 0 then
|
||||||
|
spriteUnload(handle)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
textSprites = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.pageSize = function()
|
||||||
|
return PAGE_ROWS
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.frame = function()
|
||||||
|
local x = 0
|
||||||
|
local y = 0
|
||||||
|
local shown = 0
|
||||||
|
|
||||||
|
-- Cabinet image
|
||||||
|
x = cabinetX + (cabinetW - spriteGetWidth(spriteCabinet)) * 0.5
|
||||||
|
y = cabinetY + (cabinetH - spriteGetHeight(spriteCabinet)) * 0.5
|
||||||
|
spriteDraw(spriteCabinet, x, y)
|
||||||
|
|
||||||
|
-- Marquee image
|
||||||
|
x = marqueeX + (marqueeW - spriteGetWidth(spriteMarquee)) * 0.5
|
||||||
|
y = marqueeY + (MARQUEE_H - spriteGetHeight(spriteMarquee)) * 0.5
|
||||||
|
spriteDraw(spriteMarquee, x, y)
|
||||||
|
|
||||||
|
-- Attract mode video
|
||||||
|
videoDraw(VIDEO_ATTRACT, videoX, videoY, videoX + videoW, videoY + videoH)
|
||||||
|
|
||||||
|
-- Which game this is, since there is no list to look at
|
||||||
|
colorForeground(255, 211, 90, 255)
|
||||||
|
fontPrint(cabinetX, cabinetY, string.format("%s (%d of %d)", field(GAME_LIST[GAME_SELECTED].TITLE), GAME_SELECTED, GAME_COUNT))
|
||||||
|
|
||||||
|
-- Game description, clipped to the lines that fit
|
||||||
|
y = textY
|
||||||
|
for _, handle in ipairs(textSprites) do
|
||||||
|
-- Find height of font and number of lines that fit
|
||||||
|
if textHeight == 0 and handle >= 0 then
|
||||||
|
textHeight = spriteGetHeight(handle)
|
||||||
|
textLimit = math.floor(textH / textHeight)
|
||||||
|
end
|
||||||
|
if shown < textLimit then
|
||||||
|
if handle >= 0 then
|
||||||
|
spriteDraw(handle, textX, y)
|
||||||
|
end
|
||||||
|
y = y + textHeight + 1
|
||||||
|
shown = shown + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MENU_RENDER.tools = function()
|
||||||
|
local y = MARGIN_Y
|
||||||
|
|
||||||
|
-- A tool that draws its own picture into the overlay has already done so, and the page has to
|
||||||
|
-- let it through; every other tool gets a page painted over everything.
|
||||||
|
if not TOOL_CLEAR then
|
||||||
|
if TOOL_FLASH then
|
||||||
|
colorBackground(255, 255, 255, 255)
|
||||||
|
else
|
||||||
|
colorBackground(0, 0, 0, 255)
|
||||||
|
end
|
||||||
|
overlayClear()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Measuring text means rendering it, so the page is laid out only when a tool says it changed.
|
||||||
|
if TOOL_DIRTY then
|
||||||
|
TOOL_DIRTY = false
|
||||||
|
toolsLayout()
|
||||||
|
end
|
||||||
|
|
||||||
|
local ink = TOOL_FLASH and 0 or 255
|
||||||
|
local quiet = TOOL_FLASH and 90 or 160
|
||||||
|
|
||||||
|
-- A page that lets the overlay through gathers its text along the bottom, out of the way of
|
||||||
|
-- whatever the tool drew: the light gun's corner targets in particular.
|
||||||
|
if TOOL_CLEAR then
|
||||||
|
y = overlayGetHeight() - MARGIN_Y - (#toolLines + 1) * LINE_HEIGHT
|
||||||
|
end
|
||||||
|
colorForeground(ink, ink, ink, 255)
|
||||||
|
fontPrint(MARGIN_X, y, TOOL_TITLE)
|
||||||
|
y = y + LINE_HEIGHT
|
||||||
|
if not TOOL_CLEAR then
|
||||||
|
y = y + LINE_HEIGHT
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, line in ipairs(toolLines) do
|
||||||
|
for _, piece in ipairs(line) do
|
||||||
|
if piece.role == "quiet" then
|
||||||
|
colorForeground(quiet, quiet, quiet, 255)
|
||||||
|
elseif piece.role == "pick" then
|
||||||
|
colorForeground(255, 211, 90, 255)
|
||||||
|
else
|
||||||
|
colorForeground(ink, ink, ink, 255)
|
||||||
|
end
|
||||||
|
fontPrint(piece.x, y, piece.text)
|
||||||
|
end
|
||||||
|
y = y + LINE_HEIGHT
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -23,27 +23,81 @@
|
||||||
|
|
||||||
|
|
||||||
-- The service tools: everything an operator or a developer needs to check a cabinet, reached by
|
-- The service tools: everything an operator or a developer needs to check a cabinet, reached by
|
||||||
-- pressing SERVICE in the menu. Each tool is a page of Singe/Menu.rml and an entry in TOOLS with
|
-- pressing SERVICE in the menu. Each tool is an entry in TOOLS with up to four functions: begin
|
||||||
-- up to four functions: begin when it opens, input for a switch, update once a frame, and finish
|
-- when it opens, input for a switch, update once a frame, and finish when it closes. Anything a
|
||||||
-- when it closes. Anything a tool leaves out simply does not happen.
|
-- tool leaves out simply does not happen.
|
||||||
--
|
--
|
||||||
-- Only the RmlUi menu has these. Singe/MenuClassic.singe exists for a machine with no GPU device,
|
-- Nothing here draws. A tool builds a list of rows with the toolsText, toolsPair, toolsSub,
|
||||||
-- where there is no document to put a page in; it keeps its own audio delay screen and nothing
|
-- toolsKeys, toolsItem and toolsBlank constructors and hands it to toolsShow; the menu that loaded
|
||||||
-- else.
|
-- this file draws it whichever way it draws everything else. That is what lets the same ten tools
|
||||||
|
-- run over the RmlUi document and over the plain overlay on a machine with no GPU.
|
||||||
|
|
||||||
|
|
||||||
local lfs = require("lfs")
|
local lfs = require("lfs")
|
||||||
|
|
||||||
|
|
||||||
|
TOOLS = {}
|
||||||
TOOL_SELECTED = 1
|
TOOL_SELECTED = 1
|
||||||
TOOL_OPEN = nil -- The tool being used, or nil while the list itself is showing
|
TOOL_OPEN = nil -- The tool being used, or nil while the list itself is showing
|
||||||
TOOLS = {}
|
TOOL_SHOWING = false -- Whether the tools have the screen at all
|
||||||
|
|
||||||
|
-- What the menu draws. It redraws when TOOL_DIRTY is set and clears it; a tool that changes
|
||||||
|
-- nothing costs nothing.
|
||||||
|
TOOL_TITLE = ""
|
||||||
|
TOOL_ROWS = {}
|
||||||
|
TOOL_FLASH = false -- One frame of white, for the audio delay tool
|
||||||
|
TOOL_CLEAR = false -- This page lets the overlay through: the tool draws its own picture
|
||||||
|
TOOL_DIRTY = false
|
||||||
|
|
||||||
|
|
||||||
-- Writing into a page is the one thing every tool does, and the document and the GUI are the same
|
-- ===== Rows =====
|
||||||
-- ones every time, so they are named here rather than in twenty-two places.
|
|
||||||
local function set(id, text)
|
-- A row is a table with a kind the menu knows how to draw. Tools never build one by hand.
|
||||||
guiSetValue(GUI, DOCUMENT, id, text)
|
function toolsText(text)
|
||||||
|
return { kind = "text", text = text }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function toolsPair(label, value)
|
||||||
|
return { kind = "pair", label = label, value = tostring(value) }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function toolsSub(text)
|
||||||
|
return { kind = "sub", text = text }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function toolsKeys(text)
|
||||||
|
return { kind = "keys", text = text }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function toolsItem(text, selected)
|
||||||
|
return { kind = "item", text = text, selected = selected }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function toolsBlank()
|
||||||
|
return { kind = "blank" }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- What the page says now. Called as often as a tool likes; only the last one before the menu
|
||||||
|
-- draws is seen.
|
||||||
|
function toolsShow(rows)
|
||||||
|
TOOL_ROWS = rows
|
||||||
|
TOOL_DIRTY = true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The one frame of white the audio delay tool flashes. A menu that cannot invert its page can
|
||||||
|
-- ignore it, and the tool still works by ear.
|
||||||
|
function toolsFlash(on)
|
||||||
|
if TOOL_FLASH ~= on then
|
||||||
|
TOOL_FLASH = on
|
||||||
|
TOOL_DIRTY = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -51,8 +105,8 @@ end
|
||||||
|
|
||||||
function toolsBegin()
|
function toolsBegin()
|
||||||
TOOL_OPEN = nil
|
TOOL_OPEN = nil
|
||||||
|
TOOL_SHOWING = true
|
||||||
toolsShowList()
|
toolsShowList()
|
||||||
toolsShowPage("tools")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -61,37 +115,36 @@ function toolsEnd()
|
||||||
TOOL_OPEN.finish()
|
TOOL_OPEN.finish()
|
||||||
end
|
end
|
||||||
TOOL_OPEN = nil
|
TOOL_OPEN = nil
|
||||||
toolsShowPage(nil)
|
TOOL_SHOWING = false
|
||||||
end
|
TOOL_CLEAR = false
|
||||||
|
TOOL_FLASH = false
|
||||||
|
TOOL_DIRTY = true
|
||||||
-- One page of the document at a time, and the game list when nothing is named.
|
|
||||||
function toolsShowPage(id)
|
|
||||||
menuElement("games"):SetClass("hidden", id ~= nil)
|
|
||||||
menuElement("tools"):SetClass("hidden", id ~= "tools")
|
|
||||||
for _, tool in ipairs(TOOLS) do
|
|
||||||
menuElement(tool.id):SetClass("hidden", id ~= tool.id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function toolsShowList()
|
function toolsShowList()
|
||||||
local rows = {}
|
local rows = {}
|
||||||
|
|
||||||
|
TOOL_TITLE = "Service Tools"
|
||||||
|
TOOL_CLEAR = false
|
||||||
for i, tool in ipairs(TOOLS) do
|
for i, tool in ipairs(TOOLS) do
|
||||||
local class = (i == TOOL_SELECTED) and ' class="selected"' or ""
|
rows[#rows + 1] = toolsItem(tool.name, i == TOOL_SELECTED)
|
||||||
rows[#rows + 1] = string.format("<div%s>%s</div>", class, tool.name)
|
|
||||||
end
|
end
|
||||||
set("toolList", table.concat(rows))
|
rows[#rows + 1] = toolsBlank()
|
||||||
set("toolHelp", TOOLS[TOOL_SELECTED].help or "")
|
rows[#rows + 1] = toolsText(TOOLS[TOOL_SELECTED].help or "")
|
||||||
|
rows[#rows + 1] = toolsKeys("Up / Down: choose Button 1: open Service: back to the games")
|
||||||
|
toolsShow(rows)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function toolsOpen(index)
|
function toolsOpen(index)
|
||||||
TOOL_OPEN = TOOLS[index]
|
TOOL_OPEN = TOOLS[index]
|
||||||
|
TOOL_TITLE = TOOL_OPEN.name
|
||||||
|
TOOL_CLEAR = TOOL_OPEN.clear or false
|
||||||
if TOOL_OPEN.begin then
|
if TOOL_OPEN.begin then
|
||||||
TOOL_OPEN.begin()
|
TOOL_OPEN.begin()
|
||||||
end
|
end
|
||||||
toolsShowPage(TOOL_OPEN.id)
|
TOOL_DIRTY = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -100,8 +153,8 @@ function toolsClose()
|
||||||
TOOL_OPEN.finish()
|
TOOL_OPEN.finish()
|
||||||
end
|
end
|
||||||
TOOL_OPEN = nil
|
TOOL_OPEN = nil
|
||||||
|
TOOL_FLASH = false
|
||||||
toolsShowList()
|
toolsShowList()
|
||||||
toolsShowPage("tools")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -141,7 +194,7 @@ end
|
||||||
|
|
||||||
-- Whether the tools have the screen, which is what the menu asks before doing anything of its own.
|
-- Whether the tools have the screen, which is what the menu asks before doing anything of its own.
|
||||||
function toolsActive()
|
function toolsActive()
|
||||||
return TOOL_OPEN ~= nil or not menuElement("tools"):IsClassSet("hidden")
|
return TOOL_SHOWING
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -158,12 +211,18 @@ local calValue, calBeat, calClickDone, calFlashDone
|
||||||
|
|
||||||
|
|
||||||
local function audioShow()
|
local function audioShow()
|
||||||
set("calValue", tostring(calValue))
|
toolsShow({
|
||||||
|
toolsText("Adjust until the flash and the click happen together."),
|
||||||
|
toolsBlank(),
|
||||||
|
toolsPair("Delay", calValue .. " ms"),
|
||||||
|
toolsPair("Device queue", singeGetAudioLatency() .. " ms"),
|
||||||
|
toolsKeys("Left / Right: 10 ms Up / Down: 1 ms Start: reset to 0"),
|
||||||
|
toolsKeys("Button 1: save Button 2: back"),
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
TOOLS[#TOOLS + 1] = {
|
TOOLS[#TOOLS + 1] = {
|
||||||
id = "toolAudio",
|
|
||||||
name = "Audio Delay",
|
name = "Audio Delay",
|
||||||
help = "Line the click up with the flash so sound and picture agree.",
|
help = "Line the click up with the flash so sound and picture agree.",
|
||||||
|
|
||||||
|
|
@ -172,7 +231,6 @@ TOOLS[#TOOLS + 1] = {
|
||||||
calBeat = singeGetTicks() - CAL_PERIOD
|
calBeat = singeGetTicks() - CAL_PERIOD
|
||||||
calClickDone = true
|
calClickDone = true
|
||||||
calFlashDone = true
|
calFlashDone = true
|
||||||
set("calLatency", tostring(singeGetAudioLatency()))
|
|
||||||
audioShow()
|
audioShow()
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
|
@ -218,14 +276,14 @@ TOOLS[#TOOLS + 1] = {
|
||||||
end
|
end
|
||||||
if not calFlashDone and (now - calBeat >= singeGetAudioLatency() + calValue) then
|
if not calFlashDone and (now - calBeat >= singeGetAudioLatency() + calValue) then
|
||||||
calFlashDone = true
|
calFlashDone = true
|
||||||
menuElement("toolAudio"):SetClass("flash", true)
|
toolsFlash(true)
|
||||||
elseif calFlashDone then
|
elseif calFlashDone then
|
||||||
menuElement("toolAudio"):SetClass("flash", false)
|
toolsFlash(false)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
finish = function()
|
finish = function()
|
||||||
menuElement("toolAudio"):SetClass("flash", false)
|
toolsFlash(false)
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,7 +309,6 @@ end
|
||||||
|
|
||||||
|
|
||||||
TOOLS[#TOOLS + 1] = {
|
TOOLS[#TOOLS + 1] = {
|
||||||
id = "toolInput",
|
|
||||||
name = "Input Test",
|
name = "Input Test",
|
||||||
help = "Controllers, keyboard, mice and light guns, live.",
|
help = "Controllers, keyboard, mice and light guns, live.",
|
||||||
|
|
||||||
|
|
@ -269,9 +326,9 @@ TOOLS[#TOOLS + 1] = {
|
||||||
|
|
||||||
update = function()
|
update = function()
|
||||||
local rows = {}
|
local rows = {}
|
||||||
rows[#rows + 1] = string.format("<div class='row'>Last switch: <b>%s</b> count: %d</div>", inputLast, inputCount)
|
rows[#rows + 1] = toolsPair("Last switch", string.format("%s count: %d", inputLast, inputCount))
|
||||||
rows[#rows + 1] = string.format("<div class='row'>Gamepads: %d Mice: %d Stick drives mouse: %s</div>",
|
rows[#rows + 1] = toolsPair("Devices", string.format("%d gamepads, %d mice, stick drives mouse: %s",
|
||||||
controllerHowMany(), mouseHowMany(), tostring(joyMouseIsEnabled()))
|
controllerHowMany(), mouseHowMany(), tostring(joyMouseIsEnabled())))
|
||||||
for slot = 0, SINGE_MAX_CONTROLLERS - 1 do
|
for slot = 0, SINGE_MAX_CONTROLLERS - 1 do
|
||||||
if controllerIsValid(slot) then
|
if controllerIsValid(slot) then
|
||||||
local axes = {}
|
local axes = {}
|
||||||
|
|
@ -286,16 +343,17 @@ TOOLS[#TOOLS + 1] = {
|
||||||
buttons[#buttons + 1] = tostring(b)
|
buttons[#buttons + 1] = tostring(b)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
rows[#rows + 1] = string.format("<div class='row'>%d: <b>%s</b></div>", slot, controllerGetName(slot))
|
rows[#rows + 1] = toolsText(string.format("%d: %s", slot, controllerGetName(slot)))
|
||||||
rows[#rows + 1] = string.format("<div class='sub'>axes %s buttons %s</div>",
|
rows[#rows + 1] = toolsSub(string.format("axes %s buttons %s",
|
||||||
table.concat(axes, ", "), (#buttons > 0) and table.concat(buttons, " ") or "-")
|
table.concat(axes, ", "), (#buttons > 0) and table.concat(buttons, " ") or "-"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for m = 0, mouseHowMany() - 1 do
|
for m = 0, mouseHowMany() - 1 do
|
||||||
local x, y = mouseGetPosition(m)
|
local x, y = mouseGetPosition(m)
|
||||||
rows[#rows + 1] = string.format("<div class='sub'>mouse %d: %s at %d, %d</div>", m, mouseGetName(m), x, y)
|
rows[#rows + 1] = toolsSub(string.format("mouse %d: %s at %d, %d", m, mouseGetName(m), x, y))
|
||||||
end
|
end
|
||||||
set("inputRows", table.concat(rows))
|
rows[#rows + 1] = toolsKeys("Press anything. Button 2: back")
|
||||||
|
toolsShow(rows)
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,29 +363,26 @@ TOOLS[#TOOLS + 1] = {
|
||||||
-- Everything the trace header knows, on one screen. A photograph of this answers most of a bug
|
-- Everything the trace header knows, on one screen. A photograph of this answers most of a bug
|
||||||
-- report without anyone having to find trace.txt, which is the point of it.
|
-- report without anyone having to find trace.txt, which is the point of it.
|
||||||
TOOLS[#TOOLS + 1] = {
|
TOOLS[#TOOLS + 1] = {
|
||||||
id = "toolSystem",
|
|
||||||
name = "System Information",
|
name = "System Information",
|
||||||
help = "What this machine is and what the engine chose to run on.",
|
help = "What this machine is and what the engine chose to run on.",
|
||||||
|
|
||||||
begin = function()
|
begin = function()
|
||||||
local i = singeGetSystemInfo()
|
local i = singeGetSystemInfo()
|
||||||
local rows = {}
|
toolsShow({
|
||||||
local function line(label, value)
|
toolsPair("Singe", i.version),
|
||||||
rows[#rows + 1] = string.format("<div class='pair'><span class='label'>%s</span><span class='detail'>%s</span></div>", label, tostring(value))
|
toolsPair("System", i.os),
|
||||||
end
|
toolsPair("Processor", i.cpu),
|
||||||
line("Singe", i.version)
|
toolsPair("Renderer", i.renderer),
|
||||||
line("System", i.os)
|
toolsPair("3D device", i.gpu),
|
||||||
line("Processor", i.cpu)
|
toolsPair("Video", i.decoder),
|
||||||
line("Renderer", i.renderer)
|
toolsPair("Audio", i.audio),
|
||||||
line("3D device", i.gpu)
|
toolsPair("SoundFont", i.soundFont),
|
||||||
line("Video", i.decoder)
|
toolsPair("MIDI", i.midi),
|
||||||
line("Audio", i.audio)
|
toolsPair("Window", string.format("%d x %d, canvas %d x %d, overlay %d x %d",
|
||||||
line("SoundFont", i.soundFont)
|
i.windowWidth, i.windowHeight, i.canvasWidth, i.canvasHeight, overlayGetWidth(), overlayGetHeight())),
|
||||||
line("MIDI", i.midi)
|
toolsPair("Data", i.dataPath),
|
||||||
line("Window", string.format("%d x %d, canvas %d x %d, overlay %d x %d",
|
toolsKeys("Button 2: back"),
|
||||||
i.windowWidth, i.windowHeight, i.canvasWidth, i.canvasHeight, overlayGetWidth(), overlayGetHeight()))
|
})
|
||||||
line("Data", i.dataPath)
|
|
||||||
set("systemRows", table.concat(rows))
|
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -337,6 +392,7 @@ TOOLS[#TOOLS + 1] = {
|
||||||
-- Where the gun is pointing, against the picture it is pointing at. A cabinet needs this every
|
-- Where the gun is pointing, against the picture it is pointing at. A cabinet needs this every
|
||||||
-- time the gun or the monitor moves, and nothing else in the engine shows the mapping from the
|
-- time the gun or the monitor moves, and nothing else in the engine shows the mapping from the
|
||||||
-- gun's own coordinates to the canvas.
|
-- gun's own coordinates to the canvas.
|
||||||
|
--
|
||||||
-- The targets: one in the middle and one in each corner, set in far enough that a gun that is
|
-- The targets: one in the middle and one in each corner, set in far enough that a gun that is
|
||||||
-- slightly out still puts its shot on the screen beside them.
|
-- slightly out still puts its shot on the screen beside them.
|
||||||
local GUN_SHOTS_KEPT = 12
|
local GUN_SHOTS_KEPT = 12
|
||||||
|
|
@ -348,14 +404,23 @@ local gunShots = {}
|
||||||
local gunLast = "none yet"
|
local gunLast = "none yet"
|
||||||
|
|
||||||
|
|
||||||
|
local function gunShow()
|
||||||
|
toolsShow({
|
||||||
|
toolsPair("Last shot", string.format("%s shots shown: %d", gunLast, #gunShots)),
|
||||||
|
toolsKeys("Button 1: fire Start: clear Button 2: back"),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
TOOLS[#TOOLS + 1] = {
|
TOOLS[#TOOLS + 1] = {
|
||||||
id = "toolGun",
|
|
||||||
name = "Light Gun",
|
name = "Light Gun",
|
||||||
help = "Aim and fire: see where the shot actually lands.",
|
help = "Aim and fire: see where the shot actually lands.",
|
||||||
|
clear = true,
|
||||||
|
|
||||||
begin = function()
|
begin = function()
|
||||||
gunShots = {}
|
gunShots = {}
|
||||||
gunLast = "none yet"
|
gunLast = "none yet"
|
||||||
|
gunShow()
|
||||||
end,
|
end,
|
||||||
|
|
||||||
input = function(what)
|
input = function(what)
|
||||||
|
|
@ -366,9 +431,11 @@ TOOLS[#TOOLS + 1] = {
|
||||||
table.remove(gunShots, 1)
|
table.remove(gunShots, 1)
|
||||||
end
|
end
|
||||||
gunLast = string.format("%d, %d", x, y)
|
gunLast = string.format("%d, %d", x, y)
|
||||||
|
gunShow()
|
||||||
return true
|
return true
|
||||||
elseif what == SWITCH_START1 or what == SWITCH_START2 then
|
elseif what == SWITCH_START1 or what == SWITCH_START2 then
|
||||||
gunShots = {}
|
gunShots = {}
|
||||||
|
gunShow()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
|
|
@ -394,8 +461,6 @@ TOOLS[#TOOLS + 1] = {
|
||||||
overlayLine(shot.x - GUN_CROSS, shot.y, shot.x + GUN_CROSS, shot.y)
|
overlayLine(shot.x - GUN_CROSS, shot.y, shot.x + GUN_CROSS, shot.y)
|
||||||
overlayLine(shot.x, shot.y - GUN_CROSS, shot.x, shot.y + GUN_CROSS)
|
overlayLine(shot.x, shot.y - GUN_CROSS, shot.x, shot.y + GUN_CROSS)
|
||||||
end
|
end
|
||||||
set("gunLast", gunLast)
|
|
||||||
set("gunCount", tostring(#gunShots))
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
finish = function()
|
finish = function()
|
||||||
|
|
@ -411,22 +476,34 @@ TOOLS[#TOOLS + 1] = {
|
||||||
local soundStep = 0
|
local soundStep = 0
|
||||||
local soundNames = { "left speaker", "right speaker", "both speakers" }
|
local soundNames = { "left speaker", "right speaker", "both speakers" }
|
||||||
local soundPans = { -1.0, 1.0, 0.0 }
|
local soundPans = { -1.0, 1.0, 0.0 }
|
||||||
|
local soundWhich = ""
|
||||||
|
local soundList = ""
|
||||||
|
|
||||||
|
|
||||||
|
local function soundShow()
|
||||||
|
toolsShow({
|
||||||
|
toolsPair("Playing", soundWhich),
|
||||||
|
toolsPair("Disc tracks", soundList),
|
||||||
|
toolsKeys("Button 1: left, right, both Left / Right: disc track Button 2: back"),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
TOOLS[#TOOLS + 1] = {
|
TOOLS[#TOOLS + 1] = {
|
||||||
id = "toolSound",
|
|
||||||
name = "Sound Test",
|
name = "Sound Test",
|
||||||
help = "Left, right, both, and the disc's own audio tracks.",
|
help = "Left, right, both, and the disc's own audio tracks.",
|
||||||
|
|
||||||
begin = function()
|
begin = function()
|
||||||
soundStep = 0
|
|
||||||
set("soundWhich", "press Button 1")
|
|
||||||
local tracks = {}
|
local tracks = {}
|
||||||
|
|
||||||
|
soundStep = 0
|
||||||
|
soundWhich = "press Button 1"
|
||||||
for t = 0, discGetAudioTracks() - 1 do
|
for t = 0, discGetAudioTracks() - 1 do
|
||||||
local code = discGetLanguage(t)
|
local code = discGetLanguage(t)
|
||||||
tracks[#tracks + 1] = string.format("%d:%s", t, (code ~= "" and code ~= "unk") and code or "unnamed")
|
tracks[#tracks + 1] = string.format("%d:%s", t, (code ~= "" and code ~= "unk") and code or "unnamed")
|
||||||
end
|
end
|
||||||
set("soundTracks", (#tracks > 0) and table.concat(tracks, " ") or "the disc has none")
|
soundList = (#tracks > 0) and table.concat(tracks, " ") or "the disc has none"
|
||||||
|
soundShow()
|
||||||
end,
|
end,
|
||||||
|
|
||||||
input = function(what)
|
input = function(what)
|
||||||
|
|
@ -434,15 +511,16 @@ TOOLS[#TOOLS + 1] = {
|
||||||
soundStep = (soundStep % #soundNames) + 1
|
soundStep = (soundStep % #soundNames) + 1
|
||||||
soundSetPan(SND_CLICK, soundPans[soundStep])
|
soundSetPan(SND_CLICK, soundPans[soundStep])
|
||||||
soundPlay(SND_CLICK)
|
soundPlay(SND_CLICK)
|
||||||
set("soundWhich", soundNames[soundStep])
|
soundWhich = soundNames[soundStep]
|
||||||
|
soundShow()
|
||||||
return true
|
return true
|
||||||
elseif what == SWITCH_LEFT or what == SWITCH_RIGHT then
|
elseif what == SWITCH_LEFT or what == SWITCH_RIGHT then
|
||||||
-- Walk the disc's audio tracks, which is how a multi language release is checked.
|
|
||||||
local count = discGetAudioTracks()
|
local count = discGetAudioTracks()
|
||||||
if count > 1 then
|
if count > 1 then
|
||||||
local next = (discGetAudioTrack() + ((what == SWITCH_RIGHT) and 1 or (count - 1))) % count
|
local next = (discGetAudioTrack() + ((what == SWITCH_RIGHT) and 1 or (count - 1))) % count
|
||||||
discSetAudioTrack(next)
|
discSetAudioTrack(next)
|
||||||
set("soundWhich", string.format("disc track %d (%s)", next, discGetLanguage(next)))
|
soundWhich = string.format("disc track %d (%s)", next, discGetLanguage(next))
|
||||||
|
soundShow()
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
@ -470,23 +548,29 @@ local DISP_ROWS = 12
|
||||||
local DISP_STEPS = 8
|
local DISP_STEPS = 8
|
||||||
local DISP_STEP_WIDE = 32
|
local DISP_STEP_WIDE = 32
|
||||||
local DISP_WEDGE = 16
|
local DISP_WEDGE = 16
|
||||||
|
local dispSaved = ""
|
||||||
|
|
||||||
|
|
||||||
local function displayShow()
|
local function displayShow()
|
||||||
local x, y = vldpGetShift()
|
local x, y = vldpGetShift()
|
||||||
set("dispValues", string.format("scale %d%% shift %d, %d rotation %d",
|
toolsShow({
|
||||||
vldpGetScale(), x, y, vldpGetRotate()))
|
toolsPair("Picture", string.format("scale %d%% shift %d, %d rotation %d",
|
||||||
|
vldpGetScale(), x, y, vldpGetRotate())),
|
||||||
|
toolsPair("Kept", dispSaved),
|
||||||
|
toolsKeys("Arrows: move Button 3 / 4: size Coin 1: rotate Start: reset"),
|
||||||
|
toolsKeys("Button 1: keep for this machine Button 2: back"),
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
TOOLS[#TOOLS + 1] = {
|
TOOLS[#TOOLS + 1] = {
|
||||||
id = "toolDisplay",
|
|
||||||
name = "Display",
|
name = "Display",
|
||||||
help = "Size, position and rotation of the picture, and a test pattern.",
|
help = "Size, position and rotation of the picture, and a test pattern.",
|
||||||
|
clear = true,
|
||||||
|
|
||||||
begin = function()
|
begin = function()
|
||||||
|
dispSaved = "not yet"
|
||||||
displayShow()
|
displayShow()
|
||||||
set("dispSaved", "")
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
input = function(what)
|
input = function(what)
|
||||||
|
|
@ -511,13 +595,14 @@ TOOLS[#TOOLS + 1] = {
|
||||||
vldpSetRotate(0)
|
vldpSetRotate(0)
|
||||||
elseif what == SWITCH_BUTTON1 then
|
elseif what == SWITCH_BUTTON1 then
|
||||||
singeSaveGeometry()
|
singeSaveGeometry()
|
||||||
set("dispSaved", "kept for this machine")
|
dispSaved = "kept for this machine"
|
||||||
|
displayShow()
|
||||||
return true
|
return true
|
||||||
else
|
else
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
dispSaved = "not yet"
|
||||||
displayShow()
|
displayShow()
|
||||||
set("dispSaved", "")
|
|
||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
|
@ -563,6 +648,7 @@ TOOLS[#TOOLS + 1] = {
|
||||||
|
|
||||||
-- How long the disc takes to find a frame. A rip with its keyframes far apart seeks slowly, and
|
-- How long the disc takes to find a frame. A rip with its keyframes far apart seeks slowly, and
|
||||||
-- the only symptom in a game is that everything feels heavy; this measures it directly.
|
-- the only symptom in a game is that everything feels heavy; this measures it directly.
|
||||||
|
--
|
||||||
-- Twelve seeks spread over the disc, and a seek that has not landed in three seconds has failed
|
-- Twelve seeks spread over the disc, and a seek that has not landed in three seconds has failed
|
||||||
-- rather than been slow.
|
-- rather than been slow.
|
||||||
local DISC_SEEKS = 12
|
local DISC_SEEKS = 12
|
||||||
|
|
@ -572,8 +658,21 @@ local discPending = nil
|
||||||
local discWorst = 0
|
local discWorst = 0
|
||||||
|
|
||||||
|
|
||||||
|
local function discShow(note)
|
||||||
|
local rows = {}
|
||||||
|
|
||||||
|
if note ~= nil then
|
||||||
|
rows[#rows + 1] = toolsText(note)
|
||||||
|
end
|
||||||
|
for _, line in ipairs(discResults) do
|
||||||
|
rows[#rows + 1] = line
|
||||||
|
end
|
||||||
|
rows[#rows + 1] = toolsKeys("Button 1: run Button 2: back")
|
||||||
|
toolsShow(rows)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
TOOLS[#TOOLS + 1] = {
|
TOOLS[#TOOLS + 1] = {
|
||||||
id = "toolDisc",
|
|
||||||
name = "Disc Test",
|
name = "Disc Test",
|
||||||
help = "Seek timing: how quickly the disc finds a frame.",
|
help = "Seek timing: how quickly the disc finds a frame.",
|
||||||
|
|
||||||
|
|
@ -581,7 +680,7 @@ TOOLS[#TOOLS + 1] = {
|
||||||
discResults = {}
|
discResults = {}
|
||||||
discPending = nil
|
discPending = nil
|
||||||
discWorst = 0
|
discWorst = 0
|
||||||
set("discRows", "<div class='row'>Button 1 runs the test.</div>")
|
discShow("Button 1 runs the test.")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
input = function(what)
|
input = function(what)
|
||||||
|
|
@ -589,6 +688,7 @@ TOOLS[#TOOLS + 1] = {
|
||||||
discResults = {}
|
discResults = {}
|
||||||
discWorst = 0
|
discWorst = 0
|
||||||
discPending = { step = 0, started = 0 }
|
discPending = { step = 0, started = 0 }
|
||||||
|
discShow("Running.")
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
|
|
@ -610,14 +710,16 @@ TOOLS[#TOOLS + 1] = {
|
||||||
end
|
end
|
||||||
if discGetFrame() == discPending.target or singeGetTicks() - discPending.started > DISC_SEEK_LIMIT then
|
if discGetFrame() == discPending.target or singeGetTicks() - discPending.started > DISC_SEEK_LIMIT then
|
||||||
local took = singeGetTicks() - discPending.started
|
local took = singeGetTicks() - discPending.started
|
||||||
discResults[#discResults + 1] = string.format("<div class='sub'>frame %d took %d ms</div>", discPending.target, took)
|
discResults[#discResults + 1] = toolsSub(string.format("frame %d took %d ms", discPending.target, took))
|
||||||
discWorst = math.max(discWorst, took)
|
discWorst = math.max(discWorst, took)
|
||||||
discPending.started = 0
|
discPending.started = 0
|
||||||
if discPending.step >= DISC_SEEKS then
|
if discPending.step >= DISC_SEEKS then
|
||||||
discResults[#discResults + 1] = string.format("<div class='row'>worst <b>%d ms</b> over %d seeks</div>", discWorst, discPending.step)
|
discResults[#discResults + 1] = toolsText(string.format("worst %d ms over %d seeks", discWorst, discPending.step))
|
||||||
discPending = nil
|
discPending = nil
|
||||||
|
discShow(nil)
|
||||||
|
else
|
||||||
|
discShow("Running.")
|
||||||
end
|
end
|
||||||
set("discRows", table.concat(discResults))
|
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
|
@ -638,26 +740,25 @@ local saveSelected = 1
|
||||||
|
|
||||||
local function savedShow()
|
local function savedShow()
|
||||||
local rows = {}
|
local rows = {}
|
||||||
|
|
||||||
if #saveDirs == 0 then
|
if #saveDirs == 0 then
|
||||||
rows[#rows + 1] = "<div class='row'>No game has saved anything yet.</div>"
|
rows[#rows + 1] = toolsText("No game has saved anything yet.")
|
||||||
end
|
end
|
||||||
for i, entry in ipairs(saveDirs) do
|
for i, entry in ipairs(saveDirs) do
|
||||||
local class = (i == saveSelected) and " class='selected'" or ""
|
rows[#rows + 1] = toolsItem(string.format("%s %d bytes", entry.name, entry.size), i == saveSelected)
|
||||||
rows[#rows + 1] = string.format("<div%s>%s <span class='sub'>%d bytes</span></div>", class, entry.name, entry.size)
|
|
||||||
end
|
end
|
||||||
set("savedRows", table.concat(rows))
|
rows[#rows + 1] = toolsKeys("Up / Down: choose Button 3: delete this game's save Button 2: back")
|
||||||
|
toolsShow(rows)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
TOOLS[#TOOLS + 1] = {
|
TOOLS[#TOOLS + 1] = {
|
||||||
id = "toolSaved",
|
|
||||||
name = "Saved Data",
|
name = "Saved Data",
|
||||||
help = "What each game has kept, and clearing it.",
|
help = "What each game has kept, and clearing it.",
|
||||||
|
|
||||||
begin = function()
|
begin = function()
|
||||||
saveDirs = {}
|
saveDirs = {}
|
||||||
saveSelected = 1
|
saveSelected = 1
|
||||||
-- Every game's data directory is one below the root the engine was given.
|
|
||||||
-- lfs.dir hands back its iterator and the directory it walks; the iterator is useless
|
-- lfs.dir hands back its iterator and the directory it walks; the iterator is useless
|
||||||
-- without it, so both are kept. pcall because the root need not exist yet.
|
-- without it, so both are kept. pcall because the root need not exist yet.
|
||||||
local root = singeGetSystemInfo().dataRoot
|
local root = singeGetSystemInfo().dataRoot
|
||||||
|
|
@ -707,6 +808,7 @@ TOOLS[#TOOLS + 1] = {
|
||||||
|
|
||||||
-- The ports this machine has, and whether anything is coming in or going out. Nothing is opened
|
-- The ports this machine has, and whether anything is coming in or going out. Nothing is opened
|
||||||
-- until this tool asks, so a cabinet that never touches MIDI never pays for it.
|
-- until this tool asks, so a cabinet that never touches MIDI never pays for it.
|
||||||
|
--
|
||||||
-- Middle C on channel 1 at a plain mezzo-forte: loud enough to hear on any instrument, and on
|
-- Middle C on channel 1 at a plain mezzo-forte: loud enough to hear on any instrument, and on
|
||||||
-- the channel a synthesiser is most likely to have something assigned to.
|
-- the channel a synthesiser is most likely to have something assigned to.
|
||||||
local MIDI_TEST_CHANNEL = 1
|
local MIDI_TEST_CHANNEL = 1
|
||||||
|
|
@ -720,17 +822,19 @@ local midiDirty = false
|
||||||
|
|
||||||
local function midiShow()
|
local function midiShow()
|
||||||
local rows = {}
|
local rows = {}
|
||||||
rows[#rows + 1] = string.format("<div class='row'>%d in, %d out</div>", midiInputCount(), midiOutputCount())
|
|
||||||
|
rows[#rows + 1] = toolsPair("Ports", string.format("%d in, %d out", midiInputCount(), midiOutputCount()))
|
||||||
for i = 0, midiOutputCount() - 1 do
|
for i = 0, midiOutputCount() - 1 do
|
||||||
rows[#rows + 1] = string.format("<div class='sub'>out %d%s %s</div>", i, (i == midiOut) and " *" or "", midiOutputName(i))
|
rows[#rows + 1] = toolsSub(string.format("out %d%s %s", i, (i == midiOut) and " *" or "", midiOutputName(i)))
|
||||||
end
|
end
|
||||||
for i = 0, midiInputCount() - 1 do
|
for i = 0, midiInputCount() - 1 do
|
||||||
rows[#rows + 1] = string.format("<div class='sub'>in %d%s %s</div>", i, (i == midiIn) and " *" or "", midiInputName(i))
|
rows[#rows + 1] = toolsSub(string.format("in %d%s %s", i, (i == midiIn) and " *" or "", midiInputName(i)))
|
||||||
end
|
end
|
||||||
for _, m in ipairs(midiSeen) do
|
for _, m in ipairs(midiSeen) do
|
||||||
rows[#rows + 1] = string.format("<div class='sub'>received %s</div>", m)
|
rows[#rows + 1] = toolsSub("received " .. m)
|
||||||
end
|
end
|
||||||
set("midiRows", table.concat(rows))
|
rows[#rows + 1] = toolsKeys("Button 1: send a note Button 3: look again Button 2: back")
|
||||||
|
toolsShow(rows)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -744,7 +848,6 @@ end
|
||||||
|
|
||||||
|
|
||||||
TOOLS[#TOOLS + 1] = {
|
TOOLS[#TOOLS + 1] = {
|
||||||
id = "toolMidi",
|
|
||||||
name = "MIDI Ports",
|
name = "MIDI Ports",
|
||||||
help = "List the ports, send a note, watch what arrives.",
|
help = "List the ports, send a note, watch what arrives.",
|
||||||
|
|
||||||
|
|
@ -790,19 +893,25 @@ TOOLS[#TOOLS + 1] = {
|
||||||
|
|
||||||
-- ===== 10. Frame statistics =====
|
-- ===== 10. Frame statistics =====
|
||||||
|
|
||||||
|
local function statsShow()
|
||||||
|
toolsShow({
|
||||||
|
toolsPair("Overlay", statsIsEnabled() and "on" or "off"),
|
||||||
|
toolsText("It stays on while you play, in the top left corner."),
|
||||||
|
toolsKeys("Button 1: turn it on or off Button 2: back"),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
TOOLS[#TOOLS + 1] = {
|
TOOLS[#TOOLS + 1] = {
|
||||||
id = "toolStats",
|
|
||||||
name = "Frame Statistics",
|
name = "Frame Statistics",
|
||||||
help = "The developer's overlay: frame time, what is alive, what the disc is doing.",
|
help = "The developer's overlay: frame time, what is alive, what the disc is doing.",
|
||||||
|
|
||||||
begin = function()
|
begin = statsShow,
|
||||||
set("statsState", statsIsEnabled() and "on" or "off")
|
|
||||||
end,
|
|
||||||
|
|
||||||
input = function(what)
|
input = function(what)
|
||||||
if what == SWITCH_BUTTON1 then
|
if what == SWITCH_BUTTON1 then
|
||||||
statsEnable(not statsIsEnabled())
|
statsEnable(not statsIsEnabled())
|
||||||
set("statsState", statsIsEnabled() and "on" or "off")
|
statsShow()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -215,11 +215,21 @@ body {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The chosen line of a list the tool draws, in the game list's gold. */
|
/* A line a tool offers to choose between, and the chosen one in the game list's gold -- the same
|
||||||
.tool .selected {
|
way the game list itself shows a selection, since RCSS has no generated content to mark it with. */
|
||||||
|
.tool .item {
|
||||||
|
margin-bottom: 2dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool .item.selected {
|
||||||
color: #ffd35a;
|
color: #ffd35a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A gap a tool asked for. */
|
||||||
|
.tool .blank {
|
||||||
|
height: 12dp;
|
||||||
|
}
|
||||||
|
|
||||||
/* Two tools draw their own picture into the overlay, which is underneath the document: the light
|
/* Two tools draw their own picture into the overlay, which is underneath the document: the light
|
||||||
gun's targets and the display's test pattern. Those pages let it through and gather their text
|
gun's targets and the display's test pattern. Those pages let it through and gather their text
|
||||||
into a strip along the bottom, between the two lower targets, so nothing worth aiming at is
|
into a strip along the bottom, between the two lower targets, so nothing worth aiming at is
|
||||||
|
|
@ -238,6 +248,10 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool.clear h1,
|
.tool.clear h1,
|
||||||
|
.tool.clear .row,
|
||||||
|
.tool.clear .pair,
|
||||||
|
.tool.clear .sub,
|
||||||
|
.tool.clear .item,
|
||||||
.tool.clear p {
|
.tool.clear p {
|
||||||
background-color: #000000c0;
|
background-color: #000000c0;
|
||||||
padding: 1dp 8dp;
|
padding: 1dp 8dp;
|
||||||
|
|
|
||||||
|
|
@ -94,14 +94,38 @@ click on a row selects that game and a second click on the same row, or a
|
||||||
click on the Start button in the footer, launches it; the mouse wheel
|
click on the Start button in the footer, launches it; the mouse wheel
|
||||||
scrolls a description too long for its panel. The menu keeps its selection
|
scrolls a description too long for its panel. The menu keeps its selection
|
||||||
in `menu.dat` in its data directory, so it comes back to the same game when
|
in `menu.dat` in its data directory, so it comes back to the same game when
|
||||||
one quits or when Singe is next started.
|
one quits or when Singe is next started. That file is a Lua fragment the menu
|
||||||
|
reads at startup and rewrites at shutdown: `GAME_SELECTED` is the game it was
|
||||||
|
showing, `SHOW_INTRO` is false when it is returning from a game so the intro
|
||||||
|
is not sat through twice, and `MENU_OVERLAY` chooses a renderer (see
|
||||||
|
<<menurenderers,The two renderers>>).
|
||||||
|
|
||||||
A GUI needs the GPU device the 3D scene uses, so on a machine without one
|
[#menurenderers]
|
||||||
`guiNew` fails and the menu cannot start. `Singe/MenuClassic.singe` is the
|
==== The two renderers
|
||||||
previous menu, drawn with sprites and `fontPrint` straight into the overlay,
|
|
||||||
kept for one release for exactly that machine: edit `Menu.sh` (or `Menu.bat`)
|
A GUI document is drawn through the GPU device the 3D scene uses, so on a
|
||||||
to name it in place of `Singe/Menu.singe`. It has the audio delay screen and
|
machine without one there is no document to build. The menu handles that
|
||||||
nothing else; the rest of the service tools need the document.
|
itself: `Singe/Menu.singe` asks <<singehasgpu,singeHasGpu>> at startup and
|
||||||
|
loads one of two renderers.
|
||||||
|
|
||||||
|
`Singe/MenuDocument.singe`::
|
||||||
|
The RmlUi document described above, for a machine with a GPU device.
|
||||||
|
|
||||||
|
`Singe/MenuOverlay.singe`::
|
||||||
|
Sprites and `fontPrint` straight into the overlay, the look Singe 2 had. It
|
||||||
|
needs nothing beyond the overlay every game already draws into.
|
||||||
|
|
||||||
|
Both draw the same menu, take the same keys and run the same service tools;
|
||||||
|
`Singe/Menu.singe` holds everything that is not drawing -- the game list, the
|
||||||
|
selection, launching a game -- so there is one menu rather than two. The
|
||||||
|
overlay renderer shows one game at a time instead of a list, since it has no
|
||||||
|
panel to scroll, and it clips a description too long for its column rather
|
||||||
|
than scrolling it.
|
||||||
|
|
||||||
|
To see the overlay renderer on a machine that does have a GPU, put
|
||||||
|
`MENU_OVERLAY = true` in `menu.dat` in the menu's data directory; the menu
|
||||||
|
writes the line back out, so it survives. That is how the overlay path is
|
||||||
|
tested, and how a cabinet builder who prefers the old look gets it.
|
||||||
|
|
||||||
[#servicetools]
|
[#servicetools]
|
||||||
==== The Service Tools
|
==== The Service Tools
|
||||||
|
|
@ -109,9 +133,11 @@ nothing else; the rest of the service tools need the document.
|
||||||
The key mapped to `INPUT_SERVICE` opens a list of tools over the game list.
|
The key mapped to `INPUT_SERVICE` opens a list of tools over the game list.
|
||||||
Up and down choose one and button 1 opens it; button 2 backs out of a tool to
|
Up and down choose one and button 1 opens it; button 2 backs out of a tool to
|
||||||
the list, and the service key again leaves the tools altogether. Everything
|
the list, and the service key again leaves the tools altogether. Everything
|
||||||
lives in `Singe/Tools.singe`, which the engine extracts beside `Menu.singe`,
|
lives in `Singe/Tools.singe`, which the engine extracts beside `Menu.singe`.
|
||||||
and each tool is a page of `Singe/Menu.rml`. Nothing here needs a game to be
|
Nothing there draws: a tool builds a list of rows and hands it to the menu,
|
||||||
running, and nothing changes a game's own files.
|
which is why the same ten tools run on both renderers (see
|
||||||
|
<<menurenderers,The two renderers>>). Nothing here needs a game to be running,
|
||||||
|
and nothing changes a game's own files.
|
||||||
|
|
||||||
Audio Delay::
|
Audio Delay::
|
||||||
The downstream audio delay, measured against a click and a flash. See
|
The downstream audio delay, measured against a click and a flash. See
|
||||||
|
|
@ -947,10 +973,11 @@ longer there.
|
||||||
Singe/ Support files extracted by the engine
|
Singe/ Support files extracted by the engine
|
||||||
Framework.singe Loaded by every game (dofile it)
|
Framework.singe Loaded by every game (dofile it)
|
||||||
Menu.singe The bundled game menu
|
Menu.singe The bundled game menu
|
||||||
Menu.rml The menu's RmlUi document
|
|
||||||
menu.rcss The menu's style sheet, on top of gui.rcss
|
|
||||||
Tools.singe The menu's service tools (see The Service Tools)
|
Tools.singe The menu's service tools (see The Service Tools)
|
||||||
MenuClassic.singe The previous, overlay-drawn menu (see The Bundled Menu)
|
MenuDocument.singe The menu drawn as a GUI document
|
||||||
|
MenuOverlay.singe The menu drawn into the overlay, for a machine with no GPU
|
||||||
|
Menu.rml The document renderer's RmlUi document
|
||||||
|
menu.rcss Its style sheet, on top of gui.rcss
|
||||||
gui.rcss The shipped GUI theme
|
gui.rcss The shipped GUI theme
|
||||||
controls.cfg.example Template for input mappings
|
controls.cfg.example Template for input mappings
|
||||||
click.wav Used by the menu's audio delay and sound tests
|
click.wav Used by the menu's audio delay and sound tests
|
||||||
|
|
@ -1419,9 +1446,10 @@ texture goes wherever a texture can: composited over the overlay with
|
||||||
the same options page works flat on the screen and on the monitor of a
|
the same options page works flat on the screen and on the monitor of a
|
||||||
modeled arcade cabinet. A GUI needs the GPU device the 3D scene uses, so it
|
modeled arcade cabinet. A GUI needs the GPU device the 3D scene uses, so it
|
||||||
is unavailable on a machine without one (see <<sceneperformance,Performance
|
is unavailable on a machine without one (see <<sceneperformance,Performance
|
||||||
and Requirements>>); `guiNew` raises an error there. The bundled menu is such
|
and Requirements>>); `guiNew` raises an error rather than answering false, so
|
||||||
a document, which is why `Singe/MenuClassic.singe` exists (see
|
ask <<singehasgpu,singeHasGpu>> first if your game can do without. The bundled
|
||||||
<<bundledmenu,The Bundled Menu>>). Every call named here has an entry in the
|
menu does exactly that, and draws itself into the overlay instead when the
|
||||||
|
answer is no (see <<menurenderers,The two renderers>>). Every call named here has an entry in the
|
||||||
<<guiref,Gui>> section of the reference.
|
<<guiref,Gui>> section of the reference.
|
||||||
|
|
||||||
The language reference for RML and RCSS is RmlUi's own documentation at
|
The language reference for RML and RCSS is RmlUi's own documentation at
|
||||||
|
|
@ -14227,6 +14255,40 @@ else
|
||||||
end
|
end
|
||||||
----
|
----
|
||||||
|
|
||||||
|
[#singehasgpu]
|
||||||
|
==== singeHasGpu
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
present = singeHasGpu()
|
||||||
|
----
|
||||||
|
|
||||||
|
Whether the GPU device came up. Everything drawn through it is unavailable
|
||||||
|
without one: the 3D scene, the particles, and every GUI document. Ask this
|
||||||
|
before asking for any of them, because <<guinew,guiNew>> and the scene calls
|
||||||
|
end the script rather than answering `false` -- there is no way to try and
|
||||||
|
recover.
|
||||||
|
|
||||||
|
A machine can lack one because it has no Vulkan, Direct3D 12 or Metal driver
|
||||||
|
at all, or because `--nogpu` was given. The bundled menu calls this at startup
|
||||||
|
and picks a renderer from the answer (see <<menurenderers,The two renderers>>).
|
||||||
|
|
||||||
|
*Returns:* boolean, true when a GPU device is present.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<guinew,guiNew>>, <<singegetsysteminfo,singeGetSystemInfo>>, <<sceneperformance,Performance and Requirements>>
|
||||||
|
|
||||||
|
.Example
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
-- A game with a 3D title screen and a flat one to fall back on.
|
||||||
|
if singeHasGpu() then
|
||||||
|
buildSceneTitle()
|
||||||
|
else
|
||||||
|
buildSpriteTitle()
|
||||||
|
end
|
||||||
|
----
|
||||||
|
|
||||||
[#singequit]
|
[#singequit]
|
||||||
==== singeQuit
|
==== singeQuit
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@
|
||||||
#include "generated/settings_cfg.h"
|
#include "generated/settings_cfg.h"
|
||||||
#include "generated/Menu_singe.h"
|
#include "generated/Menu_singe.h"
|
||||||
#include "generated/Tools_singe.h"
|
#include "generated/Tools_singe.h"
|
||||||
#include "generated/MenuClassic_singe.h"
|
#include "generated/MenuDocument_singe.h"
|
||||||
|
#include "generated/MenuOverlay_singe.h"
|
||||||
#include "generated/Menu_rml.h"
|
#include "generated/Menu_rml.h"
|
||||||
#include "generated/menu_rcss.h"
|
#include "generated/menu_rcss.h"
|
||||||
#include "generated/gui_rcss.h"
|
#include "generated/gui_rcss.h"
|
||||||
|
|
|
||||||
|
|
@ -1791,7 +1791,8 @@ static void _unpackData(const char *exePath, bool absolute) {
|
||||||
{ "settings.cfg.example", settings_cfg, settings_cfg_len },
|
{ "settings.cfg.example", settings_cfg, settings_cfg_len },
|
||||||
{ "Menu.singe", Menu_singe, Menu_singe_len },
|
{ "Menu.singe", Menu_singe, Menu_singe_len },
|
||||||
{ "Tools.singe", Tools_singe, Tools_singe_len },
|
{ "Tools.singe", Tools_singe, Tools_singe_len },
|
||||||
{ "MenuClassic.singe", MenuClassic_singe, MenuClassic_singe_len },
|
{ "MenuDocument.singe", MenuDocument_singe, MenuDocument_singe_len },
|
||||||
|
{ "MenuOverlay.singe", MenuOverlay_singe, MenuOverlay_singe_len },
|
||||||
{ "Menu.rml", Menu_rml, Menu_rml_len },
|
{ "Menu.rml", Menu_rml, Menu_rml_len },
|
||||||
{ "menu.rcss", menu_rcss, menu_rcss_len },
|
{ "menu.rcss", menu_rcss, menu_rcss_len },
|
||||||
{ "FreeSansBold.ttf", FreeSansBold_ttf, FreeSansBold_ttf_len },
|
{ "FreeSansBold.ttf", FreeSansBold_ttf, FreeSansBold_ttf_len },
|
||||||
|
|
|
||||||
16
src/singe.c
16
src/singe.c
|
|
@ -1275,6 +1275,7 @@ static int32_t apiSingeGetPauseFlag(lua_State *L);
|
||||||
static int32_t apiSingeGetScriptPath(lua_State *L);
|
static int32_t apiSingeGetScriptPath(lua_State *L);
|
||||||
static int32_t apiSingeGetTicks(lua_State *L);
|
static int32_t apiSingeGetTicks(lua_State *L);
|
||||||
static int32_t apiSingeGetWidth(lua_State *L);
|
static int32_t apiSingeGetWidth(lua_State *L);
|
||||||
|
static int32_t apiSingeHasGpu(lua_State *L);
|
||||||
static int32_t apiSingeQuit(lua_State *L);
|
static int32_t apiSingeQuit(lua_State *L);
|
||||||
static int32_t apiSingeReload(lua_State *L);
|
static int32_t apiSingeReload(lua_State *L);
|
||||||
static int32_t apiSingeScreenshot(lua_State *L);
|
static int32_t apiSingeScreenshot(lua_State *L);
|
||||||
|
|
@ -5478,6 +5479,7 @@ static void _registerApi(lua_State *L) {
|
||||||
lua_register(L, "singeGetScriptPath", apiSingeGetScriptPath); // 1.15 RDG
|
lua_register(L, "singeGetScriptPath", apiSingeGetScriptPath); // 1.15 RDG
|
||||||
lua_register(L, "singeGetTicks", apiSingeGetTicks); // 3.00
|
lua_register(L, "singeGetTicks", apiSingeGetTicks); // 3.00
|
||||||
lua_register(L, "singeGetWidth", apiSingeGetWidth); // 1.xx
|
lua_register(L, "singeGetWidth", apiSingeGetWidth); // 1.xx
|
||||||
|
lua_register(L, "singeHasGpu", apiSingeHasGpu); // 3.00
|
||||||
lua_register(L, "singeQuit", apiSingeQuit); // 1.xx RDG
|
lua_register(L, "singeQuit", apiSingeQuit); // 1.xx RDG
|
||||||
lua_register(L, "singeReload", apiSingeReload); // 3.00
|
lua_register(L, "singeReload", apiSingeReload); // 3.00
|
||||||
lua_register(L, "singeSaveGeometry", apiSingeSaveGeometry); // 3.00
|
lua_register(L, "singeSaveGeometry", apiSingeSaveGeometry); // 3.00
|
||||||
|
|
@ -12724,6 +12726,20 @@ static int32_t apiSingeGetWidth(lua_State *L) {
|
||||||
|
|
||||||
|
|
||||||
// singeQuit()
|
// singeQuit()
|
||||||
|
// Whether the GPU device came up. Everything drawn through it -- the 3D scene, the particles and
|
||||||
|
// every GUI document -- is unavailable without one, and guiNew ends the script rather than answering
|
||||||
|
// false, so a script that can do without has to ask before it asks for a GUI.
|
||||||
|
static int32_t apiSingeHasGpu(lua_State *L) {
|
||||||
|
bool has = (_global.device != NULL);
|
||||||
|
|
||||||
|
_argCheck(L, "singeHasGpu", 0, 0);
|
||||||
|
_luaTrace(L, "singeHasGpu", "%s", has ? "true" : "false");
|
||||||
|
lua_pushboolean(L, has);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static int32_t apiSingeQuit(lua_State *L) {
|
static int32_t apiSingeQuit(lua_State *L) {
|
||||||
_luaTrace(L, "singeQuit", "Quit requested.");
|
_luaTrace(L, "singeQuit", "Quit requested.");
|
||||||
_global.running = false;
|
_global.running = false;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue