More tools added to the menu for assisting with setup.

This commit is contained in:
Scott Duensing 2026-09-11 18:19:13 -05:00
parent e7b24ea7b3
commit 421ef75bdc
14 changed files with 1512 additions and 191 deletions

View file

@ -587,7 +587,7 @@ API Changes
delay (DAC, receiver, Bluetooth, display). Press the service key on delay (DAC, receiver, Bluetooth, display). Press the service key on
the game list; a click plays once a second and the screen flashes. the game list; a click plays once a second and the screen flashes.
Adjust with the stick until the flash and the click coincide, then Adjust with the stick until the flash and the click coincide, then
press button 1 to save. The value is kept in audio.cfg in the data press button 1 to save. The value is kept in machine.cfg in the data
root and applied to every game on the machine, from the menu or the root and applied to every game on the machine, from the menu or the
command line, on top of any per-game AUDIO_DELAY. Scripts can read and command line, on top of any per-game AUDIO_DELAY. Scripts can read and
set it with singeGetAudioCalibration() / singeSetAudioCalibration(), set it with singeGetAudioCalibration() / singeSetAudioCalibration(),
@ -862,6 +862,41 @@ 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.
- 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:
Audio Delay, Input Test, System Information, Light Gun, Sound Test,
Display, Disc Test, Saved Data, MIDI Ports and Frame Statistics.
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
each is a page of Singe/Menu.rml, so a cabinet builder can add one.
MenuClassic keeps the audio delay screen and nothing else.
Four calls were added for them, and are worth having on their own:
singeGetSystemInfo() returns everything the trace header knows in one
table -- version, operating system, processor, renderer, 3D device,
video decoders, audio formats, SoundFont, MIDI, window and canvas
sizes, this game's data directory and the data root every game writes
under -- so a bug report can be a photograph.
vldpGetShift() and vldpSetShift(x, y) move the picture within the
window while the game runs, the running equivalent of --shiftx and
--shifty, beside vldpGetScale and vldpSetScale.
singeSaveGeometry() keeps the scale, shift and rotation for this
machine, in the same machine.cfg the audio delay uses, and the engine
applies them to every game it starts afterwards unless the command
line names its own. A monitor is squared up once, not once per game.
The file is called machine.cfg rather than audio.cfg, because the
audio delay is no longer all it holds.
discGetFrameCount() answers how many frames the disc has. A framefile
answers for the whole disc rather than the segment playing.
SINGE_CONTROLLER_AXES and SINGE_CONTROLLER_BUTTONS join the input
layout globals, so a service screen can walk a pad without knowing the
numbers.
- Subtitles carried inside the video file: discGetSubtitleTracks(), - Subtitles carried inside the video file: discGetSubtitleTracks(),
discGetSubtitleLanguage(track) and srtLoadTrack(track), which reads discGetSubtitleLanguage(track) and srtLoadTrack(track), which reads
one track out of the disc's own container and loads it exactly as one track out of the disc's own container and loads it exactly as

View file

@ -189,6 +189,7 @@ singeEmbed(${CMAKE_SOURCE_DIR}/assets/Framework.singe ${GENERATED_DIR}/Framework
singeEmbed(${CMAKE_SOURCE_DIR}/assets/controls.cfg ${GENERATED_DIR}/controls_cfg.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/controls.cfg ${GENERATED_DIR}/controls_cfg.h "")
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/MenuClassic.singe ${GENERATED_DIR}/MenuClassic_singe.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuClassic.singe ${GENERATED_DIR}/MenuClassic_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 "")

View file

@ -28,17 +28,72 @@
<div id="description"></div> <div id="description"></div>
</div> </div>
<div id="footer"> <div id="footer">
<div id="hints">Up / Down: select &nbsp; Left / Right: page &nbsp; Start or button: play &nbsp; Service: audio delay</div> <div id="hints">Up / Down: select &nbsp; Left / Right: page &nbsp; Start or button: play &nbsp; Service: tools</div>
<button id="start">Start</button> <button id="start">Start</button>
</div> </div>
</div> </div>
<div id="calibration" class="hidden"> <div id="tools" class="tool hidden">
<h1>Audio Delay Calibration</h1> <h1>Service Tools</h1>
<div id="toolList"></div>
<p class="value" id="toolHelp"></p>
<p class="keys">Up / Down: choose &nbsp; Button 1: open &nbsp; Service: back to the games</p>
</div>
<div id="toolAudio" class="tool hidden">
<h1>Audio Delay</h1>
<p>Adjust until the flash and the click happen together.</p> <p>Adjust until the flash and the click happen together.</p>
<p class="value">Delay: <span id="calValue"></span> ms</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="value">Measured device queue: <span id="calLatency"></span> ms</p>
<p class="keys">Left / Right: 10 ms &nbsp; Up / Down: 1 ms &nbsp; Start: reset to 0</p> <p class="keys">Left / Right: 10 ms &nbsp; Up / Down: 1 ms &nbsp; Start: reset to 0</p>
<p class="keys">Button 1: save &nbsp; Button 2: cancel</p> <p class="keys">Button 1: save &nbsp; 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> &nbsp; shots shown: <span id="gunCount"></span></p>
<p class="keys">Button 1: fire &nbsp; Start: clear &nbsp; 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 &nbsp; Left / Right: disc track &nbsp; 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 &nbsp; Button 3 / 4: size &nbsp; Coin 1: rotate &nbsp; Start: reset</p>
<p class="keys">Button 1: keep for this machine &nbsp; 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 &nbsp; 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 &nbsp; Button 3: delete this game's save &nbsp; 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 &nbsp; Button 3: look again &nbsp; 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 &nbsp; Button 2: back</p>
</div> </div>
</body> </body>
</rml> </rml>

View file

@ -29,102 +29,11 @@
-- Singe/MenuClassic.singe is the previous, sprite drawn menu, kept for one release. -- Singe/MenuClassic.singe is the previous, sprite drawn menu, kept for one release.
dofile("Singe/Framework.singe") dofile("Singe/Framework.singe")
dofile("Singe/Tools.singe")
local lfs = require("lfs") 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
guiSetValue(GUI, DOCUMENT, "calLatency", tostring(CAL_LATENCY))
calibrationShowValue()
menuShowPage(true)
end
function calibrationEnd(save)
if save then
singeSetAudioCalibration(CAL_VALUE)
end
CALIBRATING = false
menuElement("calibration"):SetClass("flash", false)
menuShowPage(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
calibrationShowValue()
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))
calibrationShowValue()
end
end
function calibrationShowValue()
guiSetValue(GUI, DOCUMENT, "calValue", tostring(CAL_VALUE))
end
function calibrationUpdate()
local now = singeGetTicks()
local gap = CAL_LATENCY + CAL_VALUE -- flash this long after the click; negative means before
local flash = false
-- 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
-- The page is white for the one frame of the flash and black otherwise.
menuElement("calibration"):SetClass("flash", flash)
end
function cleanTitle(a) function cleanTitle(a)
local output = string.lower(a) local output = string.lower(a)
output = string.gsub(output, '%p', ' ') output = string.gsub(output, '%p', ' ')
@ -189,7 +98,7 @@ end
-- The game page takes input once the disc has reached the grid behind it. -- The game page takes input once the disc has reached the grid behind it.
function menuActive() function menuActive()
return (not CALIBRATING) and (discGetFrame() >= DISC_GRID_START) return (not toolsActive()) and (discGetFrame() >= DISC_GRID_START)
end end
@ -332,13 +241,6 @@ function menuScrollToSelection()
end end
-- The game page or the calibration page; never both.
function menuShowPage(calibrating)
menuElement("games"):SetClass("hidden", calibrating)
menuElement("calibration"):SetClass("hidden", not calibrating)
end
function menuStartClick(gui, document, id, event, value) function menuStartClick(gui, document, id, event, value)
if menuActive() then if menuActive() then
gameStart() gameStart()
@ -348,8 +250,9 @@ end
function onInputPressed(what) function onInputPressed(what)
if CALIBRATING then -- The service tools take every switch while they are up, including the one that closes them.
calibrationInput(what) if toolsActive() then
toolsInput(what)
return return
end end
@ -357,7 +260,7 @@ function onInputPressed(what)
if menuActive() then if menuActive() then
if what == SWITCH_SERVICE then if what == SWITCH_SERVICE then
calibrationBegin() toolsBegin()
elseif what == SWITCH_UP then elseif what == SWITCH_UP then
gameSelect(GAME_SELECTED - 1) gameSelect(GAME_SELECTED - 1)
elseif what == SWITCH_DOWN then elseif what == SWITCH_DOWN then
@ -398,8 +301,8 @@ function onOverlayUpdate()
colorBackground(0, 0, 0, 0) colorBackground(0, 0, 0, 0)
overlayClear() overlayClear()
if CALIBRATING then if toolsActive() then
calibrationUpdate() toolsUpdate()
guiDraw(GUI) guiDraw(GUI)
return(OVERLAY_UPDATED) return(OVERLAY_UPDATED)
end end
@ -530,13 +433,8 @@ else
LIST_SCROLL_PENDING = false LIST_SCROLL_PENDING = false
SHUTDOWN_FROM_PUSH = false SHUTDOWN_FROM_PUSH = false
-- Audio delay calibration screen (SWITCH_SERVICE) -- The service tools (SWITCH_SERVICE) share this click with the audio delay screen.
CALIBRATING = false SND_CLICK = soundLoad("Singe/click.wav")
CAL_PERIOD = 1000
CAL_STEP_COARSE = 10
CAL_STEP_FINE = 1
CAL_LIMIT = 1000
SND_CLICK = soundLoad("Singe/click.wav")
-- Load configuration -- Load configuration
SHOW_INTRO = true SHOW_INTRO = true

810
assets/Tools.singe Normal file
View file

@ -0,0 +1,810 @@
--[[
*
* 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 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
-- up to four functions: begin when it opens, input for a switch, update once a frame, and finish
-- when it closes. Anything a tool leaves out simply does not happen.
--
-- Only the RmlUi menu has these. Singe/MenuClassic.singe exists for a machine with no GPU device,
-- where there is no document to put a page in; it keeps its own audio delay screen and nothing
-- else.
local lfs = require("lfs")
TOOL_SELECTED = 1
TOOL_OPEN = nil -- The tool being used, or nil while the list itself is showing
TOOLS = {}
-- Writing into a page is the one thing every tool does, and the document and the GUI are the same
-- ones every time, so they are named here rather than in twenty-two places.
local function set(id, text)
guiSetValue(GUI, DOCUMENT, id, text)
end
-- ===== The list =====
function toolsBegin()
TOOL_OPEN = nil
toolsShowList()
toolsShowPage("tools")
end
function toolsEnd()
if TOOL_OPEN and TOOL_OPEN.finish then
TOOL_OPEN.finish()
end
TOOL_OPEN = nil
toolsShowPage(nil)
end
-- 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
function toolsShowList()
local rows = {}
for i, tool in ipairs(TOOLS) do
local class = (i == TOOL_SELECTED) and ' class="selected"' or ""
rows[#rows + 1] = string.format("<div%s>%s</div>", class, tool.name)
end
set("toolList", table.concat(rows))
set("toolHelp", TOOLS[TOOL_SELECTED].help or "")
end
function toolsOpen(index)
TOOL_OPEN = TOOLS[index]
if TOOL_OPEN.begin then
TOOL_OPEN.begin()
end
toolsShowPage(TOOL_OPEN.id)
end
function toolsClose()
if TOOL_OPEN and TOOL_OPEN.finish then
TOOL_OPEN.finish()
end
TOOL_OPEN = nil
toolsShowList()
toolsShowPage("tools")
end
-- Every switch while the tools are up. A tool that wants a switch for itself says so by returning
-- true from its input function; everything else falls through to the rules here, so Button 2 always
-- backs out and SERVICE always leaves.
function toolsInput(what)
if TOOL_OPEN then
if TOOL_OPEN.input and TOOL_OPEN.input(what) then
return
end
if what == SWITCH_BUTTON2 or what == SWITCH_SERVICE then
toolsClose()
end
return
end
if what == SWITCH_UP then
TOOL_SELECTED = (TOOL_SELECTED == 1) and #TOOLS or (TOOL_SELECTED - 1)
toolsShowList()
elseif what == SWITCH_DOWN then
TOOL_SELECTED = (TOOL_SELECTED == #TOOLS) and 1 or (TOOL_SELECTED + 1)
toolsShowList()
elseif what == SWITCH_BUTTON1 or what == SWITCH_START1 or what == SWITCH_START2 then
toolsOpen(TOOL_SELECTED)
elseif what == SWITCH_BUTTON2 or what == SWITCH_SERVICE then
toolsEnd()
end
end
function toolsUpdate()
if TOOL_OPEN and TOOL_OPEN.update then
TOOL_OPEN.update()
end
end
-- Whether the tools have the screen, which is what the menu asks before doing anything of its own.
function toolsActive()
return TOOL_OPEN ~= nil or not menuElement("tools"):IsClassSet("hidden")
end
-- ===== 1. Audio delay =====
-- 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 this machine's delay.
local CAL_PERIOD = 1000
local CAL_STEP_COARSE = 10
local CAL_STEP_FINE = 1
local CAL_LIMIT = 1000
local calValue, calBeat, calClickDone, calFlashDone
local function audioShow()
set("calValue", tostring(calValue))
end
TOOLS[#TOOLS + 1] = {
id = "toolAudio",
name = "Audio Delay",
help = "Line the click up with the flash so sound and picture agree.",
begin = function()
calValue = singeGetAudioCalibration()
calBeat = singeGetTicks() - CAL_PERIOD
calClickDone = true
calFlashDone = true
set("calLatency", tostring(singeGetAudioLatency()))
audioShow()
end,
input = function(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
calValue = 0
audioShow()
return true
elseif what == SWITCH_BUTTON1 then
singeSetAudioCalibration(calValue)
toolsClose()
return true
end
if delta ~= 0 then
calValue = math.max(-CAL_LIMIT, math.min(CAL_LIMIT, calValue + delta))
audioShow()
return true
end
return false
end,
update = function()
local now = singeGetTicks()
if now - calBeat >= CAL_PERIOD then
calBeat = now
calClickDone = false
calFlashDone = false
end
-- The click goes first; the flash waits out the device queue and the candidate delay, so
-- the two land together only when the candidate matches the machine.
if not calClickDone then
calClickDone = true
soundPlay(SND_CLICK)
end
if not calFlashDone and (now - calBeat >= singeGetAudioLatency() + calValue) then
calFlashDone = true
menuElement("toolAudio"):SetClass("flash", true)
elseif calFlashDone then
menuElement("toolAudio"):SetClass("flash", false)
end
end,
finish = function()
menuElement("toolAudio"):SetClass("flash", false)
end,
}
-- ===== 2. Input test =====
-- Every device the engine can see and what it is doing right now. Singe opens every controller as
-- a gamepad, an unrecognised one through a mapping it writes itself, so there is one family to
-- report rather than two. The switch line shows what the game would actually receive, which is the
-- question being asked when a button "does nothing".
local inputLast = "none yet"
local inputCount = 0
-- The engine sets every switch as a SWITCH_ global, so the names come from there rather than from a
-- second list here that could fall behind it.
local SWITCH_NAMES = {}
for name, value in pairs(_G) do
if type(name) == "string" and type(value) == "number" and name:sub(1, 7) == "SWITCH_" then
SWITCH_NAMES[value] = name
end
end
TOOLS[#TOOLS + 1] = {
id = "toolInput",
name = "Input Test",
help = "Controllers, keyboard, mice and light guns, live.",
begin = function()
inputLast = "none yet"
inputCount = 0
end,
input = function(what)
inputCount = inputCount + 1
inputLast = string.format("%s (%d)", SWITCH_NAMES[what] or "unnamed", what)
-- Button 2 is how every other tool backs out, so it is reported and then allowed through.
return false
end,
update = function()
local rows = {}
rows[#rows + 1] = string.format("<div class='row'>Last switch: <b>%s</b> &nbsp; count: %d</div>", inputLast, inputCount)
rows[#rows + 1] = string.format("<div class='row'>Gamepads: %d &nbsp; Mice: %d &nbsp; Stick drives mouse: %s</div>",
controllerHowMany(), mouseHowMany(), tostring(joyMouseIsEnabled()))
for slot = 0, SINGE_MAX_CONTROLLERS - 1 do
if controllerIsValid(slot) then
local axes = {}
for a = 0, SINGE_CONTROLLER_AXES - 1 do
axes[#axes + 1] = string.format("%d", controllerGetAxis(slot, a))
end
local buttons = {}
-- controllerGetButton wants the framework's own code for the button, not SDL's
-- index, so it is built here the way Framework.singe builds GAMEPAD_0.
for b = 0, SINGE_CONTROLLER_BUTTONS - 1 do
if controllerGetButton(slot, SINGE_GAMEPAD_BASE + slot * SINGE_GAMEPAD_STRIDE + SINGE_GAMEPAD_BUTTON_OFFSET + b) then
buttons[#buttons + 1] = tostring(b)
end
end
rows[#rows + 1] = string.format("<div class='row'>%d: <b>%s</b></div>", slot, controllerGetName(slot))
rows[#rows + 1] = string.format("<div class='sub'>axes %s &nbsp; buttons %s</div>",
table.concat(axes, ", "), (#buttons > 0) and table.concat(buttons, " ") or "-")
end
end
for m = 0, mouseHowMany() - 1 do
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)
end
set("inputRows", table.concat(rows))
end,
}
-- ===== 3. System information =====
-- 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.
TOOLS[#TOOLS + 1] = {
id = "toolSystem",
name = "System Information",
help = "What this machine is and what the engine chose to run on.",
begin = function()
local i = singeGetSystemInfo()
local rows = {}
local function line(label, value)
rows[#rows + 1] = string.format("<div class='pair'><span class='label'>%s</span><span class='detail'>%s</span></div>", label, tostring(value))
end
line("Singe", i.version)
line("System", i.os)
line("Processor", i.cpu)
line("Renderer", i.renderer)
line("3D device", i.gpu)
line("Video", i.decoder)
line("Audio", i.audio)
line("SoundFont", i.soundFont)
line("MIDI", i.midi)
line("Window", string.format("%d x %d, canvas %d x %d, overlay %d x %d",
i.windowWidth, i.windowHeight, i.canvasWidth, i.canvasHeight, overlayGetWidth(), overlayGetHeight()))
line("Data", i.dataPath)
set("systemRows", table.concat(rows))
end,
}
-- ===== 4. Light gun =====
-- 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
-- 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
-- slightly out still puts its shot on the screen beside them.
local GUN_SHOTS_KEPT = 12
local GUN_INSET = 24
local GUN_RING = 14
local GUN_CENTRE = 2
local GUN_CROSS = 8
local gunShots = {}
local gunLast = "none yet"
TOOLS[#TOOLS + 1] = {
id = "toolGun",
name = "Light Gun",
help = "Aim and fire: see where the shot actually lands.",
begin = function()
gunShots = {}
gunLast = "none yet"
end,
input = function(what)
if what == SWITCH_BUTTON1 then
local x, y = mouseGetPosition(0)
gunShots[#gunShots + 1] = { x = x, y = y }
while #gunShots > GUN_SHOTS_KEPT do
table.remove(gunShots, 1)
end
gunLast = string.format("%d, %d", x, y)
return true
elseif what == SWITCH_START1 or what == SWITCH_START2 then
gunShots = {}
return true
end
return false
end,
update = function()
local w, h = overlayGetWidth(), overlayGetHeight()
-- A target in the middle and one in each corner: a gun that is square on the picture puts
-- its shots on them, and one that is not shows which way it is out.
overlayClear()
colorForeground(90, 90, 90)
overlayBox(1, 1, w - 2, h - 2)
overlayLine(w / 2, 0, w / 2, h)
overlayLine(0, h / 2, w, h / 2)
colorForeground(255, 255, 0)
for _, p in ipairs({ { w / 2, h / 2 }, { GUN_INSET, GUN_INSET }, { w - GUN_INSET, GUN_INSET },
{ GUN_INSET, h - GUN_INSET }, { w - GUN_INSET, h - GUN_INSET } }) do
overlayCircle(p[1], p[2], GUN_RING)
overlayCircle(p[1], p[2], GUN_CENTRE)
end
colorForeground(255, 60, 60)
for _, shot in ipairs(gunShots) do
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)
end
set("gunLast", gunLast)
set("gunCount", tostring(#gunShots))
end,
finish = function()
overlayClear()
end,
}
-- ===== 5. Sound test =====
-- Which speaker is which, and how loud each kind of sound is. A cabinet with its wires crossed
-- sounds perfectly fine until something is meant to come from one side.
local soundStep = 0
local soundNames = { "left speaker", "right speaker", "both speakers" }
local soundPans = { -1.0, 1.0, 0.0 }
TOOLS[#TOOLS + 1] = {
id = "toolSound",
name = "Sound Test",
help = "Left, right, both, and the disc's own audio tracks.",
begin = function()
soundStep = 0
set("soundWhich", "press Button 1")
local tracks = {}
for t = 0, discGetAudioTracks() - 1 do
local code = discGetLanguage(t)
tracks[#tracks + 1] = string.format("%d:%s", t, (code ~= "" and code ~= "unk") and code or "unnamed")
end
set("soundTracks", (#tracks > 0) and table.concat(tracks, " &nbsp; ") or "the disc has none")
end,
input = function(what)
if what == SWITCH_BUTTON1 then
soundStep = (soundStep % #soundNames) + 1
soundSetPan(SND_CLICK, soundPans[soundStep])
soundPlay(SND_CLICK)
set("soundWhich", soundNames[soundStep])
return true
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()
if count > 1 then
local next = (discGetAudioTrack() + ((what == SWITCH_RIGHT) and 1 or (count - 1))) % count
discSetAudioTrack(next)
set("soundWhich", string.format("disc track %d (%s)", next, discGetLanguage(next)))
end
return true
end
return false
end,
finish = function()
soundSetPan(SND_CLICK, 0.0)
end,
}
-- ===== 6. Display =====
-- The monitor, and the picture's place on it. Scale, shift and rotation are live, so the operator
-- sees what they are doing, and singeSaveGeometry keeps them in machine.cfg beside the audio delay
-- -- once for the cabinet rather than once for every game.
--
-- The test pattern: a 16 by 12 grid, which is square on a 4:3 monitor and on a 16:9 one when the
-- picture is letterboxed into it, and an eight step greyscale wedge a thirty-second of the width
-- per step.
local FULL_TURN = 360
local DISP_COLUMNS = 16
local DISP_ROWS = 12
local DISP_STEPS = 8
local DISP_STEP_WIDE = 32
local DISP_WEDGE = 16
local function displayShow()
local x, y = vldpGetShift()
set("dispValues", string.format("scale %d%% &nbsp; shift %d, %d &nbsp; rotation %d",
vldpGetScale(), x, y, vldpGetRotate()))
end
TOOLS[#TOOLS + 1] = {
id = "toolDisplay",
name = "Display",
help = "Size, position and rotation of the picture, and a test pattern.",
begin = function()
displayShow()
set("dispSaved", "")
end,
input = function(what)
local x, y = vldpGetShift()
if what == SWITCH_LEFT then
vldpSetShift(math.max(SINGE_SHIFT_MIN, x - 1), y)
elseif what == SWITCH_RIGHT then
vldpSetShift(math.min(SINGE_SHIFT_MAX, x + 1), y)
elseif what == SWITCH_UP then
vldpSetShift(x, math.max(SINGE_SHIFT_MIN, y - 1))
elseif what == SWITCH_DOWN then
vldpSetShift(x, math.min(SINGE_SHIFT_MAX, y + 1))
elseif what == SWITCH_BUTTON3 then
vldpSetScale(math.max(SINGE_SCALE_MIN, vldpGetScale() - 1))
elseif what == SWITCH_BUTTON4 then
vldpSetScale(math.min(SINGE_SCALE_MAX, vldpGetScale() + 1))
elseif what == SWITCH_COIN1 then
vldpSetRotate((vldpGetRotate() + SINGE_ROTATE_STEP) % FULL_TURN)
elseif what == SWITCH_START1 or what == SWITCH_START2 then
vldpSetScale(SINGE_SCALE_MAX)
vldpSetShift(0, 0)
vldpSetRotate(0)
elseif what == SWITCH_BUTTON1 then
singeSaveGeometry()
set("dispSaved", "kept for this machine")
return true
else
return false
end
displayShow()
set("dispSaved", "")
return true
end,
update = function()
-- A grid to square the picture up with, a bar of greys to set brightness by, and a frame
-- one pixel inside the edge: if any of the frame is missing, the monitor is overscanning.
local w, h = overlayGetWidth(), overlayGetHeight()
overlayClear()
colorForeground(60, 60, 60)
for gx = 0, w, math.floor(w / DISP_COLUMNS) do
overlayLine(gx, 0, gx, h)
end
for gy = 0, h, math.floor(h / DISP_ROWS) do
overlayLine(0, gy, w, gy)
end
-- The greyscale wedge. overlayBox draws an outline, so each step is filled by its own
-- columns; a monitor with its brightness wrong loses the dark end or the light one.
local step = math.floor(w / DISP_STEP_WIDE)
local first = math.floor((w - DISP_STEPS * step) / 2)
local top = math.floor(h / 2) - DISP_WEDGE
local bottom = math.floor(h / 2) + DISP_WEDGE
for i = 0, DISP_STEPS - 1 do
local level = math.floor(i * 255 / (DISP_STEPS - 1))
local left = first + i * step
colorForeground(level, level, level)
for x = left, left + step - 1 do
overlayLine(x, top, x, bottom)
end
end
-- Two frames, one inside the other: a monitor that overscans eats the outer one first.
colorForeground(255, 255, 255)
overlayBox(0, 0, w - 1, h - 1)
overlayBox(1, 1, w - 2, h - 2)
end,
finish = function()
overlayClear()
end,
}
-- ===== 7. Disc test =====
-- 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.
-- Twelve seeks spread over the disc, and a seek that has not landed in three seconds has failed
-- rather than been slow.
local DISC_SEEKS = 12
local DISC_SEEK_LIMIT = 3000
local discResults = {}
local discPending = nil
local discWorst = 0
TOOLS[#TOOLS + 1] = {
id = "toolDisc",
name = "Disc Test",
help = "Seek timing: how quickly the disc finds a frame.",
begin = function()
discResults = {}
discPending = nil
discWorst = 0
set("discRows", "<div class='row'>Button 1 runs the test.</div>")
end,
input = function(what)
if what == SWITCH_BUTTON1 and not discPending then
discResults = {}
discWorst = 0
discPending = { step = 0, started = 0 }
return true
end
return false
end,
update = function()
if not discPending then
return
end
if discPending.started == 0 then
-- Spread the targets over the whole disc rather than clustering, so a framefile's
-- segment changes are included in what is measured.
local total = discGetFrameCount()
discPending.step = discPending.step + 1
discPending.target = math.floor((discPending.step / DISC_SEEKS) * math.max(1, total - 1))
discPending.started = singeGetTicks()
discSearch(discPending.target)
return
end
if discGetFrame() == discPending.target or singeGetTicks() - discPending.started > DISC_SEEK_LIMIT then
local took = singeGetTicks() - discPending.started
discResults[#discResults + 1] = string.format("<div class='sub'>frame %d took %d ms</div>", discPending.target, took)
discWorst = math.max(discWorst, took)
discPending.started = 0
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)
discPending = nil
end
set("discRows", table.concat(discResults))
end
end,
finish = function()
discPending = nil
end,
}
-- ===== 8. Saved data =====
-- What each game has kept, and a way to throw it away. High scores are the usual reason: an
-- operator wants the board cleared without deleting the game. Each game saves into its own data
-- directory, so they are found by looking rather than by being told.
local saveDirs = {}
local saveSelected = 1
local function savedShow()
local rows = {}
if #saveDirs == 0 then
rows[#rows + 1] = "<div class='row'>No game has saved anything yet.</div>"
end
for i, entry in ipairs(saveDirs) do
local class = (i == saveSelected) and " class='selected'" or ""
rows[#rows + 1] = string.format("<div%s>%s &nbsp; <span class='sub'>%d bytes</span></div>", class, entry.name, entry.size)
end
set("savedRows", table.concat(rows))
end
TOOLS[#TOOLS + 1] = {
id = "toolSaved",
name = "Saved Data",
help = "What each game has kept, and clearing it.",
begin = function()
saveDirs = {}
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
-- without it, so both are kept. pcall because the root need not exist yet.
local root = singeGetSystemInfo().dataRoot
local ok, iter, state = pcall(lfs.dir, root)
if ok then
for name in iter, state do
if name ~= "." and name ~= ".." then
local file = root .. name .. "/save.json"
local attr = lfs.attributes(file)
if attr and attr.mode == "file" then
saveDirs[#saveDirs + 1] = { name = name, path = file, size = attr.size }
end
end
end
end
table.sort(saveDirs, function(a, b) return a.name < b.name end)
savedShow()
end,
input = function(what)
if #saveDirs == 0 then
return false
end
if what == SWITCH_UP then
saveSelected = (saveSelected == 1) and #saveDirs or (saveSelected - 1)
savedShow()
return true
elseif what == SWITCH_DOWN then
saveSelected = (saveSelected == #saveDirs) and 1 or (saveSelected + 1)
savedShow()
return true
elseif what == SWITCH_BUTTON3 then
-- Button 3 rather than Button 1: Button 1 is "choose" everywhere else, and this
-- throws away somebody's high scores.
os.remove(saveDirs[saveSelected].path)
table.remove(saveDirs, saveSelected)
saveSelected = math.max(1, math.min(saveSelected, #saveDirs))
savedShow()
return true
end
return false
end,
}
-- ===== 9. MIDI =====
-- 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.
-- 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.
local MIDI_TEST_CHANNEL = 1
local MIDI_TEST_NOTE = 60
local MIDI_TEST_VELOCITY = 100
local MIDI_SEEN_MAX = 8
local midiIn, midiOut = -1, -1
local midiSeen = {}
local midiDirty = false
local function midiShow()
local rows = {}
rows[#rows + 1] = string.format("<div class='row'>%d in, %d out</div>", midiInputCount(), midiOutputCount())
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))
end
for i = 0, midiInputCount() - 1 do
rows[#rows + 1] = string.format("<div class='sub'>in &nbsp;%d%s %s</div>", i, (i == midiIn) and " *" or "", midiInputName(i))
end
for _, m in ipairs(midiSeen) do
rows[#rows + 1] = string.format("<div class='sub'>received %s</div>", m)
end
set("midiRows", table.concat(rows))
end
function onMidiMessage(status, data1, data2, bytes)
midiSeen[#midiSeen + 1] = string.format("%02X %02X %02X", status, data1, data2)
while #midiSeen > MIDI_SEEN_MAX do
table.remove(midiSeen, 1)
end
midiDirty = true
end
TOOLS[#TOOLS + 1] = {
id = "toolMidi",
name = "MIDI Ports",
help = "List the ports, send a note, watch what arrives.",
begin = function()
midiSeen = {}
midiDirty = false
if midiOutputCount() > 0 and not midiIsOutputOpen() then
midiOut = midiOpenOutput(0) and 0 or -1
end
if midiInputCount() > 0 and not midiIsInputOpen() then
midiIn = midiOpenInput(0) and 0 or -1
end
midiShow()
end,
input = function(what)
if what == SWITCH_BUTTON1 and midiIsOutputOpen() then
midiNoteOn(MIDI_TEST_CHANNEL, MIDI_TEST_NOTE, MIDI_TEST_VELOCITY)
midiNoteOff(MIDI_TEST_CHANNEL, MIDI_TEST_NOTE)
return true
elseif what == SWITCH_BUTTON3 then
midiRescan()
midiShow()
return true
end
return false
end,
update = function()
if midiDirty then
midiDirty = false
midiShow()
end
end,
finish = function()
midiCloseInput()
midiCloseOutput()
midiIn, midiOut = -1, -1
end,
}
-- ===== 10. Frame statistics =====
TOOLS[#TOOLS + 1] = {
id = "toolStats",
name = "Frame Statistics",
help = "The developer's overlay: frame time, what is alive, what the disc is doing.",
begin = function()
set("statsState", statsIsEnabled() and "on" or "off")
end,
input = function(what)
if what == SWITCH_BUTTON1 then
statsEnable(not statsIsEnabled())
set("statsState", statsIsEnabled() and "on" or "off")
return true
end
return false
end,
}

View file

@ -124,8 +124,16 @@ body {
margin: 0; margin: 0;
} }
/* Audio delay calibration: an opaque page over everything, white for the frame of the flash. */ /* .hidden and .tool are one class each, and the pages that let the overlay through wear two, so a
#calibration { page taken out of view has to be said again here or its display wins. */
.tool.hidden,
.tool.clear.hidden {
display: none;
}
/* The service tools: one opaque page over everything, the same for all of them. The audio delay
tool alone turns the page white for the frame of its flash, so the colours are named twice. */
.tool {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
@ -136,35 +144,107 @@ body {
box-sizing: border-box; box-sizing: border-box;
background-color: #000000; background-color: #000000;
color: #ffffff; color: #ffffff;
overflow-y: auto;
} }
#calibration.flash { .tool.flash {
background-color: #ffffff; background-color: #ffffff;
color: #000000; color: #000000;
} }
#calibration h1 { .tool h1 {
font-size: 24dp; font-size: 24dp;
color: #ffffff; color: #ffffff;
margin-bottom: 20dp; margin-bottom: 20dp;
} }
#calibration.flash h1 { .tool.flash h1 {
color: #000000; color: #000000;
} }
#calibration p { .tool p {
margin-bottom: 20dp; margin-bottom: 20dp;
} }
#calibration p.value { .tool p.value {
margin-bottom: 4dp; margin-bottom: 4dp;
} }
#calibration p.value + p.keys { /* The key hints stand away from whatever the tool put above them, and close together among
themselves when a tool needs more than one line of them. */
.tool p.keys {
margin-top: 20dp; margin-top: 20dp;
margin-bottom: 4dp;
color: #8fa2c4;
} }
#calibration p.keys { .tool p.keys + p.keys {
margin-top: 0;
}
/* The rows a tool fills in for itself: a heading row and an indented detail row under it. Text
with nowhere to break -- a long path -- wraps mid-word rather than running off the edge, but an
ordinary word is left whole. */
.tool .row {
margin-bottom: 2dp;
word-break: break-word;
}
.tool .sub {
margin-left: 20dp;
margin-bottom: 2dp;
font-size: 12dp;
color: #c8d2e6;
word-break: break-word;
}
/* A label beside its value. The two are columns rather than one run of text, so a value too long
for the line wraps under itself instead of under the label. */
.tool .pair {
display: flex;
margin-bottom: 2dp;
}
.tool .pair .label {
flex: 0 0 110dp;
color: #8fa2c4;
}
.tool .pair .detail {
flex: 1 1 0;
word-break: break-word;
}
/* The chosen line of a list the tool draws, in the game list's gold. */
.tool .selected {
color: #ffd35a;
}
/* 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
into a strip along the bottom, between the two lower targets, so nothing worth aiming at is
covered. */
.tool.clear {
background-color: transparent;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
}
.tool.clear h1 {
font-size: 16dp;
margin-bottom: 4dp; margin-bottom: 4dp;
} }
.tool.clear h1,
.tool.clear p {
background-color: #000000c0;
padding: 1dp 8dp;
margin-bottom: 4dp;
}
/* The help under the list of tools, away from the list itself. */
#toolHelp {
margin-top: 12dp;
}

View file

@ -88,8 +88,8 @@ a line of key hints along the bottom.
Up and down move the selection one game at a time and left and right move it Up and down move the selection one game at a time and left and right move it
a page at a time; both wrap from either end of the list to the other. Start a page at a time; both wrap from either end of the list to the other. Start
or any action button launches the selected game, and the key mapped to or any action button launches the selected game, and the key mapped to
`INPUT_SERVICE` (the `9` key by default) opens the audio delay calibration `INPUT_SERVICE` (the `9` key by default) opens the service tools (see
screen (see <<menucalibration,Calibrating from the menu>>). With a mouse, a <<servicetools,The Service Tools>>). With a mouse, a
click on a row selects that game and a second click on the same row, or a 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
@ -100,7 +100,79 @@ A GUI needs the GPU device the 3D scene uses, so on a machine without one
`guiNew` fails and the menu cannot start. `Singe/MenuClassic.singe` is the `guiNew` fails and the menu cannot start. `Singe/MenuClassic.singe` is the
previous menu, drawn with sprites and `fontPrint` straight into the overlay, 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`) kept for one release for exactly that machine: edit `Menu.sh` (or `Menu.bat`)
to name it in place of `Singe/Menu.singe`. to name it in place of `Singe/Menu.singe`. It has the audio delay screen and
nothing else; the rest of the service tools need the document.
[#servicetools]
==== The Service Tools
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
the list, and the service key again leaves the tools altogether. Everything
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
running, and nothing changes a game's own files.
Audio Delay::
The downstream audio delay, measured against a click and a flash. See
<<menucalibration,Calibrating from the menu>> for what it is doing and why it
works.
Input Test::
Every controller, mouse and gun the engine can see, live: the last switch the
game would have received and how many have arrived, each pad's name with its
six axes and the buttons held down, and each mouse's name and position. This
is the screen that answers "the button does nothing" -- either the switch
arrives and the game ignores it, or it never arrives at all.
System Information::
The engine's version, the operating system, the processor and memory, the
renderer and the 3D device, which video decoders are in play, the audio
formats built in, the SoundFont, the MIDI state, the window, canvas and
overlay sizes, and the data directory. A photograph of this page answers most
of a bug report.
Light Gun::
Targets in the centre and at each corner of the overlay, over whatever the
disc is showing. Button 1 marks where the gun actually pointed, keeping the
last twelve shots; start clears them. A gun that is square on the picture puts
its crosses on the targets, and one that is not shows which way it is out.
Sound Test::
Button 1 plays the click through the left speaker, then the right, then both,
which is the whole of a crossed-wires check. It also lists the disc's own
audio tracks, and left and right switch between them.
Display::
The picture's size, position and rotation, changed live with the arrows,
buttons 3 and 4, and coin 1, over a test pattern: a 16 by 12 grid to square
the picture up with, an eight step greyscale wedge to set brightness by, and
two frames one pixel apart at the very edge -- a monitor that overscans eats
the outer one first. Button 1 keeps the values for this machine, in the same
`machine.cfg` the audio delay uses, so they apply to every game rather than
being set once per game. Start puts everything back.
Disc Test::
Twelve seeks spread over the whole disc, each timed. A rip whose keyframes are
far apart seeks slowly, and the only symptom in a game is that everything
feels heavy; this measures it directly and reports the worst of the twelve.
A seek that has not landed in three seconds is counted as that.
Saved Data::
What each game has kept, found by looking through the data root rather than by
being told, with the size of each. Button 3 -- not button 1, which is "choose"
everywhere else -- throws away the selected game's save. Clearing a high score
table without deleting the game is the usual reason.
MIDI Ports::
The input and output ports this machine has, which of them Singe has opened,
and the last few messages that arrived. Button 1 sends a note, button 3 looks
for ports again after something has been plugged in. Nothing is opened until
this tool asks, so a cabinet that never touches MIDI never pays for it.
Frame Statistics::
Turns the developer's overlay on and off. It stays on into the game, in the
top left corner. See <<statsenable,statsEnable>>.
[[controls]] [[controls]]
=== Customizing the Controls === Customizing the Controls
@ -334,12 +406,18 @@ names one, and `deterministic = false` leaves the option off.
. The built in default. . The built in default.
. The settings file. . The settings file.
. The game's `games.dat` entry, for the handful of settings it carries (see <<gamesdat,games.dat>>). . The game's `games.dat` entry, for the handful of settings it carries (see <<gamesdat,games.dat>>).
. `machine.cfg`, for the audio delay and the picture geometry the service tools
save (see <<servicetools,The Service Tools>>).
. The command line, which always wins. . The command line, which always wins.
A settings file is a set of defaults, in other words, not something you typed: A settings file is a set of defaults, in other words, not something you typed:
if a `games.dat` entry gives a game a resolution or a Sinden border, that entry if a `games.dat` entry gives a game a resolution or a Sinden border, that entry
still describes the game better than a machine-wide file does. Anything actually still describes the game better than a machine-wide file does. Anything actually
typed on the command line beats both. typed on the command line beats both. `machine.cfg` sits just under the command
line because somebody adjusted the picture while looking at this monitor, which
is better evidence than any file written elsewhere; the audio delay is the
exception to the ordering entirely, being added to the game's delay rather than
replacing it.
Options that name the game (`--framefile`, `--disc`, `--entry`), name a Options that name the game (`--framefile`, `--disc`, `--entry`), name a
directory (`--gamedir`, `--datadir`), or have to act before the file could be directory (`--gamedir`, `--datadir`), or have to act before the file could be
@ -871,10 +949,11 @@ Singe/ Support files extracted by the engine
Menu.singe The bundled game menu Menu.singe The bundled game menu
Menu.rml The menu's RmlUi document Menu.rml The menu's RmlUi document
menu.rcss The menu's style sheet, on top of gui.rcss menu.rcss The menu's style sheet, on top of gui.rcss
Tools.singe The menu's service tools (see The Service Tools)
MenuClassic.singe The previous, overlay-drawn menu (see The Bundled Menu) MenuClassic.singe The previous, overlay-drawn menu (see The Bundled Menu)
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 calibration click.wav Used by the menu's audio delay and sound tests
Manual.pdf This manual Manual.pdf This manual
ActionMax/ One game ActionMax/ One game
games.dat Menu entries for the games in this directory games.dat Menu entries for the games in this directory
@ -883,6 +962,7 @@ ActionMax/ One game
sprite_*.png, sound_*.wav, font_*.ttf sprite_*.png, sound_*.wav, font_*.ttf
DLe.game A game packed into one file (see Single-File Games) DLe.game A game packed into one file (see Single-File Games)
data/ data/
machine.cfg This machine's audio delay and picture geometry
ActionMax/ Indexes, trace.txt, screenshots, saves for that game ActionMax/ Indexes, trace.txt, screenshots, saves for that game
---- ----
@ -3107,7 +3187,9 @@ available to `controls.cfg` and to `Framework.singe` alike:
| `SINGE_TRIGGER_THRESHOLD` | The analogue trigger threshold in those same raw units; `0` when the triggers are on `SINGE_DEAD_ZONE`, which is the default. | `SINGE_TRIGGER_THRESHOLD` | The analogue trigger threshold in those same raw units; `0` when the triggers are on `SINGE_DEAD_ZONE`, which is the default.
| `SINGE_LEGACY_SPRITE_ARGS` | True when the game asked for the 2.10 sprite argument order. | `SINGE_LEGACY_SPRITE_ARGS` | True when the game asked for the 2.10 sprite argument order.
| `SINGE_DISC` | True when the game has a laserdisc; false when the canvas is the world. | `SINGE_DISC` | True when the game has a laserdisc; false when the canvas is the world.
| `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE` | Layout of the controller and mouse input codes; `Framework.singe` builds the `GAMEPAD_N` and `MOUSE_N` tables from them. | `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE`, `SINGE_CONTROLLER_AXES`, `SINGE_CONTROLLER_BUTTONS` | Layout of the controller and mouse input codes, and how many axes and buttons a pad has; `Framework.singe` builds the `GAMEPAD_N` and `MOUSE_N` tables from them, and a service screen walks a pad with them.
| `SINGE_SCALE_MIN`, `SINGE_SCALE_MAX`, `SINGE_SHIFT_MIN`, `SINGE_SHIFT_MAX`, `SINGE_ROTATE_STEP` | What `vldpSetScale`, `vldpSetShift` and `vldpSetRotate` will take. Clamp to these rather than to your own copies: a shift out of range ends the script.
|=== |===
`Framework.singe` adds the `SCANCODE` and `MODIFIER` tables (SDL's key and `Framework.singe` adds the `SCANCODE` and `MODIFIER` tables (SDL's key and
@ -3172,16 +3254,17 @@ of audio arriving early.
==== Calibrating from the menu ==== Calibrating from the menu
The bundled menu has a calibration screen for that downstream delay. Press the The bundled menu has a calibration screen for that downstream delay. Press the
key mapped to `INPUT_SERVICE` (the `9` key by default) on the game list. The key mapped to `INPUT_SERVICE` (the `9` key by default) on the game list and
screen is a page of the menu's document: it covers the menu in black, shows choose Audio Delay from the service tools (see <<servicetools,The Service
the delay being tried and the device queue the engine measured, plays a click Tools>>). The screen is a page of the menu's document: it covers the menu in
once a second and turns white for one frame at each flash. Adjust with left black, shows the delay being tried and the device queue the engine measured,
and right (10 ms) and up and down (1 ms) until the flash and the click happen plays a click once a second and turns white for one frame at each flash.
together, then press button 1 to save. Start resets to zero and button 2 (or Adjust with left and right (10 ms) and up and down (1 ms) until the flash and
the service key again) cancels. The value is stored in `audio.cfg` in the the click happen together, then press button 1 to save. Start resets to zero
data root and applied by the engine to every game on that machine, whether and button 2 (or the service key again) cancels. The value is stored in
launched from the menu or from the command line; it is added to any per-game `machine.cfg` in the data root and applied by the engine to every game on that
`AUDIO_DELAY`. machine, whether launched from the menu or from the command line; it is added
to any per-game `AUDIO_DELAY`.
Recalibrate after changing speakers, headphones, or displays. Recalibrate after changing speakers, headphones, or displays.
The screen works because the click and the disc audio share one mixer device The screen works because the click and the disc audio share one mixer device
@ -5312,7 +5395,7 @@ for track = 0, discGetAudioTracks() - 1 do
end end
---- ----
[#discgetframe] [#discgetsubtitlelanguage]
==== discGetSubtitleLanguage ==== discGetSubtitleLanguage
[source,text] [source,text]
@ -5331,6 +5414,7 @@ The language a subtitle track inside the disc's own container is labelled with,
*Since:* 3.00. *Since:* 3.00.
*See also:* <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtloadtrack,srtLoadTrack>> *See also:* <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtloadtrack,srtLoadTrack>>
[#discgetsubtitletracks]
==== discGetSubtitleTracks ==== discGetSubtitleTracks
[source,text] [source,text]
@ -5360,6 +5444,7 @@ for track = 0, discGetSubtitleTracks() - 1 do
end end
---- ----
[#discgetframe]
==== discGetFrame ==== discGetFrame
[source,text] [source,text]
@ -5389,6 +5474,31 @@ function onOverlayUpdate()
end end
---- ----
[#discgetframecount]
==== discGetFrameCount
[source,text]
----
frames = discGetFrameCount()
----
The number of frames on the disc, which is one past the highest frame
<<discsearch,discSearch>> will find. A framefile answers for the whole disc
rather than for the segment playing, so the count does not change as segments
come and go. Without a disc it answers `0`.
*Returns:* integer frame count, `0` without a disc.
*Since:* 3.00.
*See also:* <<discgetframe,discGetFrame>>, <<discsearch,discSearch>>
.Example
[source,lua]
----
-- Seek to the middle of the disc, wherever that is.
discSearch(math.floor(discGetFrameCount() / 2))
----
[#discgetheight] [#discgetheight]
==== discGetHeight ==== discGetHeight
@ -13837,7 +13947,7 @@ end
milliseconds = singeGetAudioCalibration() milliseconds = singeGetAudioCalibration()
---- ----
Returns the per-machine audio delay in effect, in milliseconds: the value the menu's calibration screen saved to `audio.cfg` in the data root, or one set since by `singeSetAudioCalibration`. Zero when the machine has never been calibrated. It applies to every game on the machine, on top of the per-game delay from `singeGetAudioDelay`. Returns the per-machine audio delay in effect, in milliseconds: the value the menu's calibration screen saved to `machine.cfg` in the data root, or one set since by `singeSetAudioCalibration`. Zero when the machine has never been calibrated. It applies to every game on the machine, on top of the per-game delay from `singeGetAudioDelay`.
*Returns:* integer milliseconds. *Returns:* integer milliseconds.
@ -14020,6 +14130,49 @@ DIR = singeGetScriptPath():match("(.*[/\\])") or "./"
titleSprite = spriteLoad(DIR .. "images/title.png") titleSprite = spriteLoad(DIR .. "images/title.png")
---- ----
[#singegetsysteminfo]
==== singeGetSystemInfo
[source,text]
----
info = singeGetSystemInfo()
----
Everything the trace header knows about this machine and this run, in one
table. It is meant for a service screen and for bug reports: the answer to
"what was it running on" without asking the player to find a log.
*Returns:* a table with these fields, all strings except the four sizes:
* `version` -- the engine version, as `SINGE_VERSION_STRING`.
* `os` -- the operating system name and release, and the architecture.
* `cpu` -- the processor, its core count and the memory in megabytes.
* `renderer` -- the name of SDL's 2D renderer in use.
* `gpu` -- the SDL_GPU driver behind the 3D scene, or a note that 3D is
unavailable.
* `decoder` -- which hardware video decoders the build will try, in words.
* `audio` -- the audio formats compiled in, comma separated.
* `soundFont` -- the SoundFont MIDI playback is using, or why there is none.
* `midi` -- the MIDI ports open, or a note that none have been asked for.
* `dataPath` -- this game's data directory.
* `dataRoot` -- the data directory the engine was given, one above `dataPath`,
which every game on the machine writes under.
* `windowWidth`, `windowHeight` -- integers, the window in pixels.
* `canvasWidth`, `canvasHeight` -- integers, the canvas in pixels.
*Since:* 3.00.
*See also:* <<singegetdatapath,singeGetDataPath>>, <<servicetools,The Service Tools>>
.Example
[source,lua]
----
-- A one-key diagnostic screen.
local info = singeGetSystemInfo()
fontPrint(10, 10, "Singe " .. info.version .. " on " .. info.os)
fontPrint(10, 30, info.cpu)
fontPrint(10, 50, info.renderer .. " / " .. info.gpu)
----
[#singegetticks] [#singegetticks]
==== singeGetTicks ==== singeGetTicks
@ -14124,6 +14277,42 @@ function onKeyPressed(keysym, scancode)
end end
---- ----
[#singesavegeometry]
==== singeSaveGeometry
[source,text]
----
singeSaveGeometry()
----
Keeps the picture's current scale, shift and rotation for this machine. They
go into `machine.cfg` in the data root, beside the audio delay, and the engine
applies them to every game it starts afterwards, from the menu or from the
command line, and across `singeReload`. An option actually typed on the command
line wins: `--scalefactor` overrides the saved scale, `--shiftx` or `--shifty`
the saved shift, `--rotate` the saved rotation. The file is written whole, so
the audio delay is kept as it stands.
This is a cabinet setting, not a game setting: it is how a monitor is squared
up once rather than once per game. The Display tool in the bundled menu is
exactly this call behind button 1.
*Returns:* nothing.
*Since:* 3.00.
*See also:* <<vldpsetscale,vldpSetScale>>, <<vldpsetshift,vldpSetShift>>, <<vldpsetrotate,vldpSetRotate>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
.Example
[source,lua]
----
-- The operator is happy with the picture: keep it for the cabinet.
function onInputPressed(input)
if input == SWITCH_BUTTON1 then
singeSaveGeometry()
end
end
----
[#singescreenshot] [#singescreenshot]
==== singeScreenshot ==== singeScreenshot
@ -14160,7 +14349,7 @@ end
singeSetAudioCalibration(milliseconds) singeSetAudioCalibration(milliseconds)
---- ----
Sets the per-machine audio delay, applies it at once, and writes it to `audio.cfg` in the data root so every game on the machine picks it up. The menu's calibration screen calls it; a game with its own service menu may too. The value is positive when the audio is heard later than the device reports; the engine delays video presentation by that much. Values outside `-1000` to `1000` abort the script. If `audio.cfg` cannot be written the value still applies for this run and a message goes to the console. Sets the per-machine audio delay, applies it at once, and writes it to `machine.cfg` in the data root so every game on the machine picks it up. The menu's calibration screen calls it; a game with its own service menu may too. The value is positive when the audio is heard later than the device reports; the engine delays video presentation by that much. Values outside `-1000` to `1000` abort the script. If `machine.cfg` cannot be written the value still applies for this run and a message goes to the console.
*Parameters:* *Parameters:*
@ -16098,6 +16287,7 @@ so it never lands in a screenshot.
Leave it off in a shipped game. Nothing stops you putting it behind a key in Leave it off in a shipped game. Nothing stops you putting it behind a key in
your own service menu. your own service menu.
[#statsenable]
==== statsEnable ==== statsEnable
[source,text] [source,text]
@ -17934,6 +18124,32 @@ else
end end
---- ----
[#vldpgetshift]
==== vldpGetShift
[source,text]
----
x, y = vldpGetShift()
----
The picture's offset within the window as percentages, the `--shiftx` and
`--shifty` options or whatever <<vldpsetshift,vldpSetShift>> last applied, and
`0, 0` when neither has been given. At the extreme a shift fills exactly the
room `--scalefactor` left, so `100` with the scale at `100` moves nothing.
*Returns:* two integers, each `-100` to `100`.
*Since:* 3.00.
*See also:* <<vldpsetshift,vldpSetShift>>, <<vldpgetscale,vldpGetScale>>
.Example
[source,lua]
----
-- Report where the operator has put the picture.
local x, y = vldpGetShift()
fontPrint(10, 10, string.format("shift %d, %d", x, y))
----
[#vldpgetwidth] [#vldpgetwidth]
==== vldpGetWidth ==== vldpGetWidth
@ -18164,6 +18380,44 @@ function onInputPressed(input)
end end
---- ----
[#vldpsetshift]
==== vldpSetShift
[source,text]
----
vldpSetShift(x, y)
----
Moves the picture within the window, the running equivalent of `--shiftx` and
`--shifty`, and rebuilds the video rectangle so the disc, the overlay, the
GUIs, the 3D scene, the particles, any Sinden border and the mouse mapping all
follow it. The values are percentages of the room `--scalefactor` left, so
with the scale at `100` there is nowhere to move to. A value outside `-100` to
`100` ends the script, unlike <<vldpsetscale,vldpSetScale>>, which answers
`false`.
*Parameters:*
* `x` -- integer, `-100` to `100`.
* `y` -- integer, `-100` to `100`.
*Returns:* nothing.
*Since:* 3.00.
*See also:* <<vldpgetshift,vldpGetShift>>, <<vldpsetscale,vldpSetScale>>, <<singesavegeometry,singeSaveGeometry>>
.Example
[source,lua]
----
-- Nudge the picture left while the operator holds the key.
function onInputPressed(input)
if input == SWITCH_LEFT then
local x, y = vldpGetShift()
vldpSetShift(math.max(-100, x - 1), y)
end
end
----
[#vldpsetverbose] [#vldpsetverbose]
==== vldpSetVerbose ==== vldpSetVerbose

View file

@ -33,6 +33,7 @@
#include "generated/controls_cfg.h" #include "generated/controls_cfg.h"
#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/MenuClassic_singe.h" #include "generated/MenuClassic_singe.h"
#include "generated/Menu_rml.h" #include "generated/Menu_rml.h"
#include "generated/menu_rcss.h" #include "generated/menu_rcss.h"

View file

@ -246,6 +246,17 @@ int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle) {
} }
int64_t frameFileGetFrameCount(int32_t frameFileHandle) {
FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileGetFrameCount");
if (f->count <= 0) {
return 0;
}
return f->files[f->count - 1].frame + videoGetFrameCount(_openSegment(f, f->count - 1));
}
int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer *renderer, bool showCalculated) { int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer *renderer, bool showCalculated) {
int32_t count = 0; int32_t count = 0;
int64_t frame = 0; int64_t frame = 0;

View file

@ -32,6 +32,10 @@
char *frameFileAudioName(const char *filename, const char *suffix); char *frameFileAudioName(const char *filename, const char *suffix);
int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle); int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle);
// Frames in the whole disc: where the last segment starts, plus how long it is. Opening that
// segment is the price of the answer, which is why it is asked for rather than kept.
int64_t frameFileGetFrameCount(int32_t frameFileHandle);
int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer *renderer, bool showCalculated); int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer *renderer, bool showCalculated);
void frameFileQuit(void); void frameFileQuit(void);
void frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame); void frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame);

View file

@ -276,9 +276,6 @@ static char *_cloneString(const char *string);
#ifndef _WIN32 #ifndef _WIN32
static void _crashHandler(int signalNumber); static void _crashHandler(int signalNumber);
#endif #endif
static char *_describeAudioDecoders(void);
static char *_describeCpu(void);
static char *_describeOs(void);
static bool _extractFile(const char *filename, const uint8_t *data, size_t length); static bool _extractFile(const char *filename, const uint8_t *data, size_t length);
static char *_findVideoFile(const char *baseName); static char *_findVideoFile(const char *baseName);
static void _launcher(const char *exeName, ConfigT *conf); static void _launcher(const char *exeName, ConfigT *conf);
@ -381,7 +378,8 @@ static void _applyOptions(const char *exeName, ConfigT *conf, int32_t argc, cons
// Overscan Zoom // Overscan Zoom
case 'b': case 'b':
target = &conf->scaleFactor; conf->given |= GIVEN_SCALE;
target = &conf->scaleFactor;
break; break;
// Show Calculated Frame File Values // Show Calculated Frame File Values
@ -510,7 +508,8 @@ static void _applyOptions(const char *exeName, ConfigT *conf, int32_t argc, cons
// Presentation Rotation // Presentation Rotation
case 'r': case 'r':
target = &conf->rotate; conf->given |= GIVEN_ROTATE;
target = &conf->rotate;
break; break;
// Where The Sinden Border Sits // Where The Sinden Border Sits
@ -550,7 +549,8 @@ static void _applyOptions(const char *exeName, ConfigT *conf, int32_t argc, cons
// Horizontal Shift // Horizontal Shift
case 'X': case 'X':
target = &conf->shiftX; conf->given |= GIVEN_SHIFT;
target = &conf->shiftX;
break; break;
// X Resolution // X Resolution
@ -562,7 +562,8 @@ static void _applyOptions(const char *exeName, ConfigT *conf, int32_t argc, cons
// Vertical Shift // Vertical Shift
case 'Y': case 'Y':
target = &conf->shiftY; conf->given |= GIVEN_SHIFT;
target = &conf->shiftY;
break; break;
// Y Resolution // Y Resolution
@ -949,7 +950,7 @@ static void _crashHandler(int signalNumber) {
// Which audio formats this build can decode, for a bug report and so a user whose music is silent // Which audio formats this build can decode, for a bug report and so a user whose music is silent
// can see at a glance whether its format was ever compiled in. The mixer keeps the list. // can see at a glance whether its format was ever compiled in. The mixer keeps the list.
static char *_describeAudioDecoders(void) { char *mainDescribeAudioDecoders(void) {
char *list = strdup(""); char *list = strdup("");
char *grown = NULL; char *grown = NULL;
int32_t count = MIX_GetNumAudioDecoders(); int32_t count = MIX_GetNumAudioDecoders();
@ -967,7 +968,7 @@ static char *_describeAudioDecoders(void) {
// The processor, for a bug report. Every platform keeps the name somewhere different, and none of // The processor, for a bug report. Every platform keeps the name somewhere different, and none of
// them is worth failing over: the core count is always there as a fallback. // them is worth failing over: the core count is always there as a fallback.
static char *_describeCpu(void) { char *mainDescribeCpu(void) {
#ifdef _WIN32 #ifdef _WIN32
char name[128]; char name[128];
DWORD bytes = sizeof(name); DWORD bytes = sizeof(name);
@ -1019,7 +1020,7 @@ static char *_describeCpu(void) {
// The operating system and its version, for a bug report. // The operating system and its version, for a bug report.
static char *_describeOs(void) { char *mainDescribeOs(void) {
#ifdef _WIN32 #ifdef _WIN32
char release[64]; char release[64];
DWORD bytes = sizeof(release); DWORD bytes = sizeof(release);
@ -1749,9 +1750,9 @@ static void _stopSDL(void) {
// started it, the machine, and what the engine picked to run on. main.c's own trace lines wait // started it, the machine, and what the engine picked to run on. main.c's own trace lines wait
// behind it so the block is always first and can be pasted whole. // behind it so the block is always first and can be pasted whole.
static void _traceHeader(const ConfigT *conf, SDL_Renderer *renderer, SDL_GPUDevice *device) { static void _traceHeader(const ConfigT *conf, SDL_Renderer *renderer, SDL_GPUDevice *device) {
char *os = _describeOs(); char *os = mainDescribeOs();
char *cpu = _describeCpu(); char *cpu = mainDescribeCpu();
char *audio = _describeAudioDecoders(); char *audio = mainDescribeAudioDecoders();
int32_t built = SDL_VERSION; int32_t built = SDL_VERSION;
int32_t linked = SDL_GetVersion(); int32_t linked = SDL_GetVersion();
@ -1789,6 +1790,7 @@ static void _unpackData(const char *exePath, bool absolute) {
{ "controls.cfg.example", controls_cfg, controls_cfg_len }, { "controls.cfg.example", controls_cfg, controls_cfg_len },
{ "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 },
{ "MenuClassic.singe", MenuClassic_singe, MenuClassic_singe_len }, { "MenuClassic.singe", MenuClassic_singe, MenuClassic_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 },

View file

@ -60,6 +60,11 @@ char *createDataDir(const char *dataDirBase, const char *filename);
char *createDataDirFor(const ConfigT *conf); char *createDataDirFor(const ConfigT *conf);
void destroyConf(ConfigT **confPointer); void destroyConf(ConfigT **confPointer);
bool isFrameFileName(const char *filename); bool isFrameFileName(const char *filename);
// What the trace header says about this machine, for the service tools to show as well. Each one
// allocates; free() what comes back.
char *mainDescribeAudioDecoders(void);
char *mainDescribeCpu(void);
char *mainDescribeOs(void);
bool parseSindenString(const char *sindenString, ConfigT *conf); bool parseSindenString(const char *sindenString, ConfigT *conf);
void queueScript(const ConfigT *conf); void queueScript(const ConfigT *conf);
char *resolveDataDir(const ConfigT *conf); char *resolveDataDir(const ConfigT *conf);

View file

@ -61,6 +61,7 @@ LSEC_API int luaopen_ssl_config(lua_State *L);
#include "collide.h" #include "collide.h"
#include "decode.h" #include "decode.h"
#include "main.h" #include "main.h"
#include "midi.h"
#include "midiIo.h" #include "midiIo.h"
#include "util.h" #include "util.h"
#include "frameFile.h" #include "frameFile.h"
@ -136,7 +137,6 @@ SDL_COMPILE_TIME_ASSERT(codeGamepadBase, CODE_GAMEPAD_BASE >= SDL_SCANCODE_RESER
#define ANIMATION_MIN_DELAY_MS 10 // GIFs often carry a zero delay #define ANIMATION_MIN_DELAY_MS 10 // GIFs often carry a zero delay
#define SCREENSHOT_MAX 10000 #define SCREENSHOT_MAX 10000
#define VIDEO_SCALE_THROTTLE_MS 15 // vldpSetScale accepts one change this often, as Hypseus does #define VIDEO_SCALE_THROTTLE_MS 15 // vldpSetScale accepts one change this often, as Hypseus does
#define VIDEO_SHIFT_MAX 100 // --shiftx and --shifty at their extreme fill the room --scalefactor leaves
#define BEZEL_DIRECTORY "bezels" // Where --bezel looks for its artwork unless --bezeldir names another #define BEZEL_DIRECTORY "bezels" // Where --bezel looks for its artwork unless --bezeldir names another
#define BEZEL_SIDECAR_EXTENSION ".cfg" // The cutout beside the artwork, named after it #define BEZEL_SIDECAR_EXTENSION ".cfg" // The cutout beside the artwork, named after it
#define BEZEL_CUTOUT_TABLE "CUTOUT" // The one table the sidecar sets #define BEZEL_CUTOUT_TABLE "CUTOUT" // The one table the sidecar sets
@ -215,7 +215,7 @@ SDL_COMPILE_TIME_ASSERT(codeGamepadBase, CODE_GAMEPAD_BASE >= SDL_SCANCODE_RESER
#define SOUND_DIRECTION_EPSILON 0.0001f // Closer than this to the listener has no direction #define SOUND_DIRECTION_EPSILON 0.0001f // Closer than this to the listener has no direction
#define HEIGHTMAP_MAX 1025 // Pixels per side of a heightmap image #define HEIGHTMAP_MAX 1025 // Pixels per side of a heightmap image
#define WATCH_INTERVAL_MS 1000 // How often --reload checks the script files #define WATCH_INTERVAL_MS 1000 // How often --reload checks the script files
#define AUDIO_CALIBRATION_FILE "audio.cfg" // Per-machine audio delay, in the data root #define MACHINE_FILE "machine.cfg" // Per-machine audio delay and picture geometry, in the data root
#define CONTROLS_FILE "controls.cfg" // Input mappings, built in and overridden per game #define CONTROLS_FILE "controls.cfg" // Input mappings, built in and overridden per game
#define SETTINGS_FILE "settings.cfg" // User options, found the same four ways controls.cfg is #define SETTINGS_FILE "settings.cfg" // User options, found the same four ways controls.cfg is
#define SETTINGS_PLACES 4 // The four places both of those are looked for #define SETTINGS_PLACES 4 // The four places both of those are looked for
@ -781,12 +781,12 @@ static int32_t _lfsDirIterator(lua_State *L);
static void _lfsFillAttributes(lua_State *L, bool directory, int64_t size, int64_t modified); static void _lfsFillAttributes(lua_State *L, bool directory, int64_t size, int64_t modified);
static int32_t _lfsMkdir(lua_State *L); static int32_t _lfsMkdir(lua_State *L);
static int32_t _lfsRmdir(lua_State *L); static int32_t _lfsRmdir(lua_State *L);
static int32_t _loadAudioCalibration(void);
static void _loadControlMappings(void); static void _loadControlMappings(void);
static void _loadControlsFile(const char *path); static void _loadControlsFile(const char *path);
static SDL_Surface *_loadEmbeddedPng(const uint8_t *data, size_t length); static SDL_Surface *_loadEmbeddedPng(const uint8_t *data, size_t length);
static SDL_Texture *_loadEmbeddedTexture(const uint8_t *data, size_t length, SDL_Surface **surface); static SDL_Texture *_loadEmbeddedTexture(const uint8_t *data, size_t length, SDL_Surface **surface);
static void _loadGamepadDatabase(void); static void _loadGamepadDatabase(void);
static void _loadMachineSettings(void);
static void _logicalRect(SDL_FRect *rect); static void _logicalRect(SDL_FRect *rect);
static int32_t _luaCallOriginal(lua_State *L); static int32_t _luaCallOriginal(lua_State *L);
static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))) __attribute__((noreturn)); static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))) __attribute__((noreturn));
@ -801,6 +801,7 @@ static int32_t _luaPanic(lua_State *L);
static int32_t _luaSearcher(lua_State *L); static int32_t _luaSearcher(lua_State *L);
static void _luaTrace(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))); static void _luaTrace(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
static int32_t _luaTraceback(lua_State *L); static int32_t _luaTraceback(lua_State *L);
static char *_machineFilePath(void);
static void _mapPointer(float px, float py, int32_t *x, int32_t *y); static void _mapPointer(float px, float py, int32_t *x, int32_t *y);
static void _mapPointerDelta(float dx, float dy, int32_t *xr, int32_t *yr); static void _mapPointerDelta(float dx, float dy, int32_t *xr, int32_t *yr);
static void _mapUnknownJoysticks(void); static void _mapUnknownJoysticks(void);
@ -809,6 +810,7 @@ static float _mixerGain(int32_t volume, int32_t maximum);
static int32_t _mouseCode(int32_t device, int32_t button); static int32_t _mouseCode(int32_t device, int32_t button);
static void _musicDestroy(MusicT *music); static void _musicDestroy(MusicT *music);
static bool _saveCjson(lua_State *L, const char *name); static bool _saveCjson(lua_State *L, const char *name);
static void _saveMachineSettings(int32_t milliseconds);
static void _saveCopy(lua_State *L, int32_t index, const char *method, int32_t depth); static void _saveCopy(lua_State *L, int32_t index, const char *method, int32_t depth);
static void _saveNormalise(lua_State *L, int32_t depth); static void _saveNormalise(lua_State *L, int32_t depth);
static void _saveCheckValue(lua_State *L, const char *method, int32_t index); static void _saveCheckValue(lua_State *L, const char *method, int32_t index);
@ -852,7 +854,6 @@ static void _resetScriptState(void);
static void _rmlEscape(char *out, size_t size, const char *text); static void _rmlEscape(char *out, size_t size, const char *text);
static void _runScript(bool fatal); static void _runScript(bool fatal);
static bool _sameLoosePath(const char *a, const char *b); static bool _sameLoosePath(const char *a, const char *b);
static void _saveAudioCalibration(int32_t milliseconds);
static SDL_Texture *_sceneVideoSource(int32_t player); static SDL_Texture *_sceneVideoSource(int32_t player);
static void _scorePanelDigits(char *text, size_t size, int32_t value, int32_t digits, bool blank); static void _scorePanelDigits(char *text, size_t size, int32_t value, int32_t digits, bool blank);
static bool _scorePanelEnable(bool enabled); static bool _scorePanelEnable(bool enabled);
@ -968,6 +969,7 @@ static int32_t apiDiscGetAudioTracks(lua_State *L);
static int32_t apiDiscGetSubtitleLanguage(lua_State *L); static int32_t apiDiscGetSubtitleLanguage(lua_State *L);
static int32_t apiDiscGetSubtitleTracks(lua_State *L); static int32_t apiDiscGetSubtitleTracks(lua_State *L);
static int32_t apiDiscGetFrame(lua_State *L); static int32_t apiDiscGetFrame(lua_State *L);
static int32_t apiDiscGetFrameCount(lua_State *L);
static int32_t apiDiscGetHeight(lua_State *L); static int32_t apiDiscGetHeight(lua_State *L);
static int32_t apiDiscGetLanguage(lua_State *L); static int32_t apiDiscGetLanguage(lua_State *L);
static int32_t apiDiscGetState(lua_State *L); static int32_t apiDiscGetState(lua_State *L);
@ -1266,6 +1268,8 @@ static int32_t apiSingeGetAudioCalibration(lua_State *L);
static int32_t apiSingeGetAudioDelay(lua_State *L); static int32_t apiSingeGetAudioDelay(lua_State *L);
static int32_t apiSingeGetAudioLatency(lua_State *L); static int32_t apiSingeGetAudioLatency(lua_State *L);
static int32_t apiSingeGetDataPath(lua_State *L); static int32_t apiSingeGetDataPath(lua_State *L);
static int32_t apiSingeGetSystemInfo(lua_State *L);
static int32_t apiSingeSaveGeometry(lua_State *L);
static int32_t apiSingeGetHeight(lua_State *L); static int32_t apiSingeGetHeight(lua_State *L);
static int32_t apiSingeGetPauseFlag(lua_State *L); static int32_t apiSingeGetPauseFlag(lua_State *L);
static int32_t apiSingeGetScriptPath(lua_State *L); static int32_t apiSingeGetScriptPath(lua_State *L);
@ -1402,7 +1406,9 @@ static int32_t apiVldpSetBlend(lua_State *L);
static int32_t apiVldpSetLuma(lua_State *L); static int32_t apiVldpSetLuma(lua_State *L);
static int32_t apiVldpSetMonochrome(lua_State *L); static int32_t apiVldpSetMonochrome(lua_State *L);
static int32_t apiVldpSetRotate(lua_State *L); static int32_t apiVldpSetRotate(lua_State *L);
static int32_t apiVldpGetShift(lua_State *L);
static int32_t apiVldpSetScale(lua_State *L); static int32_t apiVldpSetScale(lua_State *L);
static int32_t apiVldpSetShift(lua_State *L);
static int32_t apiVldpSetVerbose(lua_State *L); static int32_t apiVldpSetVerbose(lua_State *L);
@ -2354,8 +2360,8 @@ static void _computeVideoRect(void) {
scaledH = rect.h * (float)_global.videoScale / (float)SCALE_FACTOR_MAX; scaledH = rect.h * (float)_global.videoScale / (float)SCALE_FACTOR_MAX;
roomX = (rect.w - scaledW) / 2.0f; roomX = (rect.w - scaledW) / 2.0f;
roomY = (rect.h - scaledH) / 2.0f; roomY = (rect.h - scaledH) / 2.0f;
rect.x += roomX + roomX * (float)_global.videoShiftX / (float)VIDEO_SHIFT_MAX; rect.x += roomX + roomX * (float)_global.videoShiftX / (float)SHIFT_MAX;
rect.y += roomY + roomY * (float)_global.videoShiftY / (float)VIDEO_SHIFT_MAX; rect.y += roomY + roomY * (float)_global.videoShiftY / (float)SHIFT_MAX;
rect.w = scaledW; rect.w = scaledW;
rect.h = scaledH; rect.h = scaledH;
@ -3776,28 +3782,6 @@ static int32_t _lfsRmdir(lua_State *L) {
} }
// The per-machine audio delay lives beside the data directories, since it is not a property of any game.
static int32_t _loadAudioCalibration(void) {
char *path = utilCreateString("%s%s", _global.conf->dataDirBase, AUDIO_CALIBRATION_FILE);
size_t bytes = 0;
char *data = utilReadFile(path, &bytes);
int32_t value = 0;
if (data) {
if (sscanf(data, "delay = %d", &value) != 1) {
value = 0;
}
free(data);
}
free(path);
if ((value < -VIDEO_AUDIO_DELAY_MAX) || (value > VIDEO_AUDIO_DELAY_MAX)) {
value = 0;
}
return value;
}
// The built-in controls.cfg, then every override in turn (the working directory, above and inside // The built-in controls.cfg, then every override in turn (the working directory, above and inside
// the data directory, beside the script), each place only once, in a throwaway Lua state that has // the data directory, beside the script), each place only once, in a throwaway Lua state that has
// the framework but not the API. Leaves the dead zone and the switch mappings behind. // the framework but not the API. Leaves the dead zone and the switch mappings behind.
@ -4000,6 +3984,50 @@ static void _loadGamepadDatabase(void) {
// The whole logical rectangle: the window, in the coordinates everything is drawn in. // The whole logical rectangle: the window, in the coordinates everything is drawn in.
// Everything the service tools keep for this machine, read in one pass: the audio delay and the
// picture geometry. A file missing a key leaves that setting alone, which is why each is looked
// for on its own rather than with one scan of the whole file.
//
// The geometry goes into the configuration rather than straight into the live values, because the
// live ones are set back to the configuration whenever a script starts or reloads; writing there
// makes "start from the command line" mean "start from the command line as this cabinet amends it".
// An option actually typed wins, which is what the GIVEN_ flags are for.
static void _loadMachineSettings(void) {
char *path = _machineFilePath();
size_t bytes = 0;
char *data = utilReadFile(path, &bytes);
const char *at = NULL;
int32_t delay = 0;
int32_t value = 0;
free(path);
if (data != NULL) {
at = strstr(data, "delay = ");
if ((at != NULL) && (sscanf(at, "delay = %d", &value) == 1) && (value >= -VIDEO_AUDIO_DELAY_MAX) && (value <= VIDEO_AUDIO_DELAY_MAX)) {
delay = value;
}
at = strstr(data, "scale = ");
if ((at != NULL) && !(_global.conf->given & GIVEN_SCALE) && (sscanf(at, "scale = %d", &value) == 1) && (value >= SCALE_FACTOR_MIN) && (value <= SCALE_FACTOR_MAX)) {
_global.conf->scaleFactor = value;
}
at = strstr(data, "shiftx = ");
if ((at != NULL) && !(_global.conf->given & GIVEN_SHIFT) && (sscanf(at, "shiftx = %d", &value) == 1) && (value >= SHIFT_MIN) && (value <= SHIFT_MAX)) {
_global.conf->shiftX = value;
}
at = strstr(data, "shifty = ");
if ((at != NULL) && !(_global.conf->given & GIVEN_SHIFT) && (sscanf(at, "shifty = %d", &value) == 1) && (value >= SHIFT_MIN) && (value <= SHIFT_MAX)) {
_global.conf->shiftY = value;
}
at = strstr(data, "rotate = ");
if ((at != NULL) && !(_global.conf->given & GIVEN_ROTATE) && (sscanf(at, "rotate = %d", &value) == 1) && ((value % ROTATE_STEP) == 0)) {
_global.conf->rotate = value;
}
free(data);
}
videoSetAudioCalibration(delay);
}
static void _logicalRect(SDL_FRect *rect) { static void _logicalRect(SDL_FRect *rect) {
rect->x = 0.0f; rect->x = 0.0f;
rect->y = 0.0f; rect->y = 0.0f;
@ -4302,6 +4330,13 @@ static int32_t _luaTraceback(lua_State *L) {
// A pointer position in presentation coordinates to the overlay coordinates the game works in: // A pointer position in presentation coordinates to the overlay coordinates the game works in:
// the rotation comes off first, then the position maps into whatever the shift, the scale factor // the rotation comes off first, then the position maps into whatever the shift, the scale factor
// and the Sinden border left of the picture. // and the Sinden border left of the picture.
// The machine file lives beside the data directories, since what it holds belongs to the cabinet
// rather than to any game.
static char *_machineFilePath(void) {
return utilCreateString("%s%s", _global.conf->dataDirBase, MACHINE_FILE);
}
static void _mapPointer(float px, float py, int32_t *x, int32_t *y) { static void _mapPointer(float px, float py, int32_t *x, int32_t *y) {
float vx = 0; float vx = 0;
float vy = 0; float vy = 0;
@ -4954,6 +4989,24 @@ static void _pushConstants(lua_State *L) {
lua_setglobal(L, "SINGE_MOUSE_STRIDE"); lua_setglobal(L, "SINGE_MOUSE_STRIDE");
lua_pushinteger(L, MAX_CONTROLLERS); lua_pushinteger(L, MAX_CONTROLLERS);
lua_setglobal(L, "SINGE_MAX_CONTROLLERS"); lua_setglobal(L, "SINGE_MAX_CONTROLLERS");
lua_pushinteger(L, CONTROLLER_AXIS_COUNT);
lua_setglobal(L, "SINGE_CONTROLLER_AXES");
lua_pushinteger(L, CONTROLLER_BUTTON_COUNT);
lua_setglobal(L, "SINGE_CONTROLLER_BUTTONS");
// What vldpSetScale, vldpSetShift and vldpSetRotate will take, so a service screen clamps to
// the engine's limits rather than to its own copy of them: a shift out of range ends the
// script, which makes a stale copy fatal rather than merely wrong.
lua_pushinteger(L, SCALE_FACTOR_MIN);
lua_setglobal(L, "SINGE_SCALE_MIN");
lua_pushinteger(L, SCALE_FACTOR_MAX);
lua_setglobal(L, "SINGE_SCALE_MAX");
lua_pushinteger(L, SHIFT_MIN);
lua_setglobal(L, "SINGE_SHIFT_MIN");
lua_pushinteger(L, SHIFT_MAX);
lua_setglobal(L, "SINGE_SHIFT_MAX");
lua_pushinteger(L, ROTATE_STEP);
lua_setglobal(L, "SINGE_ROTATE_STEP");
lua_pushinteger(L, MAX_MICE); lua_pushinteger(L, MAX_MICE);
lua_setglobal(L, "SINGE_MAX_MICE"); lua_setglobal(L, "SINGE_MAX_MICE");
@ -5114,6 +5167,7 @@ static void _registerApi(lua_State *L) {
lua_register(L, "discGetSubtitleLanguage", apiDiscGetSubtitleLanguage); // 3.00 lua_register(L, "discGetSubtitleLanguage", apiDiscGetSubtitleLanguage); // 3.00
lua_register(L, "discGetSubtitleTracks", apiDiscGetSubtitleTracks); // 3.00 lua_register(L, "discGetSubtitleTracks", apiDiscGetSubtitleTracks); // 3.00
lua_register(L, "discGetFrame", apiDiscGetFrame); // 1.xx lua_register(L, "discGetFrame", apiDiscGetFrame); // 1.xx
lua_register(L, "discGetFrameCount", apiDiscGetFrameCount); // 3.00
lua_register(L, "discGetHeight", apiDiscGetHeight); // 2.00 lua_register(L, "discGetHeight", apiDiscGetHeight); // 2.00
lua_register(L, "discGetLanguage", apiDiscGetLanguage); // 2.10 lua_register(L, "discGetLanguage", apiDiscGetLanguage); // 2.10
lua_register(L, "discGetState", apiDiscGetState); // 1.xx RDG lua_register(L, "discGetState", apiDiscGetState); // 1.xx RDG
@ -5419,12 +5473,14 @@ static void _registerApi(lua_State *L) {
lua_register(L, "singeGetAudioLatency", apiSingeGetAudioLatency); // 3.00 lua_register(L, "singeGetAudioLatency", apiSingeGetAudioLatency); // 3.00
lua_register(L, "singeGetDataPath", apiSingeGetDataPath); // 2.00 lua_register(L, "singeGetDataPath", apiSingeGetDataPath); // 2.00
lua_register(L, "singeGetHeight", apiSingeGetHeight); // 1.xx lua_register(L, "singeGetHeight", apiSingeGetHeight); // 1.xx
lua_register(L, "singeGetSystemInfo", apiSingeGetSystemInfo); // 3.00
lua_register(L, "singeGetPauseFlag", apiSingeGetPauseFlag); // 1.xx RDG lua_register(L, "singeGetPauseFlag", apiSingeGetPauseFlag); // 1.xx RDG
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, "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, "singeScreenshot", apiSingeScreenshot); // 1.xx lua_register(L, "singeScreenshot", apiSingeScreenshot); // 1.xx
lua_register(L, "singeSetAudioCalibration", apiSingeSetAudioCalibration); // 3.00 lua_register(L, "singeSetAudioCalibration", apiSingeSetAudioCalibration); // 3.00
lua_register(L, "singeSetAudioDelay", apiSingeSetAudioDelay); // 3.00 lua_register(L, "singeSetAudioDelay", apiSingeSetAudioDelay); // 3.00
@ -5554,6 +5610,7 @@ static void _registerApi(lua_State *L) {
lua_register(L, "vldpGetPixel", apiVldpGetPixel); // 1.xx lua_register(L, "vldpGetPixel", apiVldpGetPixel); // 1.xx
lua_register(L, "vldpGetRotate", apiVldpGetRotate); // Hypseus lua_register(L, "vldpGetRotate", apiVldpGetRotate); // Hypseus
lua_register(L, "vldpGetScale", apiVldpGetScale); // Hypseus lua_register(L, "vldpGetScale", apiVldpGetScale); // Hypseus
lua_register(L, "vldpGetShift", apiVldpGetShift); // 3.00
lua_register(L, "vldpGetWidth", apiDiscGetWidth); // 1.xx Same as discGetWidth. lua_register(L, "vldpGetWidth", apiDiscGetWidth); // 1.xx Same as discGetWidth.
lua_register(L, "vldpGetYUVPixel", apiVldpGetYUVPixel); // Hypseus lua_register(L, "vldpGetYUVPixel", apiVldpGetYUVPixel); // Hypseus
lua_register(L, "vldpResetFocus", apiVldpResetFocus); // Hypseus lua_register(L, "vldpResetFocus", apiVldpResetFocus); // Hypseus
@ -5562,6 +5619,7 @@ static void _registerApi(lua_State *L) {
lua_register(L, "vldpSetMonochrome", apiVldpSetMonochrome); // Hypseus lua_register(L, "vldpSetMonochrome", apiVldpSetMonochrome); // Hypseus
lua_register(L, "vldpSetRotate", apiVldpSetRotate); // Hypseus lua_register(L, "vldpSetRotate", apiVldpSetRotate); // Hypseus
lua_register(L, "vldpSetScale", apiVldpSetScale); // Hypseus lua_register(L, "vldpSetScale", apiVldpSetScale); // Hypseus
lua_register(L, "vldpSetShift", apiVldpSetShift); // 3.00
lua_register(L, "vldpSetVerbose", apiVldpSetVerbose); // 1.xx lua_register(L, "vldpSetVerbose", apiVldpSetVerbose); // 1.xx
} }
@ -5815,12 +5873,18 @@ static bool _sameLoosePath(const char *a, const char *b) {
} }
static void _saveAudioCalibration(int32_t milliseconds) { static void _saveMachineSettings(int32_t milliseconds) {
char *path = utilCreateString("%s%s", _global.conf->dataDirBase, AUDIO_CALIBRATION_FILE); char *path = _machineFilePath();
FILE *out = fopen(path, "w"); FILE *out = fopen(path, "w");
// Everything the service tools can set for this machine goes in one file, and it is written
// whole: the geometry comes from what is live now, which is what the operator just adjusted.
if (out) { if (out) {
fprintf(out, "delay = %d\n", milliseconds); fprintf(out, "delay = %d\n", milliseconds);
fprintf(out, "scale = %d\n", _global.videoScale);
fprintf(out, "shiftx = %d\n", _global.videoShiftX);
fprintf(out, "shifty = %d\n", _global.videoShiftY);
fprintf(out, "rotate = %d\n", _global.videoRotate);
fclose(out); fclose(out);
} else { } else {
utilSay("Unable to write %s", path); utilSay("Unable to write %s", path);
@ -8328,6 +8392,21 @@ static int32_t apiDiscGetSubtitleTracks(lua_State *L) {
} }
// count = discGetFrameCount() How many frames the disc holds, which is the last frame plus one.
// With a frame file it is the whole disc rather than the segment playing.
static int32_t apiDiscGetFrameCount(lua_State *L) {
int64_t count = 0;
_argCheck(L, "discGetFrameCount", 0, 0);
if (_global.videoHandle >= 0) {
count = _global.conf->isFrameFile ? frameFileGetFrameCount(_global.frameFileHandle) : videoGetFrameCount(_global.videoHandle);
}
lua_pushinteger(L, (lua_Integer)count);
return 1;
}
// frame = discGetFrame() // frame = discGetFrame()
static int32_t apiDiscGetFrame(lua_State *L) { static int32_t apiDiscGetFrame(lua_State *L) {
int64_t frame = 0; int64_t frame = 0;
@ -12533,6 +12612,55 @@ static int32_t apiSingeGetAudioLatency(lua_State *L) {
} }
// info = singeGetSystemInfo()
// What the trace header says about this machine, as a table, so a service screen can put it on
// screen and a photograph of it can answer most of a bug report. The same strings, from the same
// place, rather than a second description that could drift from the first.
static int32_t apiSingeGetSystemInfo(lua_State *L) {
char *os = mainDescribeOs();
char *cpu = mainDescribeCpu();
char *audio = mainDescribeAudioDecoders();
int32_t width = 0;
int32_t height = 0;
_argCheck(L, "singeGetSystemInfo", 0, 0);
lua_newtable(L);
lua_pushstring(L, VERSION_STRING); lua_setfield(L, -2, "version");
lua_pushstring(L, (os != NULL) ? os : ""); lua_setfield(L, -2, "os");
lua_pushstring(L, (cpu != NULL) ? cpu : ""); lua_setfield(L, -2, "cpu");
lua_pushstring(L, SDL_GetRendererName(_global.renderer)); lua_setfield(L, -2, "renderer");
lua_pushstring(L, (_global.device != NULL) ? SDL_GetGPUDeviceDriver(_global.device) : "none (3D unavailable)"); lua_setfield(L, -2, "gpu");
lua_pushstring(L, videoGetDecoderDescription()); lua_setfield(L, -2, "decoder");
lua_pushstring(L, (audio != NULL) ? audio : ""); lua_setfield(L, -2, "audio");
lua_pushstring(L, midiSoundfont()); lua_setfield(L, -2, "soundFont");
lua_pushstring(L, midiIoDescription()); lua_setfield(L, -2, "midi");
lua_pushstring(L, _global.conf->dataDir); lua_setfield(L, -2, "dataPath");
lua_pushstring(L, _global.conf->dataDirBase); lua_setfield(L, -2, "dataRoot");
lua_pushinteger(L, _global.canvasWidth); lua_setfield(L, -2, "canvasWidth");
lua_pushinteger(L, _global.canvasHeight); lua_setfield(L, -2, "canvasHeight");
SDL_GetWindowSize(_global.window, &width, &height);
lua_pushinteger(L, width); lua_setfield(L, -2, "windowWidth");
lua_pushinteger(L, height); lua_setfield(L, -2, "windowHeight");
free(os);
free(cpu);
free(audio);
return 1;
}
// singeSaveGeometry() Keeps the picture's scale, shift and rotation for this machine, beside the
// audio delay, so an operator sets a monitor up once rather than once per game. The live values
// are what is written, which is what the service menu has just been adjusting.
static int32_t apiSingeSaveGeometry(lua_State *L) {
_argCheck(L, "singeSaveGeometry", 0, 0);
_saveMachineSettings(videoGetAudioCalibration());
_luaTrace(L, "singeSaveGeometry", "%d %d %d %d", _global.videoScale, _global.videoShiftX, _global.videoShiftY, _global.videoRotate);
return 0;
}
// path = singeGetDataPath() // path = singeGetDataPath()
static int32_t apiSingeGetDataPath(lua_State *L) { static int32_t apiSingeGetDataPath(lua_State *L) {
_luaTrace(L, "singeGetDataPath", "%s", _global.conf->dataDir); _luaTrace(L, "singeGetDataPath", "%s", _global.conf->dataDir);
@ -12632,7 +12760,7 @@ static int32_t apiSingeSetAudioCalibration(lua_State *L) {
_luaDie(L, "singeSetAudioCalibration", "Audio calibration must be between %d and %d milliseconds: %d", -VIDEO_AUDIO_DELAY_MAX, VIDEO_AUDIO_DELAY_MAX, value); _luaDie(L, "singeSetAudioCalibration", "Audio calibration must be between %d and %d milliseconds: %d", -VIDEO_AUDIO_DELAY_MAX, VIDEO_AUDIO_DELAY_MAX, value);
} }
videoSetAudioCalibration(value); videoSetAudioCalibration(value);
_saveAudioCalibration(value); _saveMachineSettings(value);
_luaTrace(L, "singeSetAudioCalibration", "%d", value); _luaTrace(L, "singeSetAudioCalibration", "%d", value);
return 0; return 0;
@ -14878,6 +15006,39 @@ static int32_t apiVldpSetRotate(lua_State *L) {
// applied = vldpSetScale(percent) Hypseus extension. Shrinks the picture about its centre, 25 to // applied = vldpSetScale(percent) Hypseus extension. Shrinks the picture about its centre, 25 to
// 100 per cent; anything else answers false, as does a second change within 15 milliseconds, which // 100 per cent; anything else answers false, as does a second change within 15 milliseconds, which
// is the rate Hypseus limits its own held-key zoom to. // is the rate Hypseus limits its own held-key zoom to.
// x, y = vldpGetShift() Where the picture sits inside the room the scale factor leaves, as
// --shiftx and --shifty set it, -100 to 100.
static int32_t apiVldpGetShift(lua_State *L) {
_argCheck(L, "vldpGetShift", 0, 0);
lua_pushinteger(L, _global.videoShiftX);
lua_pushinteger(L, _global.videoShiftY);
return 2;
}
// vldpSetShift(x, y) Moves the picture inside that room, live. vldpGetScale and vldpSetScale
// decide how much room there is.
static int32_t apiVldpSetShift(lua_State *L) {
int32_t x = 0;
int32_t y = 0;
_argCheck(L, "vldpSetShift", 2, 2);
x = _argInteger(L, "vldpSetShift", 1);
y = _argInteger(L, "vldpSetShift", 2);
if ((x < SHIFT_MIN) || (x > SHIFT_MAX) || (y < SHIFT_MIN) || (y > SHIFT_MAX)) {
_luaDie(L, "vldpSetShift", "A shift is %d to %d per cent: %d, %d", SHIFT_MIN, SHIFT_MAX, x, y);
}
_global.videoShiftX = x;
_global.videoShiftY = y;
_computeVideoRect();
_global.refreshDisplay = true;
_luaTrace(L, "vldpSetShift", "%d %d", x, y);
return 0;
}
static int32_t apiVldpSetScale(lua_State *L) { static int32_t apiVldpSetScale(lua_State *L) {
int32_t percent = 0; int32_t percent = 0;
uint64_t now = utilTicks(); uint64_t now = utilTicks();
@ -15185,7 +15346,8 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
// packed game carries none of it inside the .game. // packed game carries none of it inside the .game.
persistOpen(_global.conf->dataDir); persistOpen(_global.conf->dataDir);
videoSetAudioDelay(_global.conf->audioDelayMs); videoSetAudioDelay(_global.conf->audioDelayMs);
videoSetAudioCalibration(_loadAudioCalibration()); // What the service tools left this machine set to, over whatever the command line asked for.
_loadMachineSettings();
utilTrace("Audio delay: device queue %d ms, calibration %d ms, game %d ms", videoGetAudioLatency(), videoGetAudioCalibration(), videoGetAudioDelay()); utilTrace("Audio delay: device queue %d ms, calibration %d ms, game %d ms", videoGetAudioLatency(), videoGetAudioCalibration(), videoGetAudioDelay());
_loadControlMappings(); _loadControlMappings();

View file

@ -70,7 +70,10 @@ typedef enum GivenE {
GIVEN_AUDIO_TRACK = 1 << 5, GIVEN_AUDIO_TRACK = 1 << 5,
GIVEN_AUDIO_DELAY = 1 << 6, GIVEN_AUDIO_DELAY = 1 << 6,
GIVEN_CANVAS = 1 << 7, GIVEN_CANVAS = 1 << 7,
GIVEN_AUDIO_SUFFIX = 1 << 8 GIVEN_AUDIO_SUFFIX = 1 << 8,
GIVEN_SCALE = 1 << 9,
GIVEN_SHIFT = 1 << 10, // -X or -Y
GIVEN_ROTATE = 1 << 11
} GivenE; } GivenE;
typedef struct ConfigS { typedef struct ConfigS {