More work on the menu system and Forge.
This commit is contained in:
parent
79e5fce23e
commit
34fc72ae5d
139 changed files with 15296 additions and 708 deletions
125
CHANGELOG
125
CHANGELOG
|
|
@ -7,6 +7,10 @@ SINGE 3.00
|
||||||
API Changes
|
API Changes
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
- onInputPressed and onInputReleased receive a second argument when the
|
||||||
|
switch came from a mouse or a light gun: the device's index, as
|
||||||
|
Hypseus passes it, so a two-gun game can tell its players apart. A
|
||||||
|
switch from the keyboard or a pad passes nothing there.
|
||||||
- singeScreenshot takes an optional file name, so a script that makes
|
- singeScreenshot takes an optional file name, so a script that makes
|
||||||
pictures on purpose can name them.
|
pictures on purpose can name them.
|
||||||
|
|
||||||
|
|
@ -21,6 +25,22 @@ API Changes
|
||||||
|
|
||||||
- In MODE_FULL a pad or mouse button reaches the script as the switch it
|
- In MODE_FULL a pad or mouse button reaches the script as the switch it
|
||||||
is mapped to, as the manual always said; it arrived as a keysym of 0.
|
is mapped to, as the manual always said; it arrived as a keysym of 0.
|
||||||
|
- A sprite scaled to nothing (a bullet shrinking to zero) draws as a
|
||||||
|
pixel instead of ending the engine with "Unable to transform sprite".
|
||||||
|
- --deterministic stepped the disc one video frame per engine frame,
|
||||||
|
whatever the step, and the overlay throttle's strict comparison ran
|
||||||
|
the script every other iteration of the virtual clock, so a game that
|
||||||
|
reads the same frame on two consecutive updates (as a game paced for
|
||||||
|
a faster overlay does) never saw it twice; the disc now follows the
|
||||||
|
virtual clock like everything else, each frame lasting the time it is
|
||||||
|
worth, and the script runs every step. A bare os.time() answers
|
||||||
|
with the virtual clock too, so a game that seeds its generator from
|
||||||
|
the wall clock when a game starts (the one-player Space Pirates)
|
||||||
|
repeats; os.time with a date table is Lua's own.
|
||||||
|
- fontSelect given no font (nil, or an index that was never loaded)
|
||||||
|
keeps the current font and warns instead of ending the script, as
|
||||||
|
Hypseus lets it pass; The Last Bounty Hunter names a font it never
|
||||||
|
loaded at its first board.
|
||||||
|
|
||||||
- guiSetHandler set again after a document's elements were rebuilt (a
|
- guiSetHandler set again after a document's elements were rebuilt (a
|
||||||
list through inner_rml) now attaches to the new element with that id.
|
list through inner_rml) now attaches to the new element with that id.
|
||||||
|
|
@ -58,7 +78,42 @@ API Changes
|
||||||
by count and interval at a moment, a disc frame, or a rail camera's
|
by count and interval at a moment, a disc frame, or a rail camera's
|
||||||
stop, from an entity or a list of them in turn, or at a point; and
|
stop, from an entity or a list of them in turn, or at a point; and
|
||||||
soundDone says when a clip playSound or a sound behaviour started has
|
soundDone says when a clip playSound or a sound behaviour started has
|
||||||
ended. Flat
|
ended. Ports of the test library began (FORGE.md section 14): the
|
||||||
|
five ActionMax games are Forge descriptions written by
|
||||||
|
util/forgePortActionMax.py, and play the same as the emulator under
|
||||||
|
the same inputs (testScripts/ports/). On the way: a lightSensor
|
||||||
|
behaviour, a pointer behaviour, fonts and anchors on the text look,
|
||||||
|
sprites drawn in copies, discPlay and discPause, pointerX and pointerY
|
||||||
|
in expressions, an event belonging to the room it happened in, and a
|
||||||
|
built game finding files beside its description. A game can bring a
|
||||||
|
vocabulary of its own: a description names a file of Lua
|
||||||
|
(vocabulary = "Vocabulary.singe") that adds looks, behaviours,
|
||||||
|
conditions, actions, layers, or events to the manifest for that game
|
||||||
|
alone; the editor loads it when the game opens and forgets it when
|
||||||
|
the game closes, the compiler checks against it, and the built game
|
||||||
|
carries it into a release. The ActionMax light sensor lives in one
|
||||||
|
now, beside those five games, and not in Forge. The Hypseus library's
|
||||||
|
eleven Karis Framework 3.32b titles (Arcade Xperience 1 to 3, Astroboy,
|
||||||
|
Danmachi, Freddy, Starship Troopers, Sugar Rush, Survival, Esh's
|
||||||
|
Aurunmilla, and Badlands Lite) are ported by the same converter, which
|
||||||
|
finds the framework where a game keeps it and notes its version; the
|
||||||
|
rest of that library is surveyed in FORGE.md 14.9. A branch on the
|
||||||
|
branching track can want its move mashed (mash = 6) or held (hold =
|
||||||
|
20). The seven Kimmy Script Engine titles (Brain Dead 13, Future Boy
|
||||||
|
Conan, Mazinga Z, Fuma Conspiracy, Sonic 1996, Sintel, Ninja Hayate
|
||||||
|
1080) are ported by the same converter and a kimmy behaviour that
|
||||||
|
plays that engine's loop, and play as their originals do (FORGE.md
|
||||||
|
14.11).
|
||||||
|
util/forgePorts.py
|
||||||
|
assembles the ported games into a project folder of their own
|
||||||
|
(~/claude/singePorts), one game to a folder with its description, the
|
||||||
|
compiled port, the runtime, its files, and a games.dat. A description is a
|
||||||
|
.forge file now (it was .game, which is the packed release's, and the
|
||||||
|
chooser had to sniff which was which). The seventeen KarisFramework
|
||||||
|
games are ported too: util/forgePortKaris.lua reads a game's own
|
||||||
|
script, and the qte behaviour plays the framework's loop over the
|
||||||
|
framework's tables, running the game's script and add-ons in a shim;
|
||||||
|
every one plays as its original under the same driver. Flat
|
||||||
bodies are the size their looks are drawn at (they were half), and a
|
bodies are the size their looks are drawn at (they were half), and a
|
||||||
platformer stands on its point.
|
platformer stands on its point.
|
||||||
|
|
||||||
|
|
@ -494,8 +549,12 @@ API Changes
|
||||||
|
|
||||||
- Compressed textures. KTX2 files (Basis Universal, from
|
- Compressed textures. KTX2 files (Basis Universal, from
|
||||||
util/compressTextures.py) stay compressed on the GPU in the block
|
util/compressTextures.py) stay compressed on the GPU in the block
|
||||||
format the machine has (BC7, ASTC, BC3, or plain RGBA where nothing
|
format the machine has (BC7, ASTC, ETC2, BC3, or plain RGBA where
|
||||||
else exists), a quarter of the memory of PNGs or less. A model whose
|
nothing else exists), a quarter of the memory of PNGs or less. ETC2
|
||||||
|
is the one block format OpenGL ES 3 guarantees, so a handheld's Mali
|
||||||
|
without ASTC keeps its textures compressed rather than falling to RGBA
|
||||||
|
on the machine with the least memory; the GLES backend takes ETC2
|
||||||
|
always and ASTC 4x4 where the driver offers it. A model whose
|
||||||
file uses KHR_texture_basisu loads them on its own; materialSetTexture
|
file uses KHR_texture_basisu loads them on its own; materialSetTexture
|
||||||
and the map calls take a .ktx2 name in place of a sprite; sceneGetStats
|
and the map calls take a .ktx2 name in place of a sprite; sceneGetStats
|
||||||
reports the texture memory in use. The Sponza test scene went from
|
reports the texture memory in use. The Sponza test scene went from
|
||||||
|
|
@ -1113,6 +1172,25 @@ API Changes
|
||||||
Fixes
|
Fixes
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
- The menu's grid, horizon and sun were drawn black and unlit through the
|
||||||
|
intro, which kept the lights off them but not the blast's glow behind
|
||||||
|
them: a black line across a flash is a line, and the whole grid showed
|
||||||
|
through the explosion as its outline. They are not drawn at all until
|
||||||
|
the grid comes up out of the dark. The recording of the backdrop is
|
||||||
|
re-made with it.
|
||||||
|
|
||||||
|
- A PNG with 16-bit samples and no alpha channel (crate.png in the test
|
||||||
|
scripts is one) drew with coloured fringes: SDL_image put libpng's
|
||||||
|
big-endian 48-bit pixels into a surface the machine reads
|
||||||
|
little-endian, swapping only the 64-bit kind, so every sample's low
|
||||||
|
byte was taken for its colour. The vendored loader swaps both now.
|
||||||
|
|
||||||
|
- musicPlay answers the handle it was given, as Hypseus's does. Its
|
||||||
|
games keep that (sndHandle = musicPlay(music)) to stop the piece, and
|
||||||
|
three of them (Freddy, Starship Troopers, Sugar Rush) died on the map
|
||||||
|
screen with "musicStop: Argument 1 must be a number" because Singe's
|
||||||
|
answered nothing. musicStop takes a nil handle as none.
|
||||||
|
|
||||||
- A machine with no enumerable mouse keeps its pointer. ManyMouse reads
|
- A machine with no enumerable mouse keeps its pointer. ManyMouse reads
|
||||||
/dev/input/event* directly where there is no X server, and those nodes
|
/dev/input/event* directly where there is no X server, and those nodes
|
||||||
are root:input, so an account that was never added to the "input" group
|
are root:input, so an account that was never added to the "input" group
|
||||||
|
|
@ -1151,13 +1229,40 @@ Fixes
|
||||||
clips the old video was cut from are gone, one of which nobody could
|
clips the old video was cut from are gone, one of which nobody could
|
||||||
account for.
|
account for.
|
||||||
|
|
||||||
- The menu has a sound. util/makeMenuSound.py writes it -- every noise in
|
- util/renderMenuAd.py records the menu's intro with its sound as one
|
||||||
it is an oscillator, a burst of noise or an envelope over one of the two,
|
H.264 file at any size and rate (1920x1080 at 50 fps by default), for
|
||||||
in a room that is a convolution with a tail of decaying noise, so there is
|
use outside the engine; the backdrop's tagline is laid out in step with
|
||||||
nothing in it that cannot be accounted for either. The charge, the rush
|
the overlay it is drawn on, so a larger recording gets a larger tagline
|
||||||
of the logo arriving, the wingbeats (on the same curve the animation flaps
|
rather than the menu's scaled up.
|
||||||
to), the flame, and a riser as the grid comes up, over a bed whose four
|
|
||||||
bars land exactly where the menu takes the screen.
|
- The service tools are dressed like the rest of the menu. Each tool was
|
||||||
|
a sheet of white text on black; it is now a card over the moving
|
||||||
|
backdrop in the game page's own colours -- the panel gradient, the
|
||||||
|
blue-grey border, the title in gold on the selection's gradient bar, a
|
||||||
|
chosen row on the same bar, and the key hints in a footer strip. The
|
||||||
|
list of tools says what the chosen tool is for in a column beside the
|
||||||
|
list rather than under it, and the two tools that draw their own
|
||||||
|
picture (Light Gun and Display) keep a strip of the card along the
|
||||||
|
bottom, out of the way of the targets. Both renderers draw it the same
|
||||||
|
way: the RmlUi document, and the plain overlay a machine without a GPU
|
||||||
|
falls back to. Rows line up: a tool no longer pads columns into its
|
||||||
|
text with spaces, which a proportional font never honoured; a pair is
|
||||||
|
a label column and a value column, and a list item carries its detail
|
||||||
|
(a size, a state, a field's value) against the right edge of the row.
|
||||||
|
util/shootMenu.py runs the menu headless through both renderers and
|
||||||
|
shoots every page into screenshots/menu, with a contact sheet each.
|
||||||
|
|
||||||
|
- The menu has a sound. util/makeMenuSound.py writes it. The three things
|
||||||
|
the picture shows are recordings now: the charge is Mark DiAngelo's
|
||||||
|
"Explosion Ultra Bass", the logo bursting out of it Mike Koenig's "Large
|
||||||
|
Fireball", and the dragon Daniel Simon's "Dragon Fire Breath and Roar",
|
||||||
|
laid so the moment its roar turns to fire is the moment the animation
|
||||||
|
breathes -- three sounds from SoundBible under Creative Commons
|
||||||
|
Attribution 3.0, kept under assets/samples and credited in LICENSES.
|
||||||
|
The rest is still made in the script -- the wingbeats (on the same curve
|
||||||
|
the animation flaps to), a riser as the grid comes up, the logo leaving,
|
||||||
|
and a bed whose four bars land exactly where the menu takes the screen --
|
||||||
|
from oscillators and noise, in a room that is a convolution.
|
||||||
The charge and the flame are built the way the things themselves are
|
The charge and the flame are built the way the things themselves are
|
||||||
heard rather than the way they are drawn. Both were made of smooth
|
heard rather than the way they are drawn. Both were made of smooth
|
||||||
envelopes over steady noise -- the flame of a band of noise wobbled by two
|
envelopes over steady noise -- the flame of a band of noise wobbled by two
|
||||||
|
|
|
||||||
12
LICENSES
12
LICENSES
|
|
@ -69,6 +69,18 @@ Fonts
|
||||||
FreeSansBold GPL-3.0 with font exception https://www.gnu.org/software/freefont
|
FreeSansBold GPL-3.0 with font exception https://www.gnu.org/software/freefont
|
||||||
|
|
||||||
|
|
||||||
|
Sounds
|
||||||
|
------
|
||||||
|
|
||||||
|
The menu's intro (assets/menuIntro.flac, written by util/makeMenuSound.py) is built on three
|
||||||
|
recordings from SoundBible, each under the Creative Commons Attribution 3.0 licence
|
||||||
|
(https://creativecommons.org/licenses/by/3.0/) and kept as FLAC under assets/samples:
|
||||||
|
|
||||||
|
Explosion Ultra Bass Mark DiAngelo CC BY 3.0 https://soundbible.com/1807-Explosion-Ultra-Bass.html
|
||||||
|
Large Fireball Mike Koenig CC BY 3.0 https://soundbible.com/1348-Large-Fireball.html
|
||||||
|
Dragon Fire Breath and Roar Daniel Simon CC BY 3.0 https://soundbible.com/2127-Dragon-Fire-Breath-and-Roar.html
|
||||||
|
|
||||||
|
|
||||||
Data
|
Data
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,8 @@ local backdropTagX = 0
|
||||||
local backdropTagY = 0
|
local backdropTagY = 0
|
||||||
local blown = false -- Whether the charge has gone off.
|
local blown = false -- Whether the charge has gone off.
|
||||||
local gridLit = false -- Whether the grid has finished coming up and its colours are final.
|
local gridLit = false -- Whether the grid has finished coming up and its colours are final.
|
||||||
|
local gridShown = false -- Whether the grid, the horizon and the sun are drawn at all yet.
|
||||||
|
local backdropParts = {} -- Those nodes: hidden through the intro, shown as the grid comes up.
|
||||||
local backdropFont = nil
|
local backdropFont = nil
|
||||||
|
|
||||||
-- ===== The backdrop ==========================================================================
|
-- ===== The backdrop ==========================================================================
|
||||||
|
|
@ -425,12 +427,14 @@ local grid = nodeNew()
|
||||||
|
|
||||||
nodeSetMesh(grid, gridMesh(), gridMat)
|
nodeSetMesh(grid, gridMesh(), gridMat)
|
||||||
nodeSetParent(grid, backdrop)
|
nodeSetParent(grid, backdrop)
|
||||||
|
backdropParts[#backdropParts + 1] = grid
|
||||||
|
|
||||||
local horizon = nodeNew()
|
local horizon = nodeNew()
|
||||||
nodeSetMesh(horizon, meshPlane(HORIZ_W * 2, HORIZ_H), horizMat)
|
nodeSetMesh(horizon, meshPlane(HORIZ_W * 2, HORIZ_H), horizMat)
|
||||||
nodeSetRotation(horizon, 90, 0, 0)
|
nodeSetRotation(horizon, 90, 0, 0)
|
||||||
nodeSetPosition(horizon, 0, horizonY(HORIZ_Z), HORIZ_Z)
|
nodeSetPosition(horizon, 0, horizonY(HORIZ_Z), HORIZ_Z)
|
||||||
nodeSetParent(horizon, backdrop)
|
nodeSetParent(horizon, backdrop)
|
||||||
|
backdropParts[#backdropParts + 1] = horizon
|
||||||
|
|
||||||
local sunCut = horizonY(SUN_Z) - SUN_Y
|
local sunCut = horizonY(SUN_Z) - SUN_Y
|
||||||
local backSun = nodeNew()
|
local backSun = nodeNew()
|
||||||
|
|
@ -438,6 +442,7 @@ local backSun = nodeNew()
|
||||||
nodeSetMesh(backSun, sunMesh(sunCut), sunMat2)
|
nodeSetMesh(backSun, sunMesh(sunCut), sunMat2)
|
||||||
nodeSetPosition(backSun, 0, SUN_Y, SUN_Z)
|
nodeSetPosition(backSun, 0, SUN_Y, SUN_Z)
|
||||||
nodeSetParent(backSun, backdrop)
|
nodeSetParent(backSun, backdrop)
|
||||||
|
backdropParts[#backdropParts + 1] = backSun
|
||||||
|
|
||||||
for n = 1, SUN_SLICES do
|
for n = 1, SUN_SLICES do
|
||||||
local slice = nodeNew()
|
local slice = nodeNew()
|
||||||
|
|
@ -448,6 +453,7 @@ for n = 1, SUN_SLICES do
|
||||||
nodeSetMesh(slice, meshPlane(w * 2, height), sliceMat)
|
nodeSetMesh(slice, meshPlane(w * 2, height), sliceMat)
|
||||||
nodeSetRotation(slice, 90, 0, 0)
|
nodeSetRotation(slice, 90, 0, 0)
|
||||||
nodeSetPosition(slice, 0, SUN_Y + y, SUN_Z + 0.4)
|
nodeSetPosition(slice, 0, SUN_Y + y, SUN_Z + 0.4)
|
||||||
|
backdropParts[#backdropParts + 1] = slice
|
||||||
nodeSetParent(slice, backdrop)
|
nodeSetParent(slice, backdrop)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -483,6 +489,15 @@ function backdropFrame()
|
||||||
-- there is no moment where one thing has ended and the next has not begun.
|
-- there is no moment where one thing has ended and the next has not begun.
|
||||||
local lit = span(t, GRID_UP, GRID_LIT)
|
local lit = span(t, GRID_UP, GRID_LIT)
|
||||||
|
|
||||||
|
-- Until then the grid, the horizon and the sun are not drawn at all. Black and unlit, they
|
||||||
|
-- were still there: a black line across the blast's glow is a line, and the whole grid showed
|
||||||
|
-- through the explosion as its outline.
|
||||||
|
if (lit > 0) and not gridShown then
|
||||||
|
gridShown = true
|
||||||
|
for _, part in ipairs(backdropParts) do
|
||||||
|
nodeSetVisible(part, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
if (lit > 0) and not gridLit then
|
if (lit > 0) and not gridLit then
|
||||||
materialSetColor(gridMat, 255 * lit, 45 * lit, 190 * lit)
|
materialSetColor(gridMat, 255 * lit, 45 * lit, 190 * lit)
|
||||||
materialSetColor(horizMat, 255 * lit, 150 * lit, 235 * lit)
|
materialSetColor(horizMat, 255 * lit, 150 * lit, 235 * lit)
|
||||||
|
|
@ -566,7 +581,10 @@ end
|
||||||
function backdropBegin(showIntro)
|
function backdropBegin(showIntro)
|
||||||
-- A font of its own. The document renderer draws everything else through RmlUi and never
|
-- A font of its own. The document renderer draws everything else through RmlUi and never
|
||||||
-- selects one, so the tagline -- the only thing put straight into the overlay -- brings it.
|
-- selects one, so the tagline -- the only thing put straight into the overlay -- brings it.
|
||||||
backdropFont = fontLoad("Singe/FreeSansBold.ttf", 30)
|
-- Thirty points on the menu's 480 rows, and in step with the overlay elsewhere: a recording
|
||||||
|
-- made at a larger size (util/renderMenuAd.py) lays the tagline out at that size rather than
|
||||||
|
-- scaling the menu's up.
|
||||||
|
backdropFont = fontLoad("Singe/FreeSansBold.ttf", math.floor(30 * overlayGetHeight() / 480 + 0.5))
|
||||||
fontSelect(backdropFont)
|
fontSelect(backdropFont)
|
||||||
fontQuality(FONT_QUALITY_BLENDED)
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
|
|
||||||
|
|
@ -582,6 +600,10 @@ function backdropBegin(showIntro)
|
||||||
backdropStarted = singeGetTicks() - (showIntro and 0 or (LIFT_TO * 1000))
|
backdropStarted = singeGetTicks() - (showIntro and 0 or (LIFT_TO * 1000))
|
||||||
blown = not showIntro
|
blown = not showIntro
|
||||||
gridLit = false
|
gridLit = false
|
||||||
|
gridShown = false
|
||||||
|
for _, part in ipairs(backdropParts) do
|
||||||
|
nodeSetVisible(part, false)
|
||||||
|
end
|
||||||
if not showIntro then
|
if not showIntro then
|
||||||
nodeSetVisible(logo, false)
|
nodeSetVisible(logo, false)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -63,7 +63,9 @@ local FUNCTIONS = {
|
||||||
max = "math.max",
|
max = "math.max",
|
||||||
floor = "math.floor",
|
floor = "math.floor",
|
||||||
time = "authorTime",
|
time = "authorTime",
|
||||||
frame = "discGetFrame"
|
frame = "discGetFrame",
|
||||||
|
pointerX = "authorPointerX",
|
||||||
|
pointerY = "authorPointerY"
|
||||||
}
|
}
|
||||||
|
|
||||||
local NAMES = {
|
local NAMES = {
|
||||||
|
|
@ -781,6 +783,21 @@ function authorCompile(game)
|
||||||
data[key] = value
|
data[key] = value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- A type's empty vars table says nothing the runtime does not assume; left out, so a
|
||||||
|
-- description that has one and one that has none compile the same.
|
||||||
|
if game.types then
|
||||||
|
data.types = {}
|
||||||
|
for name, entityType in pairs(game.types) do
|
||||||
|
local copy = {}
|
||||||
|
|
||||||
|
for key, value in pairs(entityType) do
|
||||||
|
if not ((key == "vars") and (type(value) == "table") and (next(value) == nil)) then
|
||||||
|
copy[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
data.types[name] = copy
|
||||||
|
end
|
||||||
|
end
|
||||||
out[#out + 1] = "-- Generated by Forge/AuthorCompile.singe from " .. (game.source or "a game description")
|
out[#out + 1] = "-- Generated by Forge/AuthorCompile.singe from " .. (game.source or "a game description")
|
||||||
out[#out + 1] = "-- " .. (game.title or "Untitled")
|
out[#out + 1] = "-- " .. (game.title or "Untitled")
|
||||||
out[#out + 1] = "--"
|
out[#out + 1] = "--"
|
||||||
|
|
@ -796,6 +813,17 @@ function authorCompile(game)
|
||||||
out[#out + 1] = 'local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\\\])") or "")'
|
out[#out + 1] = 'local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\\\])") or "")'
|
||||||
out[#out + 1] = 'dofile(here .. "Author.singe")'
|
out[#out + 1] = 'dofile(here .. "Author.singe")'
|
||||||
out[#out + 1] = "AUTHOR_DIR = here -- Files the description names are looked for here first."
|
out[#out + 1] = "AUTHOR_DIR = here -- Files the description names are looked for here first."
|
||||||
|
-- A description built where it lives -- a port beside its game's own files, a test -- names
|
||||||
|
-- files beside itself, which is not where the built script sits; they are looked for there
|
||||||
|
-- second. A released game has them beside the script, and a packed one inside the pack.
|
||||||
|
local sourceDir = game.source and string.match(game.source, "^(.*[/\\])") or ""
|
||||||
|
|
||||||
|
if sourceDir ~= "" then
|
||||||
|
out[#out + 1] = "AUTHOR_SOURCE_DIR = " .. string.format("%q", sourceDir) .. " -- Then beside the description this was built from."
|
||||||
|
end
|
||||||
|
if game.vocabulary then
|
||||||
|
out[#out + 1] = "authorVocabulary(" .. string.format("%q", game.vocabulary) .. ") -- The game's own looks, behaviours, conditions, and actions."
|
||||||
|
end
|
||||||
out[#out + 1] = ""
|
out[#out + 1] = ""
|
||||||
out[#out + 1] = "-- The description, as the runtime wants it: everything but the rules, which follow as code."
|
out[#out + 1] = "-- The description, as the runtime wants it: everything but the rules, which follow as code."
|
||||||
out[#out + 1] = "authorBegin(" .. valueSource(data, 0) .. ")"
|
out[#out + 1] = "authorBegin(" .. valueSource(data, 0) .. ")"
|
||||||
|
|
@ -852,6 +880,10 @@ function authorBuild(descriptionFile, outputFile)
|
||||||
local file
|
local file
|
||||||
|
|
||||||
game.source = descriptionFile
|
game.source = descriptionFile
|
||||||
|
-- The game's own vocabulary, before the checker reads the manifest.
|
||||||
|
if game.vocabulary then
|
||||||
|
authorVocabulary(game.vocabulary, string.match(descriptionFile, "^(.*[/\\])") or "")
|
||||||
|
end
|
||||||
for _, problem in ipairs(authorCheck(game)) do
|
for _, problem in ipairs(authorCheck(game)) do
|
||||||
debugPrint("Author: " .. problem)
|
debugPrint("Author: " .. problem)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ local NEW_H = 40
|
||||||
local STRIP_H = 34 -- The timeline along the bottom of the canvas.
|
local STRIP_H = 34 -- The timeline along the bottom of the canvas.
|
||||||
local STRIP_PAD = 8
|
local STRIP_PAD = 8
|
||||||
local MODES = { "entities", "types", "rules", "tracks", "dialogues" }
|
local MODES = { "entities", "types", "rules", "tracks", "dialogues" }
|
||||||
local FILE_KINDS = { png = true, jpg = true, jpeg = true, svg = true, glb = true, wav = true, ogg = true, mp3 = true, flac = true, mkv = true, mp4 = true, mpg = true, rml = true, hdr = true, ttf = true }
|
local FILE_KINDS = { png = true, jpg = true, jpeg = true, svg = true, glb = true, wav = true, ogg = true, mp3 = true, flac = true, mkv = true, mp4 = true, mpg = true, rml = true, hdr = true, ttf = true, singe = true }
|
||||||
local FILE_DEPTH = 2 -- How far under the game's directory the file picker looks.
|
local FILE_DEPTH = 2 -- How far under the game's directory the file picker looks.
|
||||||
local FILE_NONE = "(none)" -- The file picker's first entry: no file.
|
local FILE_NONE = "(none)" -- The file picker's first entry: no file.
|
||||||
local FRAME_STEP = 1
|
local FRAME_STEP = 1
|
||||||
|
|
@ -1024,6 +1024,44 @@ end
|
||||||
|
|
||||||
-- The disc layer's file, if the description has one, loaded and paused so the timeline can show
|
-- The disc layer's file, if the description has one, loaded and paused so the timeline can show
|
||||||
-- the frame under the cursor.
|
-- the frame under the cursor.
|
||||||
|
-- A frame file names several videos, each from a disc frame: the one with the longest run of
|
||||||
|
-- frames is the game's own, and is what the editor scrubs, its first frame remembered so the
|
||||||
|
-- cursor's disc frame lands on the right picture. Answers the video's path and its first frame.
|
||||||
|
local function frameFileVideo(path)
|
||||||
|
local folder = path:match("^(.*[/\\])") or ""
|
||||||
|
local entries = {}
|
||||||
|
local best = nil
|
||||||
|
|
||||||
|
for line in io.lines(path) do
|
||||||
|
local start, name = line:match("^%s*(%d+)%s+(.-)%s*$")
|
||||||
|
|
||||||
|
if start then
|
||||||
|
entries[#entries + 1] = { start = tonumber(start), name = name }
|
||||||
|
elseif (#entries == 0) and (line:match("%S")) and not line:match("^%s*%d") then
|
||||||
|
-- The first line is the directory the videos are in; its own is the usual.
|
||||||
|
local where = line:gsub("%s+$", ""):gsub("\\", "/")
|
||||||
|
|
||||||
|
if (where ~= ".") and (where ~= "./") then
|
||||||
|
folder = folder .. where:gsub("/?$", "/")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for index, entry in ipairs(entries) do
|
||||||
|
local following = entries[index + 1]
|
||||||
|
|
||||||
|
entry.span = following and (following.start - entry.start) or 0
|
||||||
|
if (best == nil) or (entry.span > best.span) then
|
||||||
|
best = entry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if best == nil then
|
||||||
|
return nil, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
return folder .. best.name, best.start
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
local function loadVideo()
|
local function loadVideo()
|
||||||
local file
|
local file
|
||||||
|
|
||||||
|
|
@ -1037,11 +1075,19 @@ local function loadVideo()
|
||||||
videoUnload(FORGE.video)
|
videoUnload(FORGE.video)
|
||||||
FORGE.video = nil
|
FORGE.video = nil
|
||||||
end
|
end
|
||||||
FORGE.videoFile = file
|
FORGE.videoFile = file
|
||||||
|
FORGE.videoStart = 0
|
||||||
if file and (lfs.attributes(forgeFile(file), "mode") == "file") then
|
if file and (lfs.attributes(forgeFile(file), "mode") == "file") then
|
||||||
FORGE.video = videoLoad(forgeFile(file))
|
local path = forgeFile(file)
|
||||||
videoPause(FORGE.video)
|
|
||||||
videoSeek(FORGE.video, FORGE.cursor)
|
if path:lower():match("%.txt$") then
|
||||||
|
path, FORGE.videoStart = frameFileVideo(path)
|
||||||
|
end
|
||||||
|
if path and (lfs.attributes(path, "mode") == "file") then
|
||||||
|
FORGE.video = videoLoad(path)
|
||||||
|
videoPause(FORGE.video)
|
||||||
|
videoSeek(FORGE.video, math.max(0, FORGE.cursor - FORGE.videoStart))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -1053,6 +1099,11 @@ function forgeBegin(path)
|
||||||
if FORGE.game == nil then
|
if FORGE.game == nil then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
-- The game's own vocabulary, in place of the last game's, before the panels read the manifest.
|
||||||
|
authorVocabularyForget()
|
||||||
|
if FORGE.game.vocabulary then
|
||||||
|
authorVocabulary(FORGE.game.vocabulary, string.match(path, "^(.*[/\\])") or "")
|
||||||
|
end
|
||||||
FORGE.game.types = FORGE.game.types or {}
|
FORGE.game.types = FORGE.game.types or {}
|
||||||
FORGE.game.rules = FORGE.game.rules or {}
|
FORGE.game.rules = FORGE.game.rules or {}
|
||||||
FORGE.game.layers = FORGE.game.layers or {}
|
FORGE.game.layers = FORGE.game.layers or {}
|
||||||
|
|
@ -1117,6 +1168,7 @@ function forgeClose()
|
||||||
if FORGE.gui ~= nil then
|
if FORGE.gui ~= nil then
|
||||||
guiDelete(FORGE.gui)
|
guiDelete(FORGE.gui)
|
||||||
end
|
end
|
||||||
|
authorVocabularyForget()
|
||||||
for _, cache in ipairs({ FORGE.sprites, FORGE.sheets }) do
|
for _, cache in ipairs({ FORGE.sprites, FORGE.sheets }) do
|
||||||
for _, sprite in pairs(cache) do
|
for _, sprite in pairs(cache) do
|
||||||
if sprite then
|
if sprite then
|
||||||
|
|
@ -1239,7 +1291,7 @@ function forgeExport(folder, name)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The description travels with the game, so the release can be opened and edited again.
|
-- The description travels with the game, so the release can be opened and edited again.
|
||||||
authorSave(FORGE.game, folder .. "/" .. script .. ".game")
|
authorSave(FORGE.game, folder .. "/" .. script .. ".forge")
|
||||||
|
|
||||||
-- A games.dat so the menu lists it, written from what the description already knows. Names
|
-- A games.dat so the menu lists it, written from what the description already knows. Names
|
||||||
-- in it are as the engine sees them from the games directory, the folder's own name first,
|
-- in it are as the engine sees them from the games directory, the folder's own name first,
|
||||||
|
|
@ -1304,6 +1356,7 @@ function forgeFilesNamed(game)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
take(game.vocabulary)
|
||||||
for _, layer in ipairs(game.layers or {}) do
|
for _, layer in ipairs(game.layers or {}) do
|
||||||
fromParams(layer, (AUTHOR.layers[layer.kind] or {}).params)
|
fromParams(layer, (AUTHOR.layers[layer.kind] or {}).params)
|
||||||
end
|
end
|
||||||
|
|
@ -3323,7 +3376,7 @@ end
|
||||||
function forgeCursorTo(at)
|
function forgeCursorTo(at)
|
||||||
FORGE.cursor = math.max(0, math.floor(at))
|
FORGE.cursor = math.max(0, math.floor(at))
|
||||||
if FORGE.video then
|
if FORGE.video then
|
||||||
videoSeek(FORGE.video, FORGE.cursor)
|
videoSeek(FORGE.video, math.max(0, FORGE.cursor - (FORGE.videoStart or 0)))
|
||||||
end
|
end
|
||||||
forgeRefresh()
|
forgeRefresh()
|
||||||
end
|
end
|
||||||
|
|
@ -3578,6 +3631,8 @@ function forgeFieldKind(name)
|
||||||
param = key
|
param = key
|
||||||
elseif (name == "reset") or (name == "depthSort") then
|
elseif (name == "reset") or (name == "depthSort") then
|
||||||
return "flag"
|
return "flag"
|
||||||
|
elseif name == "vocabulary" then
|
||||||
|
return "file"
|
||||||
end
|
end
|
||||||
elseif FORGE.mode == "tracks" then
|
elseif FORGE.mode == "tracks" then
|
||||||
local track = forgeRoom().tracks[FORGE.track]
|
local track = forgeRoom().tracks[FORGE.track]
|
||||||
|
|
@ -4220,7 +4275,7 @@ end
|
||||||
-- The fields with nothing selected: the game's -- its title, players, verbs, and layers with
|
-- The fields with nothing selected: the game's -- its title, players, verbs, and layers with
|
||||||
-- what each layer takes, named layer.parameter as a behaviour's are -- and then the room's.
|
-- what each layer takes, named layer.parameter as a behaviour's are -- and then the room's.
|
||||||
function forgeRoomFieldNames()
|
function forgeRoomFieldNames()
|
||||||
local names = { "title", "players", "vars", "verbs", "layers" }
|
local names = { "title", "players", "vars", "verbs", "layers", "vocabulary" }
|
||||||
|
|
||||||
for _, layer in ipairs(FORGE.game.layers or {}) do
|
for _, layer in ipairs(FORGE.game.layers or {}) do
|
||||||
local entry = AUTHOR.layers[layer.kind]
|
local entry = AUTHOR.layers[layer.kind]
|
||||||
|
|
@ -4263,6 +4318,8 @@ function forgeRoomGet(room, key)
|
||||||
return table.concat(FORGE.game.verbs or {}, ", ")
|
return table.concat(FORGE.game.verbs or {}, ", ")
|
||||||
elseif key == "layers" then
|
elseif key == "layers" then
|
||||||
return listText(FORGE.game.layers)
|
return listText(FORGE.game.layers)
|
||||||
|
elseif key == "vocabulary" then
|
||||||
|
return FORGE.game.vocabulary
|
||||||
elseif group ~= nil then
|
elseif group ~= nil then
|
||||||
local layer = layerOf(group)
|
local layer = layerOf(group)
|
||||||
|
|
||||||
|
|
@ -4301,6 +4358,13 @@ function forgeRoomSet(room, key, value)
|
||||||
|
|
||||||
if key == "title" then
|
if key == "title" then
|
||||||
FORGE.game.title = tostring(value)
|
FORGE.game.title = tostring(value)
|
||||||
|
elseif key == "vocabulary" then
|
||||||
|
-- The game's own vocabulary, loaded as it is named so the panels offer what it adds.
|
||||||
|
FORGE.game.vocabulary = ((value ~= nil) and (value ~= "")) and tostring(value) or nil
|
||||||
|
authorVocabularyForget()
|
||||||
|
if FORGE.game.vocabulary then
|
||||||
|
authorVocabulary(FORGE.game.vocabulary, string.match(FORGE.path or "", "^(.*[/\\])") or "")
|
||||||
|
end
|
||||||
elseif key == "players" then
|
elseif key == "players" then
|
||||||
if type(value) == "number" then
|
if type(value) == "number" then
|
||||||
FORGE.game.players = (value ~= 1) and math.floor(value) or nil
|
FORGE.game.players = (value ~= 1) and math.floor(value) or nil
|
||||||
|
|
@ -4435,7 +4499,11 @@ function forgeEditCancel()
|
||||||
if FORGE.editing == nil then
|
if FORGE.editing == nil then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
FORGE.editing.set(FORGE.editing.key, FORGE.editing.was)
|
-- Only a value the typing changed is put back: a field passed over is left exactly as it
|
||||||
|
-- was, not written again (writing "" to a type's vars used to make it an empty table).
|
||||||
|
if FORGE.editing.changed then
|
||||||
|
FORGE.editing.set(FORGE.editing.key, FORGE.editing.was)
|
||||||
|
end
|
||||||
FORGE.editing = nil
|
FORGE.editing = nil
|
||||||
FORGE.lastField = nil -- The next ENTER starts the form over rather than moving on.
|
FORGE.lastField = nil -- The next ENTER starts the form over rather than moving on.
|
||||||
FORGE.message = "unchanged"
|
FORGE.message = "unchanged"
|
||||||
|
|
@ -4602,9 +4670,10 @@ end
|
||||||
|
|
||||||
-- ===== Files ==================================================================================
|
-- ===== Files ==================================================================================
|
||||||
--
|
--
|
||||||
-- Descriptions live in Forge's data directory, which is the one place a packed Forge can write.
|
-- Descriptions are .forge files -- Lua text; .game is a packed release, which the engine's menu
|
||||||
-- Ones dropped into Forge's own directory are offered too, and opened as a copy in the data
|
-- lists and Forge does not. They live in Forge's data directory, which is the one place a packed
|
||||||
-- directory, since inside a .game they are read only.
|
-- Forge can write. Ones dropped into Forge's own directory are offered too, and opened as a copy
|
||||||
|
-- in the data directory, since inside a .game they are read only.
|
||||||
|
|
||||||
-- A game to start from: ground, a hero that runs and jumps, and a readout. Enough to press play
|
-- A game to start from: ground, a hero that runs and jumps, and a readout. Enough to press play
|
||||||
-- on, which is the point; everything in it can be changed or deleted.
|
-- on, which is the point; everything in it can be changed or deleted.
|
||||||
|
|
@ -4637,22 +4706,7 @@ function forgeTemplate()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-- Whether a .game file is a packed game rather than a description: the database's own header.
|
-- Every .forge description in a directory, sorted. A directory that is not there lists nothing.
|
||||||
local function packedGame(path)
|
|
||||||
local file = io.open(path, "rb")
|
|
||||||
local head
|
|
||||||
|
|
||||||
if file == nil then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
head = file:read(15)
|
|
||||||
file:close()
|
|
||||||
|
|
||||||
return head == "SQLite format 3"
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- Every .game description in a directory, sorted. A directory that is not there lists nothing.
|
|
||||||
local function descriptionsIn(directory)
|
local function descriptionsIn(directory)
|
||||||
local found = {}
|
local found = {}
|
||||||
|
|
||||||
|
|
@ -4660,10 +4714,8 @@ local function descriptionsIn(directory)
|
||||||
return found
|
return found
|
||||||
end
|
end
|
||||||
for name in lfs.dir(directory) do
|
for name in lfs.dir(directory) do
|
||||||
-- A description is Lua text; a packed game is a database with the same extension, and one
|
if string.sub(name, -6) == ".forge" then
|
||||||
-- released beside the descriptions is not offered as one.
|
found[#found + 1] = { name = string.sub(name, 1, -7), path = directory .. name }
|
||||||
if (string.sub(name, -5) == ".game") and not packedGame(directory .. name) then
|
|
||||||
found[#found + 1] = { name = string.sub(name, 1, -6), path = directory .. name }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.sort(found, function(a, b) return a.name < b.name end)
|
table.sort(found, function(a, b) return a.name < b.name end)
|
||||||
|
|
@ -4696,14 +4748,14 @@ local function freshPath(stem)
|
||||||
local base = singeGetDataPath() .. stem
|
local base = singeGetDataPath() .. stem
|
||||||
local n = 1
|
local n = 1
|
||||||
|
|
||||||
if lfs.attributes(base .. ".game") == nil then
|
if lfs.attributes(base .. ".forge") == nil then
|
||||||
return base .. ".game"
|
return base .. ".forge"
|
||||||
end
|
end
|
||||||
while lfs.attributes(base .. n .. ".game") ~= nil do
|
while lfs.attributes(base .. n .. ".forge") ~= nil do
|
||||||
n = n + 1
|
n = n + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
return base .. n .. ".game"
|
return base .. n .. ".forge"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,167 @@
|
||||||
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
-- Written by Forge/AuthorCompile.singe. Edit by hand or in the editor; either is fine.
|
||||||
return {
|
return {
|
||||||
|
layers = {
|
||||||
|
{ fov = 60, kind = "scene3d" },
|
||||||
|
{ kind = "overlay" }
|
||||||
|
},
|
||||||
|
rooms = {
|
||||||
|
{
|
||||||
|
entities = {
|
||||||
|
{ id = "readout", type = "readout", x = 12, y = 12 },
|
||||||
|
{ id = "floor", type = "floor", x = 0.0, y = -0.1, z = -20.0 },
|
||||||
|
{ id = "painting", type = "painting", x = 0.0, y = 6.0, z = -30.0 },
|
||||||
|
{ id = "pillar", type = "pillar", x = 0.0, y = 4.0, z = -22.0 },
|
||||||
|
{ id = "camera", type = "camera", x = 0.0, y = 1.6, z = 0.0 },
|
||||||
|
{ id = "gun", type = "gun", x = 0.0, y = 0.0, z = 0.0 }
|
||||||
|
},
|
||||||
|
name = "start",
|
||||||
|
tracks = {
|
||||||
|
{
|
||||||
|
key = "time",
|
||||||
|
name = "rail",
|
||||||
|
points = {
|
||||||
|
{
|
||||||
|
at = 0,
|
||||||
|
look = { 0, 1.2, -10 },
|
||||||
|
x = 0,
|
||||||
|
y = 1.6,
|
||||||
|
z = 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
at = 4,
|
||||||
|
look = { 0, 1.2, -16 },
|
||||||
|
stop = "gate",
|
||||||
|
x = 0,
|
||||||
|
y = 1.6,
|
||||||
|
z = -6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
at = 10,
|
||||||
|
look = { 2, 1.2, -24 },
|
||||||
|
stop = "pillar",
|
||||||
|
x = 2,
|
||||||
|
y = 1.6,
|
||||||
|
z = -14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
at = 14,
|
||||||
|
look = { 2, 1.2, -28 },
|
||||||
|
x = 2,
|
||||||
|
y = 1.6,
|
||||||
|
z = -18
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules = {
|
||||||
|
{
|
||||||
|
act = {
|
||||||
|
{ "setText", entity = "readout", text = "\"score \" .. score .. \" health \" .. health .. \" ammo \" .. gun.ammo" }
|
||||||
|
},
|
||||||
|
note = "The readout, every frame",
|
||||||
|
on = "frame"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
act = {
|
||||||
|
{ "spawn", type = "zombie", x = -2, y = 0, z = -14 },
|
||||||
|
{ "spawn", type = "zombie", x = 2, y = 0, z = -15 }
|
||||||
|
},
|
||||||
|
name = "gate",
|
||||||
|
note = "The gate: two come out of the dark",
|
||||||
|
on = "stopped",
|
||||||
|
when = {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
act = {
|
||||||
|
{ "spawn", type = "zombie", x = -1, y = 0, z = -23 },
|
||||||
|
{ "spawn", type = "zombie", x = 0, y = 0, z = -26 },
|
||||||
|
{ "spawn", type = "zombie", x = 4, y = 0, z = -24 }
|
||||||
|
},
|
||||||
|
name = "pillar",
|
||||||
|
note = "The pillar: three more, one from behind it",
|
||||||
|
on = "stopped",
|
||||||
|
when = {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
act = {
|
||||||
|
{ "setState", entity = "self", state = "walk" }
|
||||||
|
},
|
||||||
|
note = "A zombie walks the moment it is made",
|
||||||
|
on = "spawn",
|
||||||
|
type = "zombie",
|
||||||
|
when = {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
act = {
|
||||||
|
{ "damage", amount = 1, entity = "self" },
|
||||||
|
{ "addScore", amount = 100 },
|
||||||
|
{ "playSound", file = "Forge/kit/hit.wav" }
|
||||||
|
},
|
||||||
|
note = "A shot lands",
|
||||||
|
on = "hit",
|
||||||
|
type = "zombie",
|
||||||
|
when = {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
act = {
|
||||||
|
{ "setState", entity = "self", state = "attack" },
|
||||||
|
{ "timerStart", after = 1.5, entity = "self", name = "bite" }
|
||||||
|
},
|
||||||
|
note = "It reaches the camera and rears up",
|
||||||
|
on = "arrived",
|
||||||
|
type = "zombie",
|
||||||
|
when = {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
act = {
|
||||||
|
{ "addVar", amount = -1, name = "health" },
|
||||||
|
{ "flash", b = 40, g = 40, r = 255, seconds = 0.3 },
|
||||||
|
{ "shake", amount = 0.3, seconds = 0.4 },
|
||||||
|
{ "timerStart", after = 1.5, entity = "self", name = "bite" }
|
||||||
|
},
|
||||||
|
name = "bite",
|
||||||
|
note = "The bite lands unless it was killed in time",
|
||||||
|
on = "timer",
|
||||||
|
type = "zombie",
|
||||||
|
when = {
|
||||||
|
{ "inState", entity = "self", state = "attack" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
act = {
|
||||||
|
{ "pathNext", entity = "camera" }
|
||||||
|
},
|
||||||
|
note = "The stop is clear: move on",
|
||||||
|
on = "frame",
|
||||||
|
when = {
|
||||||
|
{ "waiting", entity = "camera" },
|
||||||
|
{ "test", expr = "count(\"zombie\") == 0" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
act = {
|
||||||
|
{ "say", seconds = 2, text = "\"YOU ARE DEAD\"" },
|
||||||
|
{ "gameOver" }
|
||||||
|
},
|
||||||
|
note = "Out of health",
|
||||||
|
on = "frame",
|
||||||
|
when = {
|
||||||
|
{ "test", expr = "health <= 0" },
|
||||||
|
{ "once", scope = "game", tag = "over" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
act = {
|
||||||
|
{ "say", seconds = 2, text = "\"STAGE CLEAR\"" }
|
||||||
|
},
|
||||||
|
note = "The end of the rail",
|
||||||
|
on = "railEnd",
|
||||||
|
when = {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = "The Dead Yard",
|
||||||
types = {
|
types = {
|
||||||
camera = {
|
camera = {
|
||||||
behaviours = {
|
behaviours = {
|
||||||
|
|
@ -47,169 +209,5 @@ return {
|
||||||
vars = { health = 2 }
|
vars = { health = 2 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
layers = {
|
|
||||||
{ fov = 60, kind = "scene3d" },
|
|
||||||
{ kind = "overlay" }
|
|
||||||
},
|
|
||||||
rooms = {
|
|
||||||
{
|
|
||||||
entities = {
|
|
||||||
{ id = "readout", type = "readout", x = 12, y = 12 },
|
|
||||||
{ id = "floor", type = "floor", x = 0.0, y = -0.1, z = -20.0 },
|
|
||||||
{ id = "painting", type = "painting", x = 0.0, y = 6.0, z = -30.0 },
|
|
||||||
{ id = "pillar", type = "pillar", x = 0.0, y = 4.0, z = -22.0 },
|
|
||||||
{ id = "camera", type = "camera", x = 0.0, y = 1.6, z = 0.0 },
|
|
||||||
{ id = "gun", type = "gun", x = 0.0, y = 0.0, z = 0.0 }
|
|
||||||
},
|
|
||||||
name = "start",
|
|
||||||
tracks = {
|
|
||||||
{
|
|
||||||
key = "time",
|
|
||||||
name = "rail",
|
|
||||||
points = {
|
|
||||||
{
|
|
||||||
at = 0,
|
|
||||||
look = { 0, 1.2, -10 },
|
|
||||||
stop = "nil",
|
|
||||||
x = 0,
|
|
||||||
y = 1.6,
|
|
||||||
z = 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
at = 4,
|
|
||||||
look = { 0, 1.2, -16 },
|
|
||||||
stop = "gate",
|
|
||||||
x = 0,
|
|
||||||
y = 1.6,
|
|
||||||
z = -6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
at = 10,
|
|
||||||
look = { 2, 1.2, -24 },
|
|
||||||
stop = "pillar",
|
|
||||||
x = 2,
|
|
||||||
y = 1.6,
|
|
||||||
z = -14
|
|
||||||
},
|
|
||||||
{
|
|
||||||
at = 14,
|
|
||||||
look = { 2, 1.2, -28 },
|
|
||||||
stop = "nil",
|
|
||||||
x = 2,
|
|
||||||
y = 1.6,
|
|
||||||
z = -18
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rules = {
|
|
||||||
{
|
|
||||||
act = {
|
|
||||||
{ "setText", entity = "readout", text = "\"score \" .. score .. \" health \" .. health .. \" ammo \" .. gun.ammo" }
|
|
||||||
},
|
|
||||||
note = "The readout, every frame",
|
|
||||||
on = "frame"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
act = {
|
|
||||||
{ "spawn", type = "zombie", x = -2, y = 0, z = -14 },
|
|
||||||
{ "spawn", type = "zombie", x = 2, y = 0, z = -15 }
|
|
||||||
},
|
|
||||||
name = "gate",
|
|
||||||
note = "The gate: two come out of the dark",
|
|
||||||
on = "stopped",
|
|
||||||
when = {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
act = {
|
|
||||||
{ "spawn", type = "zombie", x = -1, y = 0, z = -23 },
|
|
||||||
{ "spawn", type = "zombie", x = 0, y = 0, z = -26 },
|
|
||||||
{ "spawn", type = "zombie", x = 4, y = 0, z = -24 }
|
|
||||||
},
|
|
||||||
name = "pillar",
|
|
||||||
note = "The pillar: three more, one from behind it",
|
|
||||||
on = "stopped",
|
|
||||||
when = {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
act = {
|
|
||||||
{ "setState", entity = "self", state = "walk" }
|
|
||||||
},
|
|
||||||
type = "zombie",
|
|
||||||
note = "A zombie walks the moment it is made",
|
|
||||||
on = "spawn",
|
|
||||||
when = {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
act = {
|
|
||||||
{ "damage", amount = 1, entity = "self" },
|
|
||||||
{ "addScore", amount = 100 },
|
|
||||||
{ "playSound", file = "Forge/kit/hit.wav" }
|
|
||||||
},
|
|
||||||
type = "zombie",
|
|
||||||
note = "A shot lands",
|
|
||||||
on = "hit",
|
|
||||||
when = {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
act = {
|
|
||||||
{ "setState", entity = "self", state = "attack" },
|
|
||||||
{ "timerStart", after = 1.5, entity = "self", name = "bite" }
|
|
||||||
},
|
|
||||||
type = "zombie",
|
|
||||||
note = "It reaches the camera and rears up",
|
|
||||||
on = "arrived",
|
|
||||||
when = {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
act = {
|
|
||||||
{ "addVar", amount = -1, name = "health" },
|
|
||||||
{ "flash", b = 40, g = 40, r = 255, seconds = 0.3 },
|
|
||||||
{ "shake", amount = 0.3, seconds = 0.4 },
|
|
||||||
{ "timerStart", after = 1.5, entity = "self", name = "bite" }
|
|
||||||
},
|
|
||||||
type = "zombie",
|
|
||||||
name = "bite",
|
|
||||||
note = "The bite lands unless it was killed in time",
|
|
||||||
on = "timer",
|
|
||||||
when = {
|
|
||||||
{ "inState", entity = "self", state = "attack" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
act = {
|
|
||||||
{ "pathNext", entity = "camera" }
|
|
||||||
},
|
|
||||||
note = "The stop is clear: move on",
|
|
||||||
on = "frame",
|
|
||||||
when = {
|
|
||||||
{ "waiting", entity = "camera" },
|
|
||||||
{ "test", expr = "count(\"zombie\") == 0" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
act = {
|
|
||||||
{ "say", seconds = 2, text = "\"YOU ARE DEAD\"" },
|
|
||||||
{ "gameOver" }
|
|
||||||
},
|
|
||||||
note = "Out of health",
|
|
||||||
on = "frame",
|
|
||||||
when = {
|
|
||||||
{ "test", expr = "health <= 0" },
|
|
||||||
{ "once", scope = "game", tag = "over" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
act = {
|
|
||||||
{ "say", seconds = 2, text = "\"STAGE CLEAR\"" }
|
|
||||||
},
|
|
||||||
note = "The end of the rail",
|
|
||||||
on = "railEnd",
|
|
||||||
when = {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = "The Dead Yard",
|
|
||||||
vars = { health = 3, score = 0 }
|
vars = { health = 3, score = 0 }
|
||||||
}
|
}
|
||||||
|
|
@ -32,10 +32,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Every service tool draws through this one page: Singe/Tools.singe hands the menu a list of
|
<!-- Every service tool draws through this one page: Singe/Tools.singe hands the menu a list of
|
||||||
rows and Singe/MenuDocument.singe turns them into the divs below it. -->
|
rows and Singe/MenuDocument.singe turns them into the divs below it -- the rows into the
|
||||||
|
body, the key hints into the footer, and on the list of tools the chosen tool's note into
|
||||||
|
the column beside the list. -->
|
||||||
<div id="tool" class="tool hidden">
|
<div id="tool" class="tool hidden">
|
||||||
<h1 id="toolTitle"></h1>
|
<div id="toolCard" class="card">
|
||||||
<div id="toolBody"></div>
|
<h1 id="toolTitle"></h1>
|
||||||
|
<div id="toolColumns" class="columns">
|
||||||
|
<div id="toolBody"></div>
|
||||||
|
<div id="toolNote" class="hidden"></div>
|
||||||
|
</div>
|
||||||
|
<div id="toolKeys"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</rml>
|
</rml>
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,9 @@ local function toolRow(row)
|
||||||
elseif row.kind == "keys" then
|
elseif row.kind == "keys" then
|
||||||
return "<p class='keys'>" .. menuSpaces(row.text) .. "</p>"
|
return "<p class='keys'>" .. menuSpaces(row.text) .. "</p>"
|
||||||
elseif row.kind == "item" then
|
elseif row.kind == "item" then
|
||||||
return string.format("<div class='%s'>%s</div>", row.selected and "item selected" or "item", menuSpaces(row.text))
|
local detail = row.detail and ("<span class='detail'>" .. menuSpaces(tostring(row.detail)) .. "</span>") or ""
|
||||||
|
|
||||||
|
return string.format("<div class='%s'><span class='text'>%s</span>%s</div>", row.selected and "item selected" or "item", menuSpaces(row.text), detail)
|
||||||
elseif row.kind == "blank" then
|
elseif row.kind == "blank" then
|
||||||
return "<div class='blank'></div>"
|
return "<div class='blank'></div>"
|
||||||
end
|
end
|
||||||
|
|
@ -377,6 +379,11 @@ MENU_RENDER.tools = function()
|
||||||
-- of them quietly showing more. The style sheet can scroll this box, but nothing drives a
|
-- of them quietly showing more. The style sheet can scroll this box, but nothing drives a
|
||||||
-- scroll bar from a joystick: windowing is what actually keeps the chosen row on screen.
|
-- scroll bar from a joystick: windowing is what actually keeps the chosen row on screen.
|
||||||
local visible, more, below = toolsVisible(TOOL_ROWS, toolsCapacity())
|
local visible, more, below = toolsVisible(TOOL_ROWS, toolsCapacity())
|
||||||
|
-- The key hints go to the card's footer and a note to the column beside the list;
|
||||||
|
-- everything else is the body.
|
||||||
|
local keys = {}
|
||||||
|
local note = {}
|
||||||
|
|
||||||
if more then
|
if more then
|
||||||
rows[#rows + 1] = "<div class='sub'>...</div>"
|
rows[#rows + 1] = "<div class='sub'>...</div>"
|
||||||
end
|
end
|
||||||
|
|
@ -390,13 +397,22 @@ MENU_RENDER.tools = function()
|
||||||
if row.kind == "item" then
|
if row.kind == "item" then
|
||||||
seenItem = true
|
seenItem = true
|
||||||
end
|
end
|
||||||
rows[#rows + 1] = toolRow(row)
|
if row.kind == "keys" then
|
||||||
|
keys[#keys + 1] = toolRow(row)
|
||||||
|
elseif row.kind == "note" then
|
||||||
|
note[#note + 1] = toolRow(row)
|
||||||
|
else
|
||||||
|
rows[#rows + 1] = toolRow(row)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if below then
|
if below then
|
||||||
rows[#rows + 1] = "<div class='sub'>...</div>"
|
rows[#rows + 1] = "<div class='sub'>...</div>"
|
||||||
end
|
end
|
||||||
guiSetValue(GUI, DOCUMENT, "toolTitle", menuEscape(TOOL_TITLE))
|
guiSetValue(GUI, DOCUMENT, "toolTitle", menuEscape(TOOL_TITLE))
|
||||||
guiSetValue(GUI, DOCUMENT, "toolBody", table.concat(rows))
|
guiSetValue(GUI, DOCUMENT, "toolBody", table.concat(rows))
|
||||||
|
guiSetValue(GUI, DOCUMENT, "toolNote", table.concat(note))
|
||||||
|
guiSetValue(GUI, DOCUMENT, "toolKeys", table.concat(keys))
|
||||||
|
menuElement("toolNote"):SetClass("hidden", #note == 0)
|
||||||
menuElement("games"):SetClass("hidden", toolsActive())
|
menuElement("games"):SetClass("hidden", toolsActive())
|
||||||
menuElement("tool"):SetClass("hidden", not toolsActive())
|
menuElement("tool"):SetClass("hidden", not toolsActive())
|
||||||
menuElement("tool"):SetClass("clear", TOOL_CLEAR)
|
menuElement("tool"):SetClass("clear", TOOL_CLEAR)
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,6 @@
|
||||||
-- whoever does have a keyboard.
|
-- whoever does have a keyboard.
|
||||||
|
|
||||||
|
|
||||||
local MARGIN_X = 25
|
|
||||||
local MARGIN_Y = 25
|
|
||||||
local MARGIN = 10 -- Around the page, and between the regions on it.
|
local MARGIN = 10 -- Around the page, and between the regions on it.
|
||||||
local GAP = 8
|
local GAP = 8
|
||||||
local HINT_H = 24 -- The line of key hints along the bottom.
|
local HINT_H = 24 -- The line of key hints along the bottom.
|
||||||
|
|
@ -49,6 +47,24 @@ local VIDEO_ASPECT_H = 200
|
||||||
local LINE_HEIGHT = 24
|
local LINE_HEIGHT = 24
|
||||||
local LABEL_W = 150
|
local LABEL_W = 150
|
||||||
local SUB_INDENT = 20
|
local SUB_INDENT = 20
|
||||||
|
local CARD_PAD = 14 -- The card the tools draw on: inside its border to its text,
|
||||||
|
local TITLE_H = 36 -- its title bar, and its footer of key hints, one line each plus
|
||||||
|
local KEY_LINE = 18 -- its padding. The colours are the game page's: the panel, the
|
||||||
|
local KEY_PAD = 6 -- selection's gradient, the blue-grey border, the gold, the quiet
|
||||||
|
local BORDER = 2 -- blue-grey of a label.
|
||||||
|
local ITEM_INSET = 8
|
||||||
|
local CLEAR_FRAC = 0.70 -- How wide a strip a page that lets the overlay through may take.
|
||||||
|
local NOTE_FRAC = 0.42 -- How much of the card a note beside the list takes.
|
||||||
|
local PANEL = { 24, 38, 64, 216 }
|
||||||
|
local PANEL_DEEP = { 12, 20, 36, 216 }
|
||||||
|
local FOOT = { 16, 24, 40, 208 }
|
||||||
|
local TITLE_BAR = { 51, 80, 122, 255 }
|
||||||
|
local TITLE_BAR_L = { 74, 110, 168, 255 }
|
||||||
|
local EDGE = { 106, 134, 184, 255 }
|
||||||
|
local GOLD = { 255, 211, 90, 255 }
|
||||||
|
local INK = { 240, 240, 240, 255 }
|
||||||
|
local QUIET = { 143, 162, 196, 255 }
|
||||||
|
local SOFT = { 200, 210, 230, 255 }
|
||||||
local PAGE_ROWS = 10
|
local PAGE_ROWS = 10
|
||||||
local WRAP_BREAK = " [!wb!] "
|
local WRAP_BREAK = " [!wb!] "
|
||||||
local HINTS = "Up / Down: select Left / Right: page PgUp / PgDn: details Start or button: play Service: tools"
|
local HINTS = "Up / Down: select Left / Right: page PgUp / PgDn: details Start or button: play Service: tools"
|
||||||
|
|
@ -56,6 +72,7 @@ local HINTS = "Up / Down: select Left / Right: page PgUp / PgDn: det
|
||||||
local font = nil
|
local font = nil
|
||||||
local hintFont = nil -- Smaller: the hints are a footnote, and at the body's size the line
|
local hintFont = nil -- Smaller: the hints are a footnote, and at the body's size the line
|
||||||
-- of them ran off the end of a 720 pixel overlay.
|
-- of them ran off the end of a 720 pixel overlay.
|
||||||
|
local titleFont = nil -- Larger: a tool's title on its bar.
|
||||||
local introDone = false
|
local introDone = false
|
||||||
local videoX, videoY, videoW, videoH = 0, 0, 0, 0
|
local videoX, videoY, videoW, videoH = 0, 0, 0, 0
|
||||||
local marqueeX, marqueeY, marqueeW, marqueeH = 0, 0, 0, 0
|
local marqueeX, marqueeY, marqueeW, marqueeH = 0, 0, 0, 0
|
||||||
|
|
@ -146,6 +163,7 @@ end
|
||||||
-- rather than values because the audio delay tool inverts the whole page for a frame, and fontPrint
|
-- rather than values because the audio delay tool inverts the whole page for a frame, and fontPrint
|
||||||
-- takes the colour at the moment it draws -- unlike fontToSprite, which bakes it in.
|
-- takes the colour at the moment it draws -- unlike fontToSprite, which bakes it in.
|
||||||
local toolLines = {}
|
local toolLines = {}
|
||||||
|
local bodyX, bodyW = 0, 0 -- The card's body, where wrapped text is measured against.
|
||||||
|
|
||||||
|
|
||||||
local function toolLine(pieces)
|
local function toolLine(pieces)
|
||||||
|
|
@ -160,66 +178,120 @@ local function toolWrapped(text, x, role)
|
||||||
toolLine({})
|
toolLine({})
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
for _, line in ipairs(fitLines(text, overlayGetWidth() - MARGIN_X - x)) do
|
for _, line in ipairs(fitLines(text, bodyX + bodyW - x)) do
|
||||||
toolLine({ { x = x, text = line, role = role } })
|
toolLine({ { x = x, text = line, role = role } })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The card the tools draw on: its edges, and where its body and its columns begin. A page that
|
||||||
|
-- lets the overlay through has no card; its strip is measured once its lines are known.
|
||||||
|
local toolKeyLines = {} -- The key hints, for the card's footer.
|
||||||
|
local toolNoteLines = {} -- What the chosen tool is for, beside the list of tools.
|
||||||
|
local cardX, cardY, cardW, cardH = 0, 0, 0, 0
|
||||||
|
local noteX, noteW = 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
local function toolNote(pieces)
|
||||||
|
toolNoteLines[#toolNoteLines + 1] = pieces
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
local function toolsLayout()
|
local function toolsLayout()
|
||||||
local valueX = MARGIN_X + LABEL_W
|
local width = overlayGetWidth()
|
||||||
local wasKeys = false
|
local height = overlayGetHeight()
|
||||||
-- Only the rows that fit, with the chosen one kept in view; a catalogue is as long as the
|
-- Only the rows that fit, with the chosen one kept in view; a catalogue is as long as the
|
||||||
-- service is big and would otherwise run off the bottom of the screen.
|
-- service is big and would otherwise run off the bottom of the screen.
|
||||||
local rows, more, below = toolsVisible(TOOL_ROWS, toolsCapacity())
|
local rows, more, below = toolsVisible(TOOL_ROWS, toolsCapacity())
|
||||||
|
|
||||||
toolLines = {}
|
cardX, cardY, cardW, cardH = MARGIN, MARGIN, width - MARGIN * 2, height - MARGIN * 2
|
||||||
|
if TOOL_CLEAR then
|
||||||
|
cardW = math.floor(width * CLEAR_FRAC)
|
||||||
|
cardX = math.floor((width - cardW) / 2)
|
||||||
|
end
|
||||||
|
bodyX = cardX + BORDER + CARD_PAD
|
||||||
|
bodyW = cardW - (BORDER + CARD_PAD) * 2
|
||||||
|
local fullW = bodyW
|
||||||
|
-- A page with a note has a second column for it beside the list.
|
||||||
|
noteX, noteW = nil, 0
|
||||||
|
for _, row in ipairs(rows) do
|
||||||
|
if row.kind == "note" then
|
||||||
|
noteW = math.floor(cardW * NOTE_FRAC) - (BORDER + CARD_PAD) * 2
|
||||||
|
noteX = cardX + cardW - BORDER - CARD_PAD - noteW
|
||||||
|
bodyW = noteX - CARD_PAD - BORDER - bodyX
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local valueX = bodyX + LABEL_W
|
||||||
|
|
||||||
|
toolLines = {}
|
||||||
|
toolKeyLines = {}
|
||||||
|
toolNoteLines = {}
|
||||||
if more then
|
if more then
|
||||||
toolLine({ { x = MARGIN_X, text = " ...", role = "quiet" } })
|
toolLine({ { x = bodyX, text = " ...", role = "quiet" } })
|
||||||
end
|
end
|
||||||
local seenItem = false
|
local seenItem = false
|
||||||
for _, row in ipairs(rows) do
|
for _, row in ipairs(rows) do
|
||||||
-- The "more below" marker belongs immediately under the last item, not at the very bottom
|
-- The "more below" marker belongs immediately under the last item, not at the very bottom
|
||||||
-- of the page under the key hints, which are not part of the list.
|
-- of the page under the key hints, which are not part of the list.
|
||||||
if below and seenItem and row.kind ~= "item" then
|
if below and seenItem and row.kind ~= "item" then
|
||||||
toolLine({ { x = MARGIN_X, text = " ...", role = "quiet" } })
|
toolLine({ { x = bodyX, text = " ...", role = "quiet" } })
|
||||||
below = false
|
below = false
|
||||||
end
|
end
|
||||||
if row.kind == "item" then
|
if row.kind == "item" then
|
||||||
seenItem = true
|
seenItem = true
|
||||||
end
|
end
|
||||||
-- The key hints stand away from whatever the tool put above them, as they do in the
|
|
||||||
-- document, but sit together when a tool needs more than one line of them.
|
|
||||||
if row.kind == "keys" and not wasKeys then
|
|
||||||
toolLine({})
|
|
||||||
end
|
|
||||||
wasKeys = (row.kind == "keys")
|
|
||||||
|
|
||||||
if row.kind == "blank" then
|
if row.kind == "blank" then
|
||||||
toolLine({})
|
toolLine({})
|
||||||
elseif row.kind == "pair" then
|
elseif row.kind == "pair" then
|
||||||
-- The label and the first line of the value share a line; the rest of the value wraps
|
-- The label and the first line of the value share a line; the rest of the value wraps
|
||||||
-- under itself, in its own column.
|
-- under itself, in its own column.
|
||||||
local lines = fitLines(row.value, overlayGetWidth() - MARGIN_X - valueX)
|
local lines = fitLines(row.value, bodyX + bodyW - valueX)
|
||||||
toolLine({ { x = MARGIN_X, text = row.label, role = "quiet" },
|
toolLine({ { x = bodyX, text = row.label, role = "quiet" },
|
||||||
{ x = valueX, text = lines[1] or "", role = "ink" } })
|
{ x = valueX, text = lines[1] or "", role = "ink" } })
|
||||||
for index = 2, #lines do
|
for index = 2, #lines do
|
||||||
toolLine({ { x = valueX, text = lines[index], role = "ink" } })
|
toolLine({ { x = valueX, text = lines[index], role = "ink" } })
|
||||||
end
|
end
|
||||||
elseif row.kind == "sub" then
|
elseif row.kind == "sub" then
|
||||||
toolWrapped(row.text, MARGIN_X + SUB_INDENT, "quiet")
|
for _, line in ipairs(fitLines(row.text, bodyW - SUB_INDENT)) do
|
||||||
|
toolLine({ { x = bodyX + SUB_INDENT, text = line, role = "soft" } })
|
||||||
|
end
|
||||||
elseif row.kind == "keys" then
|
elseif row.kind == "keys" then
|
||||||
toolWrapped(row.text, MARGIN_X, "quiet")
|
-- The footer, in the smaller font, across the whole card.
|
||||||
|
fontSelect(hintFont)
|
||||||
|
for _, line in ipairs(fitLines(row.text, fullW)) do
|
||||||
|
toolKeyLines[#toolKeyLines + 1] = line
|
||||||
|
end
|
||||||
|
fontSelect(font)
|
||||||
elseif row.kind == "item" then
|
elseif row.kind == "item" then
|
||||||
toolLine({ { x = MARGIN_X, text = (row.selected and "> " or " ") .. row.text,
|
local pieces = { { x = bodyX + ITEM_INSET, text = row.text, role = row.selected and "pick" or "ink", bar = row.selected } }
|
||||||
role = row.selected and "pick" or "ink" } })
|
|
||||||
|
-- The detail against the right edge of the row, so a column of them lines up. An
|
||||||
|
-- empty one -- a field not filled in yet -- is nothing to measure or draw.
|
||||||
|
local detail = row.detail and tostring(row.detail) or ""
|
||||||
|
|
||||||
|
if detail ~= "" then
|
||||||
|
local measured = fontToSprite(detail)
|
||||||
|
|
||||||
|
pieces[#pieces + 1] = { x = bodyX + bodyW - ITEM_INSET - spriteGetWidth(measured), text = detail, role = pieces[1].role }
|
||||||
|
spriteUnload(measured)
|
||||||
|
end
|
||||||
|
toolLine(pieces)
|
||||||
|
elseif row.kind == "note" then
|
||||||
|
for _, line in ipairs(fitLines(row.text, noteW)) do
|
||||||
|
toolNote({ { x = noteX, text = line, role = "soft" } })
|
||||||
|
end
|
||||||
else
|
else
|
||||||
toolWrapped(row.text, MARGIN_X, "ink")
|
toolWrapped(row.text, bodyX, "ink")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- A list that runs to the very end of the page has no row after it to hang the marker on.
|
-- A list that runs to the very end of the page has no row after it to hang the marker on.
|
||||||
if below then
|
if below then
|
||||||
toolLine({ { x = MARGIN_X, text = " ...", role = "quiet" } })
|
toolLine({ { x = bodyX, text = " ...", role = "quiet" } })
|
||||||
|
end
|
||||||
|
-- A strip along the bottom is as tall as its lines.
|
||||||
|
if TOOL_CLEAR then
|
||||||
|
cardH = BORDER * 2 + TITLE_H + CARD_PAD + #toolLines * LINE_HEIGHT + CARD_PAD + KEY_PAD * 2 + #toolKeyLines * KEY_LINE
|
||||||
|
cardY = height - MARGIN - cardH
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -228,22 +300,21 @@ MENU_RENDER = {}
|
||||||
|
|
||||||
|
|
||||||
MENU_RENDER.begin = function()
|
MENU_RENDER.begin = function()
|
||||||
font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||||
hintFont = fontLoad("Singe/FreeSansBold.ttf", 13)
|
hintFont = fontLoad("Singe/FreeSansBold.ttf", 13)
|
||||||
|
titleFont = fontLoad("Singe/FreeSansBold.ttf", 22)
|
||||||
fontQuality(FONT_QUALITY_BLENDED)
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
fontSelect(font)
|
fontSelect(font)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
MENU_RENDER.finish = function()
|
MENU_RENDER.finish = function()
|
||||||
if font then
|
for _, handle in ipairs({ font, hintFont, titleFont }) do
|
||||||
fontUnload(font)
|
if handle then
|
||||||
font = nil
|
fontUnload(handle)
|
||||||
end
|
end
|
||||||
if hintFont then
|
|
||||||
fontUnload(hintFont)
|
|
||||||
hintFont = nil
|
|
||||||
end
|
end
|
||||||
|
font, hintFont, titleFont = nil, nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -489,17 +560,45 @@ MENU_RENDER.frame = function()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
MENU_RENDER.tools = function()
|
-- A filled rectangle: the overlay has no fill of its own, so it is lines, one a row. The colour
|
||||||
local y = MARGIN_Y
|
-- carries its alpha, which is what makes the card a panel over the moving grid and not a slab.
|
||||||
|
-- Given a second colour it runs from the first at the top to the second at the bottom: every
|
||||||
|
-- row is its own line already, so the gradient costs nothing more.
|
||||||
|
local function fillRect(x, y, w, h, colour, toColour)
|
||||||
|
local last = math.max(1, h - 1)
|
||||||
|
|
||||||
-- A tool that draws its own picture into the overlay has already done so, and the page has to
|
for row = 0, h - 1 do
|
||||||
-- let it through; every other tool gets a page painted over everything.
|
if toColour then
|
||||||
if not TOOL_CLEAR then
|
local t = row / last
|
||||||
if TOOL_FLASH then
|
|
||||||
colorBackground(255, 255, 255, 255)
|
colorForeground(math.floor(colour[1] + (toColour[1] - colour[1]) * t + 0.5),
|
||||||
else
|
math.floor(colour[2] + (toColour[2] - colour[2]) * t + 0.5),
|
||||||
colorBackground(0, 0, 0, 255)
|
math.floor(colour[3] + (toColour[3] - colour[3]) * t + 0.5),
|
||||||
|
math.floor(colour[4] + (toColour[4] - colour[4]) * t + 0.5))
|
||||||
|
elseif row == 0 then
|
||||||
|
colorForeground(colour[1], colour[2], colour[3], colour[4])
|
||||||
end
|
end
|
||||||
|
overlayLine(x, y + row, x + w - 1, y + row)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function ink(colour)
|
||||||
|
colorForeground(colour[1], colour[2], colour[3], colour[4])
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The page: the card with its title bar, the rows in their columns, the chosen row on its bar,
|
||||||
|
-- and the key hints in the footer. A page that draws its own picture gets a strip along the
|
||||||
|
-- bottom instead; the audio delay tool's flash is the whole screen white and nothing else.
|
||||||
|
MENU_RENDER.tools = function()
|
||||||
|
if TOOL_FLASH then
|
||||||
|
colorBackground(255, 255, 255, 255)
|
||||||
|
overlayClear()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not TOOL_CLEAR then
|
||||||
|
colorBackground(0, 0, 0, 0)
|
||||||
overlayClear()
|
overlayClear()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -509,29 +608,43 @@ MENU_RENDER.tools = function()
|
||||||
toolsLayout()
|
toolsLayout()
|
||||||
end
|
end
|
||||||
|
|
||||||
local ink = TOOL_FLASH and 0 or 255
|
local footH = KEY_PAD * 2 + #toolKeyLines * KEY_LINE
|
||||||
local quiet = TOOL_FLASH and 90 or 160
|
local footY = cardY + cardH - BORDER - footH
|
||||||
|
local y = cardY + BORDER + TITLE_H + CARD_PAD
|
||||||
|
|
||||||
-- A page that lets the overlay through gathers its text along the bottom, out of the way of
|
-- The card: the panel, its border twice for the width, the title bar, and the footer.
|
||||||
-- whatever the tool drew: the light gun's corner targets in particular.
|
fillRect(cardX, cardY, cardW, cardH, PANEL, PANEL_DEEP)
|
||||||
if TOOL_CLEAR then
|
fillRect(cardX + BORDER, cardY + BORDER, cardW - BORDER * 2, TITLE_H, TITLE_BAR_L, TITLE_BAR)
|
||||||
y = overlayGetHeight() - MARGIN_Y - (#toolLines + 1) * LINE_HEIGHT
|
fillRect(cardX + BORDER, cardY + BORDER + TITLE_H, cardW - BORDER * 2, BORDER, EDGE)
|
||||||
|
if #toolKeyLines > 0 then
|
||||||
|
fillRect(cardX + BORDER, footY, cardW - BORDER * 2, footH, FOOT)
|
||||||
|
fillRect(cardX + BORDER, footY - BORDER, cardW - BORDER * 2, BORDER, EDGE)
|
||||||
end
|
end
|
||||||
colorForeground(ink, ink, ink, 255)
|
ink(EDGE)
|
||||||
fontPrint(MARGIN_X, y, TOOL_TITLE)
|
overlayBox(cardX, cardY, cardX + cardW - 1, cardY + cardH - 1)
|
||||||
y = y + LINE_HEIGHT
|
overlayBox(cardX + 1, cardY + 1, cardX + cardW - 2, cardY + cardH - 2)
|
||||||
if not TOOL_CLEAR then
|
if noteX then
|
||||||
y = y + LINE_HEIGHT
|
fillRect(noteX - CARD_PAD - BORDER, cardY + BORDER + TITLE_H + BORDER, BORDER, footY - BORDER - (cardY + BORDER + TITLE_H + BORDER), EDGE)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
fontSelect(titleFont)
|
||||||
|
ink(GOLD)
|
||||||
|
fontPrint(cardX + BORDER + CARD_PAD, cardY + BORDER + math.floor((TITLE_H - 26) / 2), TOOL_TITLE)
|
||||||
|
fontSelect(font)
|
||||||
|
|
||||||
for _, line in ipairs(toolLines) do
|
for _, line in ipairs(toolLines) do
|
||||||
for _, piece in ipairs(line) do
|
for _, piece in ipairs(line) do
|
||||||
|
if piece.bar then
|
||||||
|
fillRect(bodyX, y - 3, bodyW, LINE_HEIGHT - 2, TITLE_BAR_L, TITLE_BAR)
|
||||||
|
end
|
||||||
if piece.role == "quiet" then
|
if piece.role == "quiet" then
|
||||||
colorForeground(quiet, quiet, quiet, 255)
|
ink(QUIET)
|
||||||
|
elseif piece.role == "soft" then
|
||||||
|
ink(SOFT)
|
||||||
elseif piece.role == "pick" then
|
elseif piece.role == "pick" then
|
||||||
colorForeground(255, 211, 90, 255)
|
ink(GOLD)
|
||||||
else
|
else
|
||||||
colorForeground(ink, ink, ink, 255)
|
ink(INK)
|
||||||
end
|
end
|
||||||
-- fontPrint refuses an empty string, and a tool legitimately produces one: a status
|
-- fontPrint refuses an empty string, and a tool legitimately produces one: a status
|
||||||
-- line with nothing to say yet is still a line, and it still takes up its height.
|
-- line with nothing to say yet is still a line, and it still takes up its height.
|
||||||
|
|
@ -541,4 +654,25 @@ MENU_RENDER.tools = function()
|
||||||
end
|
end
|
||||||
y = y + LINE_HEIGHT
|
y = y + LINE_HEIGHT
|
||||||
end
|
end
|
||||||
|
y = cardY + BORDER + TITLE_H + CARD_PAD
|
||||||
|
for _, line in ipairs(toolNoteLines) do
|
||||||
|
for _, piece in ipairs(line) do
|
||||||
|
ink(SOFT)
|
||||||
|
if piece.text ~= "" then
|
||||||
|
fontPrint(piece.x, y, piece.text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
y = y + LINE_HEIGHT
|
||||||
|
end
|
||||||
|
|
||||||
|
fontSelect(hintFont)
|
||||||
|
ink(QUIET)
|
||||||
|
y = footY + KEY_PAD
|
||||||
|
for _, line in ipairs(toolKeyLines) do
|
||||||
|
if line ~= "" then
|
||||||
|
fontPrint(bodyX, y, line)
|
||||||
|
end
|
||||||
|
y = y + KEY_LINE
|
||||||
|
end
|
||||||
|
fontSelect(font)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
-- tool leaves out simply does not happen.
|
-- tool leaves out simply does not happen.
|
||||||
--
|
--
|
||||||
-- Nothing here draws. A tool builds a list of rows with the toolsText, toolsPair, toolsSub,
|
-- Nothing here draws. A tool builds a list of rows with the toolsText, toolsPair, toolsSub,
|
||||||
-- toolsKeys, toolsItem and toolsBlank constructors and hands it to toolsShow; the menu that loaded
|
-- toolsKeys, toolsNote, toolsItem, and toolsBlank constructors and hands it to toolsShow; the menu that loaded
|
||||||
-- this file draws it whichever way it draws everything else. That is what lets the same ten tools
|
-- this file draws it whichever way it draws everything else. That is what lets the same ten tools
|
||||||
-- run over the RmlUi document and over the plain overlay on a machine with no GPU.
|
-- run over the RmlUi document and over the plain overlay on a machine with no GPU.
|
||||||
|
|
||||||
|
|
@ -78,8 +78,17 @@ function toolsKeys(text)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function toolsItem(text, selected)
|
-- A note beside the page's list rather than under it: what the chosen tool is for.
|
||||||
return { kind = "item", text = text, selected = selected }
|
function toolsNote(text)
|
||||||
|
return { kind = "note", text = text }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A line to choose between, with an optional detail the menu sets against the right edge of the
|
||||||
|
-- row -- a size, a state, a value. Never padded into the text with spaces: the menu's fonts are
|
||||||
|
-- proportional and nothing lines up that way.
|
||||||
|
function toolsItem(text, selected, detail)
|
||||||
|
return { kind = "item", text = text, selected = selected, detail = detail }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -96,11 +105,11 @@ function toolsShow(rows)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
-- How many rows of a page fit on screen.
|
-- How many rows of a page's body fit on screen.
|
||||||
--
|
--
|
||||||
-- Derived from the overlay, which is the size both renderers draw into, so the two show the same
|
-- Derived from the overlay, which is the size both renderers draw into, so the two show the same
|
||||||
-- thing rather than one of them quietly showing more. The subtraction is the title and the couple
|
-- thing rather than one of them quietly showing more. The subtraction is the card around the
|
||||||
-- of lines of key hints that live below every list.
|
-- body: its margins, its title bar, and the footer of key hints under every page.
|
||||||
function toolsCapacity()
|
function toolsCapacity()
|
||||||
return math.max(4, math.floor(overlayGetHeight() / TOOL_LINE_HEIGHT) - TOOL_CHROME_LINES)
|
return math.max(4, math.floor(overlayGetHeight() / TOOL_LINE_HEIGHT) - TOOL_CHROME_LINES)
|
||||||
end
|
end
|
||||||
|
|
@ -126,14 +135,21 @@ function toolsVisible(rows, capacity)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- The key hints and a note are drawn beside the body, not in it, so they take none of its room.
|
||||||
|
local body = 0
|
||||||
|
for _, row in ipairs(rows) do
|
||||||
|
if row.kind ~= "keys" and row.kind ~= "note" then
|
||||||
|
body = body + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
-- Nothing to scroll: a page with no list, or one whose list already fits.
|
-- Nothing to scroll: a page with no list, or one whose list already fits.
|
||||||
if #items == 0 or #rows <= capacity then
|
if #items == 0 or body <= capacity then
|
||||||
return rows, false, false
|
return rows, false, false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Keep the chosen row in the middle of the window where there is room either side of it, so
|
-- Keep the chosen row in the middle of the window where there is room either side of it, so
|
||||||
-- moving down does not pin it to the last line.
|
-- moving down does not pin it to the last line.
|
||||||
local room = math.max(1, capacity - (#rows - #items))
|
local room = math.max(1, capacity - (body - #items))
|
||||||
local first = math.max(1, math.min((chosen or 1) - math.floor(room / 2), #items - room + 1))
|
local first = math.max(1, math.min((chosen or 1) - math.floor(room / 2), #items - room + 1))
|
||||||
local last = math.min(#items, first + room - 1)
|
local last = math.min(#items, first + room - 1)
|
||||||
|
|
||||||
|
|
@ -191,8 +207,7 @@ function toolsShowList()
|
||||||
for i, tool in ipairs(TOOLS) do
|
for i, tool in ipairs(TOOLS) do
|
||||||
rows[#rows + 1] = toolsItem(tool.name, i == TOOL_SELECTED)
|
rows[#rows + 1] = toolsItem(tool.name, i == TOOL_SELECTED)
|
||||||
end
|
end
|
||||||
rows[#rows + 1] = toolsBlank()
|
rows[#rows + 1] = toolsNote(TOOLS[TOOL_SELECTED].help or "")
|
||||||
rows[#rows + 1] = toolsText(TOOLS[TOOL_SELECTED].help or "")
|
|
||||||
rows[#rows + 1] = toolsKeys("Up / Down: choose Button 1: open Service: back to the games")
|
rows[#rows + 1] = toolsKeys("Up / Down: choose Button 1: open Service: back to the games")
|
||||||
toolsShow(rows)
|
toolsShow(rows)
|
||||||
end
|
end
|
||||||
|
|
@ -414,7 +429,8 @@ TOOLS[#TOOLS + 1] = {
|
||||||
|
|
||||||
update = function()
|
update = function()
|
||||||
local rows = {}
|
local rows = {}
|
||||||
rows[#rows + 1] = toolsPair("Last switch", string.format("%s count: %d", inputLast, inputCount))
|
rows[#rows + 1] = toolsPair("Last switch", inputLast)
|
||||||
|
rows[#rows + 1] = toolsPair("Count", inputCount)
|
||||||
rows[#rows + 1] = toolsPair("Devices", string.format("%d gamepads, %d mice, stick drives mouse: %s",
|
rows[#rows + 1] = toolsPair("Devices", string.format("%d gamepads, %d mice, stick drives mouse: %s",
|
||||||
controllerHowMany(), mouseHowMany(), tostring(joyMouseIsEnabled())))
|
controllerHowMany(), mouseHowMany(), tostring(joyMouseIsEnabled())))
|
||||||
for slot = 0, SINGE_MAX_CONTROLLERS - 1 do
|
for slot = 0, SINGE_MAX_CONTROLLERS - 1 do
|
||||||
|
|
@ -432,8 +448,8 @@ TOOLS[#TOOLS + 1] = {
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
rows[#rows + 1] = toolsText(string.format("%d: %s", slot, controllerGetName(slot)))
|
rows[#rows + 1] = toolsText(string.format("%d: %s", slot, controllerGetName(slot)))
|
||||||
rows[#rows + 1] = toolsSub(string.format("axes %s buttons %s",
|
rows[#rows + 1] = toolsSub("axes " .. table.concat(axes, ", "))
|
||||||
table.concat(axes, ", "), (#buttons > 0) and table.concat(buttons, " ") or "-"))
|
rows[#rows + 1] = toolsSub("buttons " .. ((#buttons > 0) and table.concat(buttons, " ") or "-"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for m = 0, mouseHowMany() - 1 do
|
for m = 0, mouseHowMany() - 1 do
|
||||||
|
|
@ -494,7 +510,8 @@ local gunLast = "none yet"
|
||||||
|
|
||||||
local function gunShow()
|
local function gunShow()
|
||||||
toolsShow({
|
toolsShow({
|
||||||
toolsPair("Last shot", string.format("%s shots shown: %d", gunLast, #gunShots)),
|
toolsPair("Last shot", gunLast),
|
||||||
|
toolsPair("Shots shown", #gunShots),
|
||||||
toolsKeys("Button 1: fire Start: clear Button 2: back"),
|
toolsKeys("Button 1: fire Start: clear Button 2: back"),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
@ -642,8 +659,9 @@ local dispSaved = ""
|
||||||
local function displayShow()
|
local function displayShow()
|
||||||
local x, y = vldpGetShift()
|
local x, y = vldpGetShift()
|
||||||
toolsShow({
|
toolsShow({
|
||||||
toolsPair("Picture", string.format("scale %d%% shift %d, %d rotation %d",
|
toolsPair("Scale", vldpGetScale() .. "%"),
|
||||||
vldpGetScale(), x, y, vldpGetRotate())),
|
toolsPair("Shift", string.format("%d, %d", x, y)),
|
||||||
|
toolsPair("Rotation", vldpGetRotate()),
|
||||||
toolsPair("Kept", dispSaved),
|
toolsPair("Kept", dispSaved),
|
||||||
toolsKeys("Arrows: move Button 3 / 4: size Coin 1: rotate Start: reset"),
|
toolsKeys("Arrows: move Button 3 / 4: size Coin 1: rotate Start: reset"),
|
||||||
toolsKeys("Button 1: keep for this machine Button 2: back"),
|
toolsKeys("Button 1: keep for this machine Button 2: back"),
|
||||||
|
|
@ -833,7 +851,7 @@ local function savedShow()
|
||||||
rows[#rows + 1] = toolsText("No game has saved anything yet.")
|
rows[#rows + 1] = toolsText("No game has saved anything yet.")
|
||||||
end
|
end
|
||||||
for i, entry in ipairs(saveDirs) do
|
for i, entry in ipairs(saveDirs) do
|
||||||
rows[#rows + 1] = toolsItem(string.format("%s %d bytes", entry.name, entry.size), i == saveSelected)
|
rows[#rows + 1] = toolsItem(entry.name, i == saveSelected, entry.size .. " bytes")
|
||||||
end
|
end
|
||||||
rows[#rows + 1] = toolsKeys("Up / Down: choose Button 3: delete this game's save Button 2: back")
|
rows[#rows + 1] = toolsKeys("Up / Down: choose Button 3: delete this game's save Button 2: back")
|
||||||
toolsShow(rows)
|
toolsShow(rows)
|
||||||
|
|
@ -912,11 +930,12 @@ local function midiShow()
|
||||||
local rows = {}
|
local rows = {}
|
||||||
|
|
||||||
rows[#rows + 1] = toolsPair("Ports", string.format("%d in, %d out", midiInputCount(), midiOutputCount()))
|
rows[#rows + 1] = toolsPair("Ports", string.format("%d in, %d out", midiInputCount(), midiOutputCount()))
|
||||||
|
-- The port in use is starred.
|
||||||
for i = 0, midiOutputCount() - 1 do
|
for i = 0, midiOutputCount() - 1 do
|
||||||
rows[#rows + 1] = toolsSub(string.format("out %d%s %s", i, (i == midiOut) and " *" or "", midiOutputName(i)))
|
rows[#rows + 1] = toolsPair(string.format("Out %d%s", i, (i == midiOut) and " *" or ""), midiOutputName(i))
|
||||||
end
|
end
|
||||||
for i = 0, midiInputCount() - 1 do
|
for i = 0, midiInputCount() - 1 do
|
||||||
rows[#rows + 1] = toolsSub(string.format("in %d%s %s", i, (i == midiIn) and " *" or "", midiInputName(i)))
|
rows[#rows + 1] = toolsPair(string.format("In %d%s", i, (i == midiIn) and " *" or ""), midiInputName(i))
|
||||||
end
|
end
|
||||||
for _, m in ipairs(midiSeen) do
|
for _, m in ipairs(midiSeen) do
|
||||||
rows[#rows + 1] = toolsSub("received " .. m)
|
rows[#rows + 1] = toolsSub("received " .. m)
|
||||||
|
|
@ -1042,7 +1061,7 @@ local function accountShow()
|
||||||
if name == "server" and shown == "" then
|
if name == "server" and shown == "" then
|
||||||
shown = MASTER.url
|
shown = MASTER.url
|
||||||
end
|
end
|
||||||
rows[#rows + 1] = toolsItem(string.format("%-9s %s", name, shown), index == accountField)
|
rows[#rows + 1] = toolsItem(name, index == accountField, shown)
|
||||||
end
|
end
|
||||||
rows[#rows + 1] = toolsBlank()
|
rows[#rows + 1] = toolsBlank()
|
||||||
rows[#rows + 1] = toolsText(accountSaying)
|
rows[#rows + 1] = toolsText(accountSaying)
|
||||||
|
|
@ -1198,7 +1217,7 @@ local function shopShow()
|
||||||
elseif held ~= nil then
|
elseif held ~= nil then
|
||||||
state = (held >= game.version) and "installed" or ("update to " .. game.version)
|
state = (held >= game.version) and "installed" or ("update to " .. game.version)
|
||||||
end
|
end
|
||||||
rows[#rows + 1] = toolsItem(string.format("%-28s %s", game.title, state), index == shopSelected)
|
rows[#rows + 1] = toolsItem(game.title, index == shopSelected, state)
|
||||||
end
|
end
|
||||||
if #shopGames == 0 then
|
if #shopGames == 0 then
|
||||||
rows[#rows + 1] = toolsText("Nothing listed yet.")
|
rows[#rows + 1] = toolsText("Nothing listed yet.")
|
||||||
|
|
|
||||||
169
assets/menu.rcss
169
assets/menu.rcss
|
|
@ -123,8 +123,10 @@ body {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The service tools: one opaque page over everything, the same for all of them. The audio delay
|
/* The service tools: one page over everything, the backdrop still moving behind it, and on it
|
||||||
tool alone turns the page white for the frame of its flash, so the colours are named twice. */
|
a card in the game page's own dress -- the panel gradient, the blue-grey border, a title bar in
|
||||||
|
the selection's gradient with the title in the game list's gold, and the key hints in a footer
|
||||||
|
strip like the game page's. Nothing on it says black. */
|
||||||
.tool {
|
.tool {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
@ -132,60 +134,107 @@ body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: 25dp;
|
padding: 10dp;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: #000000;
|
display: flex;
|
||||||
color: #ffffff;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
color: #f0f0f0;
|
||||||
|
font-size: 15dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool .card {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #0a1220d8;
|
||||||
|
decorator: linear-gradient(180deg, #182640d8, #0c1424d8);
|
||||||
|
border: 2dp #6a86b8;
|
||||||
|
border-radius: 6dp;
|
||||||
|
box-shadow: 0 6dp 14dp #00000090;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The audio delay tool alone turns the whole screen white for the frame of its flash; the card
|
||||||
|
steps out of the way so the flash is the flash and nothing else. */
|
||||||
.tool.flash {
|
.tool.flash {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
color: #000000;
|
}
|
||||||
|
|
||||||
|
.tool.flash .card {
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool h1 {
|
.tool h1 {
|
||||||
font-size: 24dp;
|
font-size: 20dp;
|
||||||
color: #ffffff;
|
color: #ffd35a;
|
||||||
margin-bottom: 20dp;
|
margin: 0;
|
||||||
|
padding: 8dp 14dp;
|
||||||
|
background-color: #33507a;
|
||||||
|
decorator: linear-gradient(90deg, #4a6ea8, #33507a);
|
||||||
|
border-bottom: 2dp #6a86b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool.flash h1 {
|
.tool .columns {
|
||||||
color: #000000;
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The style sheet can scroll this box, but nothing drives a scroll bar from a joystick: the
|
||||||
|
windowing in Singe/Tools.singe is what keeps the chosen row on screen. */
|
||||||
|
.tool #toolBody {
|
||||||
|
flex: 1 1 0;
|
||||||
|
padding: 12dp 14dp;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The list of tools has a second column: what the chosen tool is for, beside the list. */
|
||||||
|
.tool #toolNote {
|
||||||
|
flex: 0 0 42%;
|
||||||
|
padding: 12dp 16dp;
|
||||||
|
border-left: 2dp #6a86b8;
|
||||||
|
color: #c8d2e6;
|
||||||
|
font-size: 14dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool #toolNote.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool #toolKeys {
|
||||||
|
padding: 6dp 14dp;
|
||||||
|
border-top: 2dp #6a86b8;
|
||||||
|
background-color: #101828d0;
|
||||||
|
color: #8fa2c4;
|
||||||
|
font-size: 13dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool p {
|
.tool p {
|
||||||
margin-bottom: 20dp;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool p.value {
|
/* The key hints, close together when a tool needs more than one line of them. */
|
||||||
margin-bottom: 4dp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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 {
|
.tool p.keys {
|
||||||
margin-top: 20dp;
|
margin: 0;
|
||||||
margin-bottom: 4dp;
|
|
||||||
color: #8fa2c4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool p.keys + p.keys {
|
.tool p.keys + p.keys {
|
||||||
margin-top: 0;
|
margin-top: 2dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The rows a tool fills in for itself: a heading row and an indented detail row under it. Text
|
/* 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
|
with nowhere to break -- a long path -- wraps mid-word rather than running off the edge, but an
|
||||||
ordinary word is left whole. */
|
ordinary word is left whole. */
|
||||||
.tool .row {
|
.tool .row {
|
||||||
margin-bottom: 2dp;
|
margin-bottom: 4dp;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool .sub {
|
.tool .sub {
|
||||||
margin-left: 20dp;
|
margin-left: 20dp;
|
||||||
margin-bottom: 2dp;
|
margin-bottom: 3dp;
|
||||||
font-size: 12dp;
|
font-size: 13dp;
|
||||||
color: #c8d2e6;
|
color: #c8d2e6;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
@ -194,11 +243,11 @@ body {
|
||||||
for the line wraps under itself instead of under the label. */
|
for the line wraps under itself instead of under the label. */
|
||||||
.tool .pair {
|
.tool .pair {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 2dp;
|
margin-bottom: 4dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool .pair .label {
|
.tool .pair .label {
|
||||||
flex: 0 0 110dp;
|
flex: 0 0 130dp;
|
||||||
color: #8fa2c4;
|
color: #8fa2c4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,13 +256,29 @@ body {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A line a tool offers to choose between, and the chosen one in the game list's gold -- the same
|
/* A line a tool offers to choose between, and the chosen one the way the game list shows its
|
||||||
way the game list itself shows a selection, since RCSS has no generated content to mark it with. */
|
selection: gold on the gradient bar. A detail the row carries -- a size, a state, a value --
|
||||||
|
sits against the right edge, so a column of them lines up whatever the text beside it. */
|
||||||
.tool .item {
|
.tool .item {
|
||||||
margin-bottom: 2dp;
|
display: flex;
|
||||||
|
padding: 3dp 8dp;
|
||||||
|
margin: 0 0 1dp -8dp;
|
||||||
|
border-radius: 3dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool .item .text {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool .item .detail {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-left: 16dp;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool .item.selected {
|
.tool .item.selected {
|
||||||
|
background-color: #33507a;
|
||||||
|
decorator: linear-gradient(90deg, #4a6ea8, #33507a);
|
||||||
color: #ffd35a;
|
color: #ffd35a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,31 +288,35 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Two tools draw their own picture into the overlay, which is underneath the document: the light
|
/* Two tools draw their own picture into the overlay, which is underneath the document: the light
|
||||||
gun's targets and the display's test pattern. Those pages let it through and gather their text
|
gun's targets and the display's test pattern. Those pages let it through, and the card becomes
|
||||||
into a strip along the bottom, between the two lower targets, so nothing worth aiming at is
|
a strip along the bottom between the two lower targets, so nothing worth aiming at is covered.
|
||||||
covered. */
|
Its width is fixed: left to fit its content, the value column of a pair had no room and broke
|
||||||
.tool.clear {
|
its words one letter to a line. */
|
||||||
background-color: transparent;
|
.tool.clear .card {
|
||||||
display: flex;
|
flex: 0 0 auto;
|
||||||
flex-direction: column;
|
margin-top: auto;
|
||||||
justify-content: flex-end;
|
align-self: center;
|
||||||
align-items: center;
|
width: 70%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool.clear h1 {
|
.tool.clear h1 {
|
||||||
font-size: 16dp;
|
font-size: 16dp;
|
||||||
margin-bottom: 4dp;
|
padding: 4dp 12dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool.clear h1,
|
/* The strip is as tall as its rows: the columns, which fill the page otherwise, take what they
|
||||||
.tool.clear .row,
|
need here. The body inside them keeps its flex, which in their row is its width; without it
|
||||||
.tool.clear .pair,
|
a pair's value column had no room. */
|
||||||
.tool.clear .sub,
|
.tool.clear .columns {
|
||||||
.tool.clear .item,
|
flex: 0 0 auto;
|
||||||
.tool.clear p {
|
}
|
||||||
background-color: #000000c0;
|
|
||||||
padding: 1dp 8dp;
|
.tool.clear #toolBody {
|
||||||
margin-bottom: 4dp;
|
padding: 6dp 12dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool.clear #toolKeys {
|
||||||
|
padding: 4dp 12dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The help under the list of tools, away from the list itself. */
|
/* The help under the list of tools, away from the list itself. */
|
||||||
|
|
|
||||||
BIN
assets/menuBackground.mkv
(Stored with Git LFS)
BIN
assets/menuBackground.mkv
(Stored with Git LFS)
Binary file not shown.
BIN
assets/menuIntro.flac
(Stored with Git LFS)
BIN
assets/menuIntro.flac
(Stored with Git LFS)
Binary file not shown.
BIN
assets/samples/dragonFireBreathAndRoar.flac
(Stored with Git LFS)
Normal file
BIN
assets/samples/dragonFireBreathAndRoar.flac
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/samples/explosionUltraBass.flac
(Stored with Git LFS)
Normal file
BIN
assets/samples/explosionUltraBass.flac
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/samples/largeFireball.flac
(Stored with Git LFS)
Normal file
BIN
assets/samples/largeFireball.flac
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -476,10 +476,10 @@ if((KANGAROO_OS STREQUAL "linux") AND (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux"))
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy ${forgeManual} ${forgeStage}/Forge/Forge.pdf
|
COMMAND ${CMAKE_COMMAND} -E copy ${forgeManual} ${forgeStage}/Forge/Forge.pdf
|
||||||
# The samples that need no art beyond what the engine ships, so the chooser has something to
|
# The samples that need no art beyond what the engine ships, so the chooser has something to
|
||||||
# open on a fresh install. They come from the test set, which is the one copy of them.
|
# open on a fresh install. They come from the test set, which is the one copy of them.
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/testScripts/author/platformer.game ${CMAKE_SOURCE_DIR}/testScripts/author/breakout.game ${CMAKE_SOURCE_DIR}/testScripts/author/qte.game ${CMAKE_SOURCE_DIR}/testScripts/author/fmv.game ${forgeStage}/Forge/
|
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/testScripts/author/platformer.forge ${CMAKE_SOURCE_DIR}/testScripts/author/breakout.forge ${CMAKE_SOURCE_DIR}/testScripts/author/qte.forge ${CMAKE_SOURCE_DIR}/testScripts/author/fmv.forge ${forgeStage}/Forge/
|
||||||
# Run it somewhere harmless: the engine writes Singe/ and data/ into the working directory.
|
# Run it somewhere harmless: the engine writes Singe/ and data/ into the working directory.
|
||||||
COMMAND ${CMAKE_COMMAND} -E chdir ${forgeStage} ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName} --pack ${forgeStage}/Forge ${forgePack}
|
COMMAND ${CMAKE_COMMAND} -E chdir ${forgeStage} ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName} --pack ${forgeStage}/Forge ${forgePack}
|
||||||
DEPENDS singe ${forgeSources} ${forgeManual} ${CMAKE_SOURCE_DIR}/testScripts/author/platformer.game ${CMAKE_SOURCE_DIR}/testScripts/author/breakout.game ${CMAKE_SOURCE_DIR}/testScripts/author/qte.game ${CMAKE_SOURCE_DIR}/testScripts/author/fmv.game
|
DEPENDS singe ${forgeSources} ${forgeManual} ${CMAKE_SOURCE_DIR}/testScripts/author/platformer.forge ${CMAKE_SOURCE_DIR}/testScripts/author/breakout.forge ${CMAKE_SOURCE_DIR}/testScripts/author/qte.forge ${CMAKE_SOURCE_DIR}/testScripts/author/fmv.forge
|
||||||
COMMENT "Packing Forge into .builddir/Forge.game"
|
COMMENT "Packing Forge into .builddir/Forge.game"
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
|
|
||||||
209
docs/Forge.adoc
209
docs/Forge.adoc
|
|
@ -121,12 +121,14 @@ types and the rest are explained in
|
||||||
* Give the ground a colour of its own.
|
* Give the ground a colour of its own.
|
||||||
* Drag the hero somewhere else with the mouse, and play again.
|
* Drag the hero somewhere else with the mouse, and play again.
|
||||||
|
|
||||||
**The description.** What kbd:[S] wrote, in full. `Forge/tutorials/01-tenMinutes.game`
|
**The description.** What kbd:[S] wrote, in full. A description is a
|
||||||
|
`.forge` file -- Lua text -- and never a `.game`, which is a packed release
|
||||||
|
the Singe menu lists. `Forge/tutorials/01-tenMinutes.forge`
|
||||||
in the chooser is this file.
|
in the chooser is this file.
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
include::../assets/Forge/tutorials/01-tenMinutes.game[]
|
include::../assets/Forge/tutorials/01-tenMinutes.forge[]
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Tutorial 2: A platformer
|
=== Tutorial 2: A platformer
|
||||||
|
|
@ -227,7 +229,7 @@ is the three types, their entities, the three rules, and the second room.
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
include::../assets/Forge/tutorials/02-platformer.game[]
|
include::../assets/Forge/tutorials/02-platformer.forge[]
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Tutorial 3: Sprites and sound
|
=== Tutorial 3: Sprites and sound
|
||||||
|
|
@ -307,7 +309,7 @@ states are in <<_instances_vars_and_states>> and
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
include::../assets/Forge/tutorials/03-sprites.game[]
|
include::../assets/Forge/tutorials/03-sprites.forge[]
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Tutorial 4: A shoot-em-up
|
=== Tutorial 4: A shoot-em-up
|
||||||
|
|
@ -408,7 +410,7 @@ runs once per instance of a type, with that instance as `self`, and a
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
include::../assets/Forge/tutorials/04-shmup.game[]
|
include::../assets/Forge/tutorials/04-shmup.forge[]
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Tutorial 5: A quick-time event
|
=== Tutorial 5: A quick-time event
|
||||||
|
|
@ -476,7 +478,7 @@ keeps a rule from firing every frame the window is open, and `keyPressed`
|
||||||
is the frame a key goes down, where `keyHeld` is every frame it is down.
|
is the frame a key goes down, where `keyHeld` is every frame it is down.
|
||||||
`setVar` and `test` on a var of your own are how a rule remembers what
|
`setVar` and `test` on a var of your own are how a rule remembers what
|
||||||
another did. The `branching` behaviour does all of this in one track for a
|
another did. The `branching` behaviour does all of this in one track for a
|
||||||
game of many branches; `Forge/fmv.game` in the chooser is one. In a
|
game of many branches; `Forge/fmv.forge` in the chooser is one. In a
|
||||||
released game, `games.dat` names the video, as it does for any Singe game.
|
released game, `games.dat` names the video, as it does for any Singe game.
|
||||||
|
|
||||||
**Try this.**
|
**Try this.**
|
||||||
|
|
@ -489,7 +491,7 @@ released game, `games.dat` names the video, as it does for any Singe game.
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
include::../assets/Forge/tutorials/05-qte.game[]
|
include::../assets/Forge/tutorials/05-qte.forge[]
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Tutorial 6: A light gun game
|
=== Tutorial 6: A light gun game
|
||||||
|
|
@ -575,7 +577,7 @@ with a light gun needs -- calibration, two guns, the recoil -- is in
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
include::../assets/Forge/tutorials/06-gun.game[]
|
include::../assets/Forge/tutorials/06-gun.forge[]
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Tutorial 7: A point-and-click adventure
|
=== Tutorial 7: A point-and-click adventure
|
||||||
|
|
@ -702,7 +704,7 @@ off. All of it is in
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
include::../assets/Forge/tutorials/07-adventure.game[]
|
include::../assets/Forge/tutorials/07-adventure.forge[]
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Tutorial 8: Into 3D
|
=== Tutorial 8: Into 3D
|
||||||
|
|
@ -797,7 +799,7 @@ viewport in <<_the_3d_viewport>>.
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
include::../assets/Forge/tutorials/08-scene.game[]
|
include::../assets/Forge/tutorials/08-scene.forge[]
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Tutorial 9: A rail shooter
|
=== Tutorial 9: A rail shooter
|
||||||
|
|
@ -916,7 +918,7 @@ in <<_games_in_the_scene>>; painted rooms in
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
include::../assets/Forge/tutorials/09-rail.game[]
|
include::../assets/Forge/tutorials/09-rail.forge[]
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Tutorial 10: Releasing your game
|
=== Tutorial 10: Releasing your game
|
||||||
|
|
@ -956,7 +958,7 @@ image::tutorials/10-arcadeRules.png[The arcade rules, 600]
|
||||||
. kbd:[S], then kbd:[X]: the game is *released* into a folder named for
|
. kbd:[S], then kbd:[X]: the game is *released* into a folder named for
|
||||||
it in Forge's data directory -- `CoinRun` -- and the message says where.
|
it in Forge's data directory -- `CoinRun` -- and the message says where.
|
||||||
In it: `CoinRun.singe`, the compiled game; `Author.singe`, the runtime
|
In it: `CoinRun.singe`, the compiled game; `Author.singe`, the runtime
|
||||||
it needs; `CoinRun.game`, the description, so it can be opened again;
|
it needs; `CoinRun.forge`, the description, so it can be opened again;
|
||||||
`games.dat`, so the menu lists it, naming the script as the menu will see
|
`games.dat`, so the menu lists it, naming the script as the menu will see
|
||||||
it, `CoinRun/CoinRun.singe`; and every file the description named, at the
|
it, `CoinRun/CoinRun.singe`; and every file the description named, at the
|
||||||
same relative name -- here the kit's hero, coins, and sounds under
|
same relative name -- here the kit's hero, coins, and sounds under
|
||||||
|
|
@ -989,7 +991,7 @@ is what a cabinet's coin door shows. Releasing is in
|
||||||
|
|
||||||
**Try this.**
|
**Try this.**
|
||||||
|
|
||||||
* Open `CoinRun.game` from the release folder in Forge -- drop it in the
|
* Open `CoinRun.forge` from the release folder in Forge -- drop it in the
|
||||||
chooser's data directory -- change something, and release again.
|
chooser's data directory -- change something, and release again.
|
||||||
* Two players: `players` `2`, a second hero on the other keys, and
|
* Two players: `players` `2`, a second hero on the other keys, and
|
||||||
`score2` and `lives2` for the bezel's other side.
|
`score2` and `lives2` for the bezel's other side.
|
||||||
|
|
@ -999,7 +1001,7 @@ is what a cabinet's coin door shows. Releasing is in
|
||||||
|
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
include::../assets/Forge/tutorials/10-release.game[]
|
include::../assets/Forge/tutorials/10-release.forge[]
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Recipes
|
=== Recipes
|
||||||
|
|
@ -1008,7 +1010,7 @@ The rest of the catalogue, a paragraph each, with a sample to open from the
|
||||||
chooser. A recipe names the behaviours; the tutorials above have shown how
|
chooser. A recipe names the behaviours; the tutorials above have shown how
|
||||||
each is typed.
|
each is typed.
|
||||||
|
|
||||||
*Racing* (`racing.game`). A `scene3d` room, a `vehicle` behaviour on a
|
*Racing* (`racing.forge`). A `scene3d` room, a `vehicle` behaviour on a
|
||||||
mesh -- `type` `car`, `motorcycle`, `tank`, or `boat`, with its `mass`,
|
mesh -- `type` `car`, `motorcycle`, `tank`, or `boat`, with its `mass`,
|
||||||
`thrust`, `torque`, and wheel sizes -- driven by `keys` through the vars `dy`
|
`thrust`, `torque`, and wheel sizes -- driven by `keys` through the vars `dy`
|
||||||
(throttle) and `dx` (steering), a `camera` in `follow` mode behind it, and
|
(throttle) and `dx` (steering), a `camera` in `follow` mode behind it, and
|
||||||
|
|
@ -1016,50 +1018,50 @@ opponents on `racer`, which drives a vehicle round a track of points on its
|
||||||
own. A `trigger` across the finish line, a `lap` var, and a `timer` for the
|
own. A `trigger` across the finish line, a `lap` var, and a `timer` for the
|
||||||
clock.
|
clock.
|
||||||
|
|
||||||
*Tower defence* (`towerdefence.game`). Creeps on `patrol` along a list of
|
*Tower defence* (`towerdefence.forge`). Creeps on `patrol` along a list of
|
||||||
points to the base, a `spawner` making waves of them, towers with `turret`
|
points to the base, a `spawner` making waves of them, towers with `turret`
|
||||||
-- `targets` the creep type, a `range`, a `rate`, and a `spawns` projectile
|
-- `targets` the creep type, a `range`, a `rate`, and a `spawns` projectile
|
||||||
or plain `damage` -- placed by `spawnAtPointer` on a click while the gold
|
or plain `damage` -- placed by `spawnAtPointer` on a click while the gold
|
||||||
var allows, and `patrolEnd` costing a life.
|
var allows, and `patrolEnd` costing a life.
|
||||||
|
|
||||||
*Pool and water* (`pool.game`). A `mesh` with the `water` behaviour is a
|
*Pool and water* (`pool.forge`). A `mesh` with the `water` behaviour is a
|
||||||
pool: bodies inside it float or sink by their `buoyancy`, and a `soft`
|
pool: bodies inside it float or sink by their `buoyancy`, and a `soft`
|
||||||
mesh is a cloth or a balloon. `body` gives anything mass, bounce, and
|
mesh is a cloth or a balloon. `body` gives anything mass, bounce, and
|
||||||
friction; `push` shoves it; `joint` hangs one body from another by a
|
friction; `push` shoves it; `joint` hangs one body from another by a
|
||||||
hinge, a ball, or a slider.
|
hinge, a ball, or a slider.
|
||||||
|
|
||||||
*Bowling* (`bowling.game`). Ten `body` pins and a ball pushed along the
|
*Bowling* (`bowling.forge`). Ten `body` pins and a ball pushed along the
|
||||||
lane, a `trigger` behind the pins that counts what falls through, and a
|
lane, a `trigger` behind the pins that counts what falls through, and a
|
||||||
`frame` rule that resets the rack with `moveTo` when the ball is past them.
|
`frame` rule that resets the rack with `moveTo` when the ball is past them.
|
||||||
|
|
||||||
*The maze* (`maze.game`). A `grid` look draws a map of tiles from a sheet
|
*The maze* (`maze.forge`). A `grid` look draws a map of tiles from a sheet
|
||||||
-- `columns` wide, `tile` pixels square, the `map` rows of tile numbers --
|
-- `columns` wide, `tile` pixels square, the `map` rows of tile numbers --
|
||||||
and `solid` walls are entities placed on it; `keys` and `mover` with
|
and `solid` walls are entities placed on it; `keys` and `mover` with
|
||||||
`clamp` walk the maze, a `timer` runs the clock, and `enter` on the exit
|
`clamp` walk the maze, a `timer` runs the clock, and `enter` on the exit
|
||||||
`trigger` ends it.
|
`trigger` ends it.
|
||||||
|
|
||||||
*Third person* (`thirdperson.game`). Tutorial 8's fox with an `orbit`
|
*Third person* (`thirdperson.forge`). Tutorial 8's fox with an `orbit`
|
||||||
camera: the mouse turns the view, the arrows walk relative to it, and
|
camera: the mouse turns the view, the arrows walk relative to it, and
|
||||||
`camera.eye` raises the viewpoint. `animator` with `attack` and `hit`
|
`camera.eye` raises the viewpoint. `animator` with `attack` and `hit`
|
||||||
clips, `health` on what it fights, and `animationDone` to return to `idle`.
|
clips, `health` on what it fights, and `animationDone` to return to `idle`.
|
||||||
|
|
||||||
*Space* (`space.game`). `thrust` pushes a body along its nose by `dy` and
|
*Space* (`space.forge`). `thrust` pushes a body along its nose by `dy` and
|
||||||
turns it by `dx` -- a ship without lift -- with `projectile`s that `aim`
|
turns it by `dx` -- a ship without lift -- with `projectile`s that `aim`
|
||||||
along their flight and a `spawner` of rocks that `drift`.
|
along their flight and a `spawner` of rocks that `drift`.
|
||||||
|
|
||||||
*Rhythm and MIDI* (`rhythm.game`). A game that opened a MIDI port hears
|
*Rhythm and MIDI* (`rhythm.forge`). A game that opened a MIDI port hears
|
||||||
`midi` events with `event.pitch` and `event.velocity`; a `timeBetween`
|
`midi` events with `event.pitch` and `event.velocity`; a `timeBetween`
|
||||||
window round each beat, and `once` per note, scores the hit. A `music`
|
window round each beat, and `once` per note, scores the hit. A `music`
|
||||||
layer plays the track from the start.
|
layer plays the track from the start.
|
||||||
|
|
||||||
*The 3D adventure* (`adventure3d.game`). Tutorial 7's rooms in a scene:
|
*The 3D adventure* (`adventure3d.forge`). Tutorial 7's rooms in a scene:
|
||||||
`walker` on the floors named in the room's `navFrom`, hotspots with a
|
`walker` on the floors named in the room's `navFrom`, hotspots with a
|
||||||
`walkX`, `walkY`, `walkZ`, a painted backdrop on an unlit mesh, and
|
`walkX`, `walkY`, `walkZ`, a painted backdrop on an unlit mesh, and
|
||||||
`occluder` boxes where the painting's furniture stands, so the hero walks
|
`occluder` boxes where the painting's furniture stands, so the hero walks
|
||||||
behind it. The verbs, the inventory, and the dialogues are exactly the flat
|
behind it. The verbs, the inventory, and the dialogues are exactly the flat
|
||||||
ones.
|
ones.
|
||||||
|
|
||||||
*The board and the bezel* (`fmv.game`). `bezel` round any game, `credit`
|
*The board and the bezel* (`fmv.forge`). `bezel` round any game, `credit`
|
||||||
on the coin switch, `restart` on start with credits, and `submitScore` at
|
on the coin switch, `restart` on start with credits, and `submitScore` at
|
||||||
the end; `branching` for a Dragon's Lair of windows, moves, and jumps.
|
the end; `branching` for a Dragon's Lair of windows, moves, and jumps.
|
||||||
|
|
||||||
|
|
@ -1139,7 +1141,7 @@ Compile it and run what comes out:
|
||||||
[source,lua]
|
[source,lua]
|
||||||
----
|
----
|
||||||
dofile("Forge/AuthorCompile.singe")
|
dofile("Forge/AuthorCompile.singe")
|
||||||
dofile(authorBuild("mygame.game", singeGetDataPath() .. "mygame.singe"))
|
dofile(authorBuild("mygame.forge", singeGetDataPath() .. "mygame.singe"))
|
||||||
----
|
----
|
||||||
|
|
||||||
`testScripts/author/` holds the worked examples -- a platformer, a QTE over
|
`testScripts/author/` holds the worked examples -- a platformer, a QTE over
|
||||||
|
|
@ -1273,7 +1275,7 @@ with `ragdoll` lets a model fall limp on death; `solid`, `body`, and
|
||||||
pointer, so the same `hit` and `miss` rules serve a light gun over video and
|
pointer, so the same `hit` and `miss` rules serve a light gun over video and
|
||||||
a rail shooter.
|
a rail shooter.
|
||||||
|
|
||||||
`testScripts/author/railshooter.game` is House of the Dead in a table: a
|
`testScripts/author/railshooter.forge` is House of the Dead in a table: a
|
||||||
rail with two stops, zombies that spawn there, walk to the camera, and bite
|
rail with two stops, zombies that spawn there, walk to the camera, and bite
|
||||||
on a timer unless they are killed, headshots through a zone on the head bone,
|
on a timer unless they are killed, headshots through a zone on the head bone,
|
||||||
and the rail moving on when the stop is clear.
|
and the rail moving on when the stop is clear.
|
||||||
|
|
@ -1298,13 +1300,73 @@ picture is drawn over what is higher, and `scaleBy = { { y = 300, scale = 0.6
|
||||||
smaller; looks with `anchor = "feet"` stand on their position rather than
|
smaller; looks with `anchor = "feet"` stand on their position rather than
|
||||||
being centred on it, which is what a walking character wants.
|
being centred on it, which is what a walking character wants.
|
||||||
|
|
||||||
|
*Overlay bits.* A `text` look may have a `font` file and `size` of its own
|
||||||
|
and an `anchor` -- `centre`, `feet`, or `top` -- instead of its top left; a
|
||||||
|
`sprite` look draws the var `copies` copies of itself `step` pixels apart, a
|
||||||
|
row of bullets or of hearts; the `pointer` behaviour keeps an instance on
|
||||||
|
the player's pointer, a crosshair, and hides it when the engine says a real
|
||||||
|
gun wants none drawn; `discPlay` and `discPause` let the disc go and hold
|
||||||
|
it, a still behind a title; `pointerX(1)` and `pointerY(1)` in an
|
||||||
|
expression say where the pointer is. The ActionMax way of judging a shot
|
||||||
|
over video -- the picture under the gun agreeing with a sensor spot across
|
||||||
|
two frames -- is a `lightSensor` behaviour those five games carry in a
|
||||||
|
vocabulary of their own (see <<own-vocabulary>>), not one of Forge's.
|
||||||
|
|
||||||
|
*A QTE game from the KarisFramework.* The `qte` behaviour plays a
|
||||||
|
laserdisc quick-time-event game as that framework does -- attract loop,
|
||||||
|
credits, levels of scenes with moves to answer in a window of frames, death
|
||||||
|
clips, continues, the dips -- from the framework's own tables under
|
||||||
|
`qte` in the description, which `util/forgePortKaris.lua` writes from a
|
||||||
|
game's script. The game's script and its `Script/addons.singe` run inside
|
||||||
|
the behaviour, so a game keeps its own scoring and level order. The
|
||||||
|
seventeen games of the test library and eleven of the Hypseus library are
|
||||||
|
ported this way and play as their originals do (FORGE.md section 14), and
|
||||||
|
so is LINEA, the one game on the MazescaterFramework -- that framework
|
||||||
|
with compound moves (sequences of two or three inputs, circles from any
|
||||||
|
start, mashes of a direction or a button, a direction with the second or
|
||||||
|
third button), which the behaviour judges from tables of its own once the
|
||||||
|
converter has marked the description with the framework's name. The
|
||||||
|
`kimmy` behaviour plays the same author's 2024 engine, whose moves are
|
||||||
|
judged by what is held together -- combos, mashes in three variants,
|
||||||
|
holds, sequences, paths, timed and yes/no branches -- with tilt, a life
|
||||||
|
bar, a die-and-retry count, a new-game menu, and a level select; the same
|
||||||
|
converter writes its description, and the seven Kimmy games are ported
|
||||||
|
with it. The `rdg` behaviour plays the map-mode lineage those frameworks
|
||||||
|
grew from (RDG's LUA SINGE 1.0 and 1.1): a game of that kind keeps its
|
||||||
|
levels, moves, death clips, order, and map as functions of its own
|
||||||
|
main.singe, and the behaviour runs those files as they are, beside the
|
||||||
|
description, over its own loop -- levels of segments, a move an input or
|
||||||
|
a mash or a pair of inputs in a window, a shot at a zone of the picture
|
||||||
|
where the game carries one, a map between levels -- with the copies'
|
||||||
|
drift (the later generation's level order, game type, and state numbers)
|
||||||
|
gated by what the game's script declares, and what a title does without
|
||||||
|
declaring it named in a `tweaks` table the converter writes. The
|
||||||
|
`timegal` behaviour plays the same author's Time Gal remake, a compact
|
||||||
|
loop of its own (checkpoints, a time-stop choice of three, a tally by
|
||||||
|
deaths, a life per quota, a level order by tiers), over the game's own
|
||||||
|
files in the same way; and the `sdq` behaviour plays Super Don Quixote,
|
||||||
|
the one hybrid of map-mode data under the Karis framework's judging,
|
||||||
|
with its difficulty select, level clear, and trophies.
|
||||||
|
|
||||||
|
*A hand-written game.* A game that was a program of its own rather than
|
||||||
|
a framework's tables -- the American Laser Games titles, Mad Dog McCree
|
||||||
|
first among them and the typing edition of Mad Dog II the ninth, and
|
||||||
|
Platoon, a prototype in their shape -- is ported as a vocabulary of its own beside
|
||||||
|
its description (see
|
||||||
|
<<own-vocabulary>>): a behaviour that is the game's loop rewritten in
|
||||||
|
Forge's terms, its screens and each level's flow, over the game's own
|
||||||
|
data files (its level setups, its hitbox tables, its hit tests, its
|
||||||
|
clips) loaded through the same shim the framework ports use. A press
|
||||||
|
carries the device it came from, so two guns are told apart, and the
|
||||||
|
hitbox behaviour's `step` keeps a box only on the frames a track keys.
|
||||||
|
|
||||||
*3D over a painting.* A room in the scene may be a picture on a flat `mesh`
|
*3D over a painting.* A room in the scene may be a picture on a flat `mesh`
|
||||||
(its `texture`, `unlit`) with 3D characters in front of it. Whatever stands
|
(its `texture`, `unlit`) with 3D characters in front of it. Whatever stands
|
||||||
in the picture -- a pillar, a doorway, a table -- that a character has to walk
|
in the picture -- a pillar, a doorway, a table -- that a character has to walk
|
||||||
behind is a `mesh` look with `occluder = true`: an invisible box that hides
|
behind is a `mesh` look with `occluder = true`: an invisible box that hides
|
||||||
what is behind it while the picture shows through it. The editor's 3D
|
what is behind it while the picture shows through it. The editor's 3D
|
||||||
viewport keeps occluders visible so they can be placed; the game hides them.
|
viewport keeps occluders visible so they can be placed; the game hides them.
|
||||||
`testScripts/author/painted.game` and scene 79 show a character crossing a
|
`testScripts/author/painted.forge` and scene 79 show a character crossing a
|
||||||
painted room behind such a pillar.
|
painted room behind such a pillar.
|
||||||
|
|
||||||
*Hotspots.* A type with `{ kind = "hotspot", name = "door", walkX = 600,
|
*Hotspots.* A type with `{ kind = "hotspot", name = "door", walkX = 600,
|
||||||
|
|
@ -1374,8 +1436,8 @@ slot, and `loadGame` brings it back. A save taken while a sequence runs
|
||||||
records the world as it stands; the sequence does not resume. `die` is the
|
records the world as it stands; the sequence does not resume. `die` is the
|
||||||
Sierra death: a line, a fade, and the game over from the start.
|
Sierra death: a line, a fade, and the game over from the start.
|
||||||
|
|
||||||
`testScripts/author/adventure2d.game` is the two-room adventure in a painted
|
`testScripts/author/adventure2d.forge` is the two-room adventure in a painted
|
||||||
room, and `adventure3d.game` is the same game in a modelled room from a fixed
|
room, and `adventure3d.forge` is the same game in a modelled room from a fixed
|
||||||
camera -- it takes the 2D description's rules, dialogue, verbs, and parser as
|
camera -- it takes the 2D description's rules, dialogue, verbs, and parser as
|
||||||
they are and supplies only types and rooms. Scenes 64 and 65 play them.
|
they are and supplies only types and rooms. Scenes 64 and 65 play them.
|
||||||
|
|
||||||
|
|
@ -1405,8 +1467,8 @@ body along its nose by `dy` and turns it by `dx` (a hovercraft, a ship);
|
||||||
or a slider through a point along an axis; and `push` shoves a body, or a
|
or a slider through a point along an axis; and `push` shoves a body, or a
|
||||||
ragdoll's bone.
|
ragdoll's bone.
|
||||||
|
|
||||||
`testScripts/author/racing.game` (a car, a rival on a track, checkpoint
|
`testScripts/author/racing.forge` (a car, a rival on a track, checkpoint
|
||||||
triggers, a hinged bar, a hovercraft) and `towerdefence.game` (creeps in
|
triggers, a hinged bar, a hovercraft) and `towerdefence.forge` (creeps in
|
||||||
waves on a lane, turrets placed by clicks, gold and lives) are the worked
|
waves on a lane, turrets placed by clicks, gold and lives) are the worked
|
||||||
examples; scenes 66 and 67 play them.
|
examples; scenes 66 and 67 play them.
|
||||||
|
|
||||||
|
|
@ -1416,14 +1478,17 @@ A `branches` track is Dragon's Lair: each branch is a window of disc frames,
|
||||||
the `move` (a key) or `switch` it wants, and the frames the disc goes to on
|
the `move` (a key) or `switch` it wants, and the frames the disc goes to on
|
||||||
`success` and on `fail`; a type with `{ kind = "branching", track = "moves" }`
|
`success` and on `fail`; a type with `{ kind = "branching", track = "moves" }`
|
||||||
watches it and raises `branchOpen` (with `event.move`), `branchTaken`, and
|
watches it and raises `branchOpen` (with `event.move`), `branchTaken`, and
|
||||||
`branchMissed`. Lives, credits, and continues are vars and rules -- `credit`
|
`branchMissed`. A branch with `mash = 6` wants the move six times before the
|
||||||
|
window closes, and one with `hold = 20` wants it held for twenty frames of
|
||||||
|
the window; `branchOpen` carries `event.mash` and `event.hold` so a prompt
|
||||||
|
can say so. Lives, credits, and continues are vars and rules -- `credit`
|
||||||
on `SWITCH_COIN1`, a `restart` on `SWITCH_START1` when `credits > 0` -- and a
|
on `SWITCH_COIN1`, a `restart` on `SWITCH_START1` when `credits > 0` -- and a
|
||||||
`bezel` layer shows `score`, `lives`, and `credits` (and a second player's
|
`bezel` layer shows `score`, `lives`, and `credits` (and a second player's
|
||||||
`score2` and `lives2` with `twin`) on the arcade panel around the picture.
|
`score2` and `lives2` with `twin`) on the arcade panel around the picture.
|
||||||
|
|
||||||
`gameOver` ends the rules, keeps the best score across runs, and shows the
|
`gameOver` ends the rules, keeps the best score across runs, and shows the
|
||||||
results card; `submitScore` sends the score to the master service's board
|
results card; `submitScore` sends the score to the master service's board
|
||||||
for the game, queued until it can go. `testScripts/author/fmv.game` is the
|
for the game, queued until it can go. `testScripts/author/fmv.forge` is the
|
||||||
worked example and scene 68 plays it: one window answered, three missed, the
|
worked example and scene 68 plays it: one window answered, three missed, the
|
||||||
lives spent, the score sent, a coin, and a continue.
|
lives spent, the score sent, a coin, and a continue.
|
||||||
|
|
||||||
|
|
@ -1464,7 +1529,7 @@ In the scene, a mesh look with `water` is filled with water: its top is the
|
||||||
surface, and a `body` with `buoyancy` floats in it; a `vehicle` of type
|
surface, and a `body` with `buoyancy` floats in it; a `vehicle` of type
|
||||||
`boat` with `thrust` is pushed by its propeller while that sits under the
|
`boat` with `thrust` is pushed by its propeller while that sits under the
|
||||||
surface; and `soft` makes a mesh look a cloth or an inflated body.
|
surface; and `soft` makes a mesh look a cloth or an inflated body.
|
||||||
`testScripts/author/tail.game` and `pool.game` are the worked examples;
|
`testScripts/author/tail.forge` and `pool.forge` are the worked examples;
|
||||||
scenes 70 and 71 play them.
|
scenes 70 and 71 play them.
|
||||||
|
|
||||||
=== The way out
|
=== The way out
|
||||||
|
|
@ -1496,6 +1561,73 @@ empty, is reported by the checker as *empty* and compiles to `nil`.
|
||||||
|
|
||||||
include::ForgeVocabulary.adoc[]
|
include::ForgeVocabulary.adoc[]
|
||||||
|
|
||||||
|
[[own-vocabulary]]
|
||||||
|
=== Your own vocabulary
|
||||||
|
|
||||||
|
A game can bring words of its own. A description may name a file of Lua --
|
||||||
|
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
vocabulary = "Vocabulary.singe",
|
||||||
|
----
|
||||||
|
|
||||||
|
-- kept beside the description, and everything that reads the description loads
|
||||||
|
that file before it reads the manifest: the editor when the game is opened
|
||||||
|
(and forgets it again when the game is closed, so the next game's panels do
|
||||||
|
not offer it), the compiler before it checks the description, and the built
|
||||||
|
game itself, which carries the file into a release like any other file the
|
||||||
|
description names and loads it from beside itself. Nothing in Forge changes;
|
||||||
|
the words exist while that game is open and nowhere else.
|
||||||
|
|
||||||
|
The file adds entries to the `AUTHOR` table exactly as `Author.singe` does
|
||||||
|
for its own -- a look, a behaviour, a condition, an action, a layer, or an
|
||||||
|
event -- and may define any Lua the entries emit or call. A behaviour's
|
||||||
|
`attach`, `step`, and `on` are called as Forge calls them; a condition's or
|
||||||
|
action's `emit` returns the Lua a rule compiles to. A behaviour that reads a
|
||||||
|
key or switch resolves the name with `authorKeyValue` or `authorSwitchValue`,
|
||||||
|
and one that wants the presses about nothing in particular -- a trigger --
|
||||||
|
sets `instance.hearsAll` and reads them in `on`. The five ActionMax games are
|
||||||
|
the working example: their `lightSensor` behaviour, which judges a shot the
|
||||||
|
way the emulator did by the disc's pixels under the gun, lives in a
|
||||||
|
`Vocabulary.singe` beside them and in no game that is not theirs.
|
||||||
|
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
-- A bob: the entity rises and falls on a sine wave about where it was put.
|
||||||
|
AUTHOR.behaviours.bob = {
|
||||||
|
help = "Rises and falls about its place.",
|
||||||
|
params = { height = "number", speed = "number" },
|
||||||
|
attach = function(instance, params)
|
||||||
|
local _, y = authorPosition(instance)
|
||||||
|
|
||||||
|
instance.bob = { height = params.height or 20, speed = params.speed or 2, base = y, at = 0 }
|
||||||
|
end,
|
||||||
|
step = function(instance, dt)
|
||||||
|
local bob = instance.bob
|
||||||
|
local x, _, z = authorPosition(instance)
|
||||||
|
|
||||||
|
bob.at = bob.at + dt * bob.speed
|
||||||
|
nodeSetPosition(instance.node, x, bob.base + math.sin(bob.at) * bob.height, z)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Whether an instance bobs: a condition of this game's own.
|
||||||
|
AUTHOR.conditions.bobbing = {
|
||||||
|
help = "An instance has a bob.",
|
||||||
|
params = { entity = "entity" },
|
||||||
|
emit = function(p) return "vocabBobbing(" .. p.entity .. ")" end
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function vocabBobbing(instance)
|
||||||
|
return (instance ~= nil) and (instance.bob ~= nil)
|
||||||
|
end
|
||||||
|
----
|
||||||
|
|
||||||
|
The game's form has a `vocabulary` field, so the file can be named from the
|
||||||
|
editor; naming it loads it at once, and the type and rule pickers offer what
|
||||||
|
it added.
|
||||||
|
|
||||||
== The Editor
|
== The Editor
|
||||||
|
|
||||||
`Forge/Forge.singe` edits a description, and it is itself a Singe game. It has
|
`Forge/Forge.singe` edits a description, and it is itself a Singe game. It has
|
||||||
|
|
@ -1522,7 +1654,7 @@ A script can drive the editor instead, which is how the test scenes do it:
|
||||||
----
|
----
|
||||||
FORGE_LIBRARY = true
|
FORGE_LIBRARY = true
|
||||||
dofile("Forge/Forge.singe")
|
dofile("Forge/Forge.singe")
|
||||||
forgeBegin("mygame.game")
|
forgeBegin("mygame.forge")
|
||||||
|
|
||||||
function onOverlayUpdate()
|
function onOverlayUpdate()
|
||||||
local x, y = mouseGetPosition(0)
|
local x, y = mouseGetPosition(0)
|
||||||
|
|
@ -1579,8 +1711,9 @@ it. In a 3D room `A` puts the new entity on the floor under the middle of
|
||||||
the viewport, and positions are world units to a tenth. `PgUp` and `PgDn`
|
the viewport, and positions are world units to a tenth. `PgUp` and `PgDn`
|
||||||
move between rooms; `N` adds a room. With nothing selected, `Enter` edits
|
move between rooms; `N` adds a room. With nothing selected, `Enter` edits
|
||||||
the game and then the room: the game's `title`, `players`, `vars` (as
|
the game and then the room: the game's `title`, `players`, `vars` (as
|
||||||
`score=0, lives=3`), `verbs`, and `layers` (a list of names, as `disc,
|
`score=0, lives=3`), `verbs`, `layers` (a list of names, as `disc,
|
||||||
overlay`) with each layer's own parameters as `disc.file` or `scene3d.fov`
|
overlay`) with each layer's own parameters as `disc.file` or `scene3d.fov`,
|
||||||
|
and `vocabulary` (a file of the game's own words, <<own-vocabulary>>)
|
||||||
-- a table parameter is typed as text: a map such as `scene3d.ambient` as
|
-- a table parameter is typed as text: a map such as `scene3d.ambient` as
|
||||||
`r=80, g=80, b=100`, a list of points such as `spawner.points` as
|
`r=80, g=80, b=100`, a list of points such as `spawner.points` as
|
||||||
`100,-20; 300,-20`, a list of records such as `target.zones` as
|
`100,-20; 300,-20`, a list of records such as `target.zones` as
|
||||||
|
|
@ -1737,7 +1870,7 @@ own, `CoinRun/CoinRun.singe`, which is how both the menu and a packed copy
|
||||||
address them), the description it was built from
|
address them), the description it was built from
|
||||||
so it can be opened again, **a copy of the runtime**, taken out of Forge, and
|
so it can be opened again, **a copy of the runtime**, taken out of Forge, and
|
||||||
**every file the description names** -- looks, sounds, music, the disc, models,
|
**every file the description names** -- looks, sounds, music, the disc, models,
|
||||||
a HUD document -- at the same relative name it had, so the kit's
|
a HUD document, the game's own vocabulary -- at the same relative name it had, so the kit's
|
||||||
`Forge/kit/hero.png` lands in `Forge/kit/` under the release. The engine's
|
`Forge/kit/hero.png` lands in `Forge/kit/` under the release. The engine's
|
||||||
own files (`Singe/...`) and absolute paths are left where they are. The
|
own files (`Singe/...`) and absolute paths are left where they are. The
|
||||||
compiled script notes its own directory in `AUTHOR_DIR`, and the runtime looks
|
compiled script notes its own directory in `AUTHOR_DIR`, and the runtime looks
|
||||||
|
|
|
||||||
|
|
@ -79,12 +79,12 @@
|
||||||
| `b` (number), `g` (number), `gravity` (number), `life` (number), `r` (number), `rate` (number), `size` (number), `speed` (number), `spread` (number)
|
| `b` (number), `g` (number), `gravity` (number), `life` (number), `r` (number), `rate` (number), `size` (number), `speed` (number), `spread` (number)
|
||||||
|
|
||||||
| `sprite`
|
| `sprite`
|
||||||
| An image, centred on the instance -- or standing on it, with anchor feet. frames splits a sheet into that many columns; the var frame picks one. The var angle turns it, starting from the entity's rz. faces says which way the art looks (right unless said, or none): while the var facing is the other way the image is mirrored, so art drawn one way walks both.
|
| An image, centred on the instance -- or standing on it, with anchor feet. frames splits a sheet into that many columns; the var frame picks one. The var angle turns it, starting from the entity's rz. faces says which way the art looks (right unless said, or none): while the var facing is the other way the image is mirrored, so art drawn one way walks both. The var copies draws it that many times in a row, step pixels apart (a row of bullets, of hearts): negative steps go left.
|
||||||
| `anchor` (feet), `faces` (right, left, none), `file` (file), `frames` (number)
|
| `anchor` (feet), `faces` (right, left, none), `file` (file), `frames` (number), `step` (number)
|
||||||
|
|
||||||
| `text`
|
| `text`
|
||||||
| A line of text, its top left at the instance. The var text is what it says.
|
| A line of text, its top left at the instance -- or centred on it, standing on it (feet), or hanging from it (top). The var text is what it says. A font file and size of its own, or the game's.
|
||||||
| `b` (number), `g` (number), `r` (number), `text` (string)
|
| `anchor` (centre, feet, top), `b` (number), `font` (file), `g` (number), `r` (number), `size` (number), `text` (string)
|
||||||
|
|
||||||
| `text3d`
|
| `text3d`
|
||||||
| A line of text standing in the scene. The var text is what it says.
|
| A line of text standing in the scene. The var text is what it says.
|
||||||
|
|
@ -107,7 +107,7 @@
|
||||||
| `bounce` (number), `buoyancy` (number), `friction` (number), `mass` (number), `shape` (box, sphere)
|
| `bounce` (number), `buoyancy` (number), `friction` (number), `mass` (number), `shape` (box, sphere)
|
||||||
|
|
||||||
| `branching`
|
| `branching`
|
||||||
| Dragon's Lair: a track of branches, each a disc frame window, the move it wants, and where the disc goes on success and on failure. Raises branchTaken and branchMissed.
|
| Dragon's Lair: a track of branches, each a disc frame window, the move it wants, and where the disc goes on success and on failure. A branch with mash wants the move that many times inside the window; one with hold wants it held that many frames. Raises branchOpen, branchTaken, and branchMissed.
|
||||||
| `track` (track)
|
| `track` (track)
|
||||||
|
|
||||||
| `camera`
|
| `camera`
|
||||||
|
|
@ -135,7 +135,7 @@
|
||||||
| `keep` (boolean), `linger` (number), `max` (number), `ragdoll` (boolean)
|
| `keep` (boolean), `linger` (number), `max` (number), `ragdoll` (boolean)
|
||||||
|
|
||||||
| `hitbox`
|
| `hitbox`
|
||||||
| The instance's hit shape comes from a track of boxes keyed by disc frame or time; between keys it is interpolated, outside them there is none.
|
| The instance's hit shape comes from a track of boxes keyed by disc frame or time; between keys it is interpolated, outside them there is none. A track with step = true keeps a box only on the frames it keys.
|
||||||
| `track` (track)
|
| `track` (track)
|
||||||
|
|
||||||
| `hotspot`
|
| `hotspot`
|
||||||
|
|
@ -150,6 +150,10 @@
|
||||||
| Reads a player's keys into the vars dx and dy (-1 to 1) and fire, so rules and movers read intent rather than hardware.
|
| Reads a player's keys into the vars dx and dy (-1 to 1) and fire, so rules and movers read intent rather than hardware.
|
||||||
| `down` (scancode), `fire` (scancode), `left` (scancode), `player` (number), `right` (scancode), `up` (scancode)
|
| `down` (scancode), `fire` (scancode), `left` (scancode), `player` (number), `right` (scancode), `up` (scancode)
|
||||||
|
|
||||||
|
| `kimmy`
|
||||||
|
| Plays a laserdisc quick-time-event game the Kimmy Script Engine way (Karis's 2024 framework), from the framework's own tables under game.qte: the attract loop, a new-game menu, levels of scenes with moves judged by what is held -- combos, mashes, holds, sequences, paths, timed and yes/no branches -- tilt, the life bar, the die-and-retry count, and the game's own add-ons. Written by util/forgePortKaris.lua from a game's script.
|
||||||
|
| --
|
||||||
|
|
||||||
| `mover`
|
| `mover`
|
||||||
| Moves by the vars dx and dy at a speed, kept on the screen when clamp is set. Pair it with keys.
|
| Moves by the vars dx and dy at a speed, kept on the screen when clamp is set. Pair it with keys.
|
||||||
| `clamp` (boolean), `speed` (number)
|
| `clamp` (boolean), `speed` (number)
|
||||||
|
|
@ -162,14 +166,30 @@
|
||||||
| Runs, falls, and jumps: the engine's character controller in 2D. Rules say which way with run and jump. Its point is its feet, so its look stands on it. Sets the var facing, and the var state to idle, walk, jump, or fall unless a rule set another.
|
| Runs, falls, and jumps: the engine's character controller in 2D. Rules say which way with run and jump. Its point is its feet, so its look stands on it. Sets the var facing, and the var state to idle, walk, jump, or fall unless a rule set another.
|
||||||
| `jump` (number), `speed` (number)
|
| `jump` (number), `speed` (number)
|
||||||
|
|
||||||
|
| `pointer`
|
||||||
|
| Follows a player's pointer: a crosshair, a hand. Hidden when the engine says a real gun wants no crosshair drawn.
|
||||||
|
| `player` (number)
|
||||||
|
|
||||||
| `projectile`
|
| `projectile`
|
||||||
| Flies at a velocity and is dropped past the edge (or far away, in 3D) or after life seconds. With aim, a sprite turns to point the way it flies.
|
| Flies at a velocity and is dropped past the edge (or far away, in 3D) or after life seconds. With aim, a sprite turns to point the way it flies.
|
||||||
| `aim` (boolean), `life` (number), `vx` (number), `vy` (number), `vz` (number)
|
| `aim` (boolean), `life` (number), `vx` (number), `vy` (number), `vz` (number)
|
||||||
|
|
||||||
|
| `qte`
|
||||||
|
| Plays a laserdisc quick-time-event game the KarisFramework way (or the MazescaterFramework's, with its compound moves), from the framework's own tables under game.qte: attract, credits, levels of scenes with moves to answer in a window of frames, death clips, continues, the dips, and the game's own add-ons. Written by util/forgePortKaris.lua from a game's script.
|
||||||
|
| --
|
||||||
|
|
||||||
| `racer`
|
| `racer`
|
||||||
| Drives a vehicle round a track of points on its own: steering toward the next, throttling by how straight the road is.
|
| Drives a vehicle round a track of points on its own: steering toward the next, throttling by how straight the road is.
|
||||||
| `reach` (number), `throttle` (number), `track` (track)
|
| `reach` (number), `throttle` (number), `track` (track)
|
||||||
|
|
||||||
|
| `rdg`
|
||||||
|
| Plays a laserdisc quick-time-event game of RDG's map-mode lineage (the ancestor of the Karis framework): levels of segments, a move an input or a mash or a pair of inputs in a window, a map between levels. The game's own files are loaded and its level, move, death-clip, order, and map functions run as they are; the loop is this. Written by util/forgePortKaris.lua from a game's script.
|
||||||
|
| --
|
||||||
|
|
||||||
|
| `sdq`
|
||||||
|
| Plays Super Don Quixote: map-mode levels, scenes, death clips, order, and map from the game's own files, judged as the Karis 3.31c framework judges, with its difficulty select, level clear, finish, and trophies. Written by util/forgePortKaris.lua from the game's script.
|
||||||
|
| --
|
||||||
|
|
||||||
| `seek`
|
| `seek`
|
||||||
| Moves straight toward a target -- an entity's id, or "camera" -- and raises arrived within stopAt of it.
|
| Moves straight toward a target -- an entity's id, or "camera" -- and raises arrived within stopAt of it.
|
||||||
| `fly` (boolean), `speed` (number), `stopAt` (number), `target` (string)
|
| `fly` (boolean), `speed` (number), `stopAt` (number), `target` (string)
|
||||||
|
|
@ -187,8 +207,8 @@
|
||||||
| `moving` (boolean)
|
| `moving` (boolean)
|
||||||
|
|
||||||
| `sound`
|
| `sound`
|
||||||
| A clip per event: spawn, hit, death, or any event the instance sees. Values are file names. volume is the clips' own, 0 to 100, under the game's.
|
| A clip per event: spawn, hit, miss, death, fire (a gun's trigger), or any event the instance sees. Values are file names. volume is the clips' own, 0 to 100, under the game's.
|
||||||
| `death` (file), `hit` (file), `pressed` (file), `spawn` (file), `volume` (number)
|
| `death` (file), `fire` (file), `hit` (file), `miss` (file), `pressed` (file), `spawn` (file), `volume` (number)
|
||||||
|
|
||||||
| `spawner`
|
| `spawner`
|
||||||
| Makes instances of a type every so many seconds at its own position or at one of its points, up to max alive at once and total in all.
|
| Makes instances of a type every so many seconds at its own position or at one of its points, up to max alive at once and total in all.
|
||||||
|
|
@ -206,6 +226,10 @@
|
||||||
| A body pushed along its nose by the var dy and turned by dx: a hovercraft, a ship, a plane without lift.
|
| A body pushed along its nose by the var dy and turned by dx: a hovercraft, a ship, a plane without lift.
|
||||||
| `force` (number), `turn` (number)
|
| `force` (number), `turn` (number)
|
||||||
|
|
||||||
|
| `timegal`
|
||||||
|
| Plays RDG2010's Time Gal (Singe Edition): levels of moves from a difficulty array (mirrored by a random draw), checkpoints, a time-stop choice of three, a tally by deaths, a life per quota, and a level order by tiers, at random, or in sequence. The game's own files are loaded and its data and order functions run as they are; the loop is this. Written by util/forgePortKaris.lua from the game's script.
|
||||||
|
| --
|
||||||
|
|
||||||
| `timer`
|
| `timer`
|
||||||
| Raises the timer event with its name after some seconds, or every so many. start false waits for timerStart.
|
| Raises the timer event with its name after some seconds, or every so many. start false waits for timerStart.
|
||||||
| `after` (number), `every` (number), `name` (string), `start` (boolean)
|
| `after` (number), `every` (number), `name` (string), `start` (boolean)
|
||||||
|
|
@ -251,7 +275,7 @@
|
||||||
| --
|
| --
|
||||||
|
|
||||||
| `branchOpen`
|
| `branchOpen`
|
||||||
| A branch's window opened; event.move says what it wants.
|
| A branch's window opened; event.move says what it wants, event.mash how many times, event.hold for how many frames.
|
||||||
| --
|
| --
|
||||||
|
|
||||||
| `branchTaken`
|
| `branchTaken`
|
||||||
|
|
@ -477,6 +501,16 @@
|
||||||
| `text` (expression)
|
| `text` (expression)
|
||||||
| yes
|
| yes
|
||||||
|
|
||||||
|
| `discPause`
|
||||||
|
| Hold the disc on its frame: a still behind a title.
|
||||||
|
| --
|
||||||
|
|
|
||||||
|
|
||||||
|
| `discPlay`
|
||||||
|
| Play the disc from where it stands.
|
||||||
|
| --
|
||||||
|
|
|
||||||
|
|
||||||
| `discTo`
|
| `discTo`
|
||||||
| Send the disc to a frame -- the branch a QTE takes.
|
| Send the disc to a frame -- the branch a QTE takes.
|
||||||
| `frame` (number)
|
| `frame` (number)
|
||||||
|
|
|
||||||
|
|
@ -563,7 +563,7 @@ name and any extension FFmpeg can demux, then for a `.txt` framefile.
|
||||||
| `--altaudio=SUFFIX` | Play `<base><SUFFIX>.ogg` beside the disc's video instead of the audio inside it, for a release whose other languages ship as separate files: `--altaudio=-es` next to `lair.m2v` plays `lair-es.ogg`. Every segment of a framefile is switched together. A file that is not there leaves the game's own audio playing and prints a warning. The `AUDIO_SUFFIX` key in `games.dat` does the same for one game, and a script changes it while running with `discAudioSuffix`. Default: none.
|
| `--altaudio=SUFFIX` | Play `<base><SUFFIX>.ogg` beside the disc's video instead of the audio inside it, for a release whose other languages ship as separate files: `--altaudio=-es` next to `lair.m2v` plays `lair-es.ogg`. Every segment of a framefile is switched together. A file that is not there leaves the game's own audio playing and prints a warning. The `AUDIO_SUFFIX` key in `games.dat` does the same for one game, and a script changes it while running with `discAudioSuffix`. Default: none.
|
||||||
| `--apiversion` | Print one machine readable line describing this build to standard output and exit, for front ends. See <<apiversion,The Version Line>>. Nothing else is printed.
|
| `--apiversion` | Print one machine readable line describing this build to standard output and exit, for front ends. See <<apiversion,The Version Line>>. Nothing else is printed.
|
||||||
| `--deinterlace=MODE` | What an interlaced picture gets: `auto` (the default) deinterlaces only the frames the file marks interlaced, `on` deinterlaces every frame for a file whose flags are wrong, and `off` never touches the picture. A laserdisc held interlaced fields and a rip that kept them combs on a progressive display; a rip that was deinterlaced when it was made needs nothing here. One picture comes out for each one that goes in, so frame numbers never move. Default: `auto`.
|
| `--deinterlace=MODE` | What an interlaced picture gets: `auto` (the default) deinterlaces only the frames the file marks interlaced, `on` deinterlaces every frame for a file whose flags are wrong, and `off` never touches the picture. A laserdisc held interlaced fields and a rip that kept them combs on a progressive display; a rip that was deinterlaced when it was made needs nothing here. One picture comes out for each one that goes in, so frame numbers never move. Default: `auto`.
|
||||||
| `--deterministic[=MS]` | For testing only: ignore real time and run the whole engine on a virtual clock that moves `MS` milliseconds every frame, so the same frame number always means the same moment. The disc steps one video frame a frame with it, and the random generators are seeded from the same number, so a run repeats to the pixel. The value is optional and is both the step and the seed, `1` to `1000`. Default when given without one: `15`, the frame time the engine's own rate implies. Audio and pacing are meaningless in this mode; see <<deterministic,Deterministic Test Mode>>. Default: off.
|
| `--deterministic[=MS]` | For testing only: ignore real time and run the whole engine on a virtual clock that moves `MS` milliseconds every frame, so the same frame number always means the same moment. The disc follows the virtual clock too, its frames lasting the time they are worth, and the random generators are seeded from the same number, so a run repeats to the pixel. The value is optional and is both the step and the seed, `1` to `1000`. Default when given without one: `15`, the frame time the engine's own rate implies. Audio and pacing are meaningless in this mode; see <<deterministic,Deterministic Test Mode>>. Default: off.
|
||||||
| `--fvalue=NUMBER` | One number handed from the launcher to the game, which reads it with `getFValue()`. Singe does nothing with it. `0` to `100000`, kept to three decimals as Hypseus keeps it. Default: `0`.
|
| `--fvalue=NUMBER` | One number handed from the launcher to the game, which reads it with `getFValue()`. Singe does nothing with it. `0` to `100000`, kept to three decimals as Hypseus keeps it. Default: `0`.
|
||||||
| `--gamepad_reorder=DIGITS` | Which physical pad fills which gamepad slot, as enumeration positions counting from `0`, one for each slot in turn: `--gamepad_reorder=10` makes the second pad found player one and the first player two. Written as bare digits (`3210`) or separated by commas or spaces, as Hypseus writes it; a repeated position is refused. Positions not named fill the slots that are left, in the order SDL found them, and a position with no pad behind it leaves its slot empty. Default: SDL's own order.
|
| `--gamepad_reorder=DIGITS` | Which physical pad fills which gamepad slot, as enumeration positions counting from `0`, one for each slot in turn: `--gamepad_reorder=10` makes the second pad found player one and the first player two. Written as bare digits (`3210`) or separated by commas or spaces, as Hypseus writes it; a repeated position is refused. Positions not named fill the slots that are left, in the order SDL found them, and a position with no pad behind it leaves its slot empty. Default: SDL's own order.
|
||||||
| `--haptic=STEP` | The strongest rumble step a game may use, `0` to `4`. `0` turns rumble off altogether, and a lower number quietens a game that asks for more: `controllerDoRumble` never rumbles harder than this. Default: `4`, which is every step a game asks for.
|
| `--haptic=STEP` | The strongest rumble step a game may use, `0` to `4`. `0` turns rumble off altogether, and a lower number quietens a game that asks for more: `controllerDoRumble` never rumbles harder than this. Default: `4`, which is every step a game asks for.
|
||||||
|
|
@ -930,10 +930,14 @@ ignore real time altogether: instead of asking the machine what time it is, the
|
||||||
engine runs on a virtual clock that moves a fixed step -- 15 milliseconds by
|
engine runs on a virtual clock that moves a fixed step -- 15 milliseconds by
|
||||||
default -- exactly once a frame. Everything paced by time follows that clock:
|
default -- exactly once a frame. Everything paced by time follows that clock:
|
||||||
physics steps, sprite and model animation, particles, navigation, the GUI's
|
physics steps, sprite and model animation, particles, navigation, the GUI's
|
||||||
animations and transitions, `os.clock()` and `singeGetTicks()`. The disc follows
|
animations and transitions, `os.clock()`, `singeGetTicks()`, and a bare
|
||||||
it too: while it is playing it advances exactly one video frame for every frame
|
`os.time()` (a game that seeds its generator from the wall clock when a game
|
||||||
the engine draws, from wherever it was last parked, and a paused or searched
|
starts seeds it from the virtual one, so it draws the same numbers every run;
|
||||||
disc stays where it was put. Lua's generator and the engine's own are seeded
|
`os.time` given a date table converts it as Lua always has). The disc follows
|
||||||
|
it too: while it is playing, each video frame stays on for as many engine frames
|
||||||
|
as its presentation time is worth on that clock (two or three at the default
|
||||||
|
step for a thirty-frame disc), as in real play, and a paused or searched disc
|
||||||
|
stays where it was put. Lua's generator and the engine's own are seeded
|
||||||
from a fixed number. Nothing waits for anything, so the game runs as fast as the machine
|
from a fixed number. Nothing waits for anything, so the game runs as fast as the machine
|
||||||
will let it.
|
will let it.
|
||||||
|
|
||||||
|
|
@ -1230,21 +1234,25 @@ function onControllerMoved(axis, value, which)
|
||||||
--]]
|
--]]
|
||||||
end
|
end
|
||||||
|
|
||||||
function onInputPressed(what)
|
function onInputPressed(what, device)
|
||||||
--[[
|
--[[
|
||||||
When in keyboard MODE_NORMAL, input events are reported here when the
|
When in keyboard MODE_NORMAL, input events are reported here when the
|
||||||
key or button is first depressed. For a full list of keys, buttons,
|
key or button is first depressed. For a full list of keys, buttons,
|
||||||
and controllers, see Singe/Framework.singe. For MODE_FULL, this event
|
and controllers, see Singe/Framework.singe. For MODE_FULL, this event
|
||||||
will be called with the keysym of the key pressed.
|
will be called with the keysym of the key pressed. When the switch
|
||||||
|
came from a mouse or a light gun, device is that device's index
|
||||||
|
(0 for the first), as Hypseus passes it, so a two-gun game can tell
|
||||||
|
its players apart; for a key or a pad it is nil.
|
||||||
--]]
|
--]]
|
||||||
end
|
end
|
||||||
|
|
||||||
function onInputReleased(what)
|
function onInputReleased(what, device)
|
||||||
--[[
|
--[[
|
||||||
When in keyboard MODE_NORMAL, input events are reported here when the
|
When in keyboard MODE_NORMAL, input events are reported here when the
|
||||||
key or button is released. For a full list of keys, buttons, and
|
key or button is released. For a full list of keys, buttons, and
|
||||||
controllers, see Singe/Framework.singe. For MODE_FULL, this event
|
controllers, see Singe/Framework.singe. For MODE_FULL, this event
|
||||||
will be called with the keysym of the key released.
|
will be called with the keysym of the key released. device is as
|
||||||
|
for onInputPressed.
|
||||||
--]]
|
--]]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -2220,7 +2228,8 @@ more than a Raspberry Pi 4 shares with its display. Textures shipped as KTX2
|
||||||
files (Basis Universal, made by `util/compressTextures.py` from PNGs or from
|
files (Basis Universal, made by `util/compressTextures.py` from PNGs or from
|
||||||
a whole `.gltf`) stay compressed on the GPU at a quarter of the size or
|
a whole `.gltf`) stay compressed on the GPU at a quarter of the size or
|
||||||
less, in whichever block format the machine has: BC7 on desktops, ASTC on
|
less, in whichever block format the machine has: BC7 on desktops, ASTC on
|
||||||
Apple GPUs and the Pi 4, BC3 on older cards, and plain RGBA where nothing
|
Apple GPUs and the Pi 4, ETC2 on any OpenGL ES 3 part (every handheld's
|
||||||
|
Mali, the Pi under GLES), BC3 on older cards, and plain RGBA where nothing
|
||||||
else exists, all from the one file. Give a `.ktx2` name in place of a sprite
|
else exists, all from the one file. Give a `.ktx2` name in place of a sprite
|
||||||
to `materialSetTexture` and the map calls, and a model whose file uses
|
to `materialSetTexture` and the map calls, and a model whose file uses
|
||||||
`KHR_texture_basisu` loads them on its own. `sceneGetStats` reports the
|
`KHR_texture_basisu` loads them on its own. `sceneGetStats` reports the
|
||||||
|
|
@ -7027,7 +7036,7 @@ fontQuality(FONT_QUALITY_BLENDED)
|
||||||
fontSelect(id)
|
fontSelect(id)
|
||||||
----
|
----
|
||||||
|
|
||||||
Makes a loaded font the one used by `fontPrint` and `fontToSprite`. The selection persists until the next `fontSelect` or `fontLoad`, since loading also selects. It can be changed as often as needed, including several times within one `onOverlayUpdate`. An unknown or unloaded handle ends the script with an error.
|
Makes a loaded font the one used by `fontPrint` and `fontToSprite`. The selection persists until the next `fontSelect` or `fontLoad`, since loading also selects. It can be changed as often as needed, including several times within one `onOverlayUpdate`. An unknown or unloaded handle (or no number at all) leaves the current font selected and prints a warning, as Hypseus Singe lets it pass.
|
||||||
|
|
||||||
*Since:* 1.x
|
*Since:* 1.x
|
||||||
*See also:* <<fontload,fontLoad>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
|
*See also:* <<fontload,fontLoad>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
|
||||||
|
|
@ -10052,10 +10061,10 @@ musicPause()
|
||||||
|
|
||||||
[source,text]
|
[source,text]
|
||||||
----
|
----
|
||||||
musicPlay(id [, loops])
|
id = musicPlay(id [, loops])
|
||||||
----
|
----
|
||||||
|
|
||||||
Hypseus extension. Plays a track from its beginning. `loops` means what <<soundplay,`soundPlay`>>'s does: `0`, the default, plays once, `-1` plays forever, and `N` repeats `N` more times. The count is clamped to `-1` to `64`, as Hypseus clamps it, so a game asking for more gets what it would have got there.
|
Hypseus extension. Plays a track from its beginning and answers its handle, which is how Hypseus's games keep it (`sndHandle = musicPlay(music)`) to stop the piece later. `loops` means what <<soundplay,`soundPlay`>>'s does: `0`, the default, plays once, `-1` plays forever, and `N` repeats `N` more times. The count is clamped to `-1` to `64`, as Hypseus clamps it, so a game asking for more gets what it would have got there.
|
||||||
|
|
||||||
Playing a track that is already playing starts it again from the beginning.
|
Playing a track that is already playing starts it again from the beginning.
|
||||||
|
|
||||||
|
|
@ -10064,7 +10073,7 @@ Playing a track that is already playing starts it again from the beginning.
|
||||||
* `id` -- number; a music handle.
|
* `id` -- number; a music handle.
|
||||||
* `loops` -- optional number; `-1` to `64`. Default `0`.
|
* `loops` -- optional number; `-1` to `64`. Default `0`.
|
||||||
|
|
||||||
*Returns:* nothing.
|
*Returns:* number; the handle it was given.
|
||||||
|
|
||||||
*Since:* 3.00, as a Hypseus Singe extension.
|
*Since:* 3.00, as a Hypseus Singe extension.
|
||||||
*See also:* <<musicstop,musicStop>>, <<musicsetvolume,musicSetVolume>>
|
*See also:* <<musicstop,musicStop>>, <<musicsetvolume,musicSetVolume>>
|
||||||
|
|
@ -10138,7 +10147,7 @@ musicSetVolume(soundGetVolume() * 2)
|
||||||
musicStop([id [, fadeMs]])
|
musicStop([id [, fadeMs]])
|
||||||
----
|
----
|
||||||
|
|
||||||
Hypseus extension. Stops that track, or every piece of music when no handle is given. `fadeMs` fades it out over that many milliseconds instead of cutting it; `0`, the default, stops at once. A negative fade terminates the script.
|
Hypseus extension. Stops that track, or every piece of music when no handle is given (a `nil` handle counts as none). `fadeMs` fades it out over that many milliseconds instead of cutting it; `0`, the default, stops at once. A negative fade terminates the script.
|
||||||
|
|
||||||
*Parameters:*
|
*Parameters:*
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ typedef enum Ktx2FormatE {
|
||||||
KTX2_RGBA = 0, // Uncompressed, the fallback
|
KTX2_RGBA = 0, // Uncompressed, the fallback
|
||||||
KTX2_BC7 = 1, // Desktop GPUs
|
KTX2_BC7 = 1, // Desktop GPUs
|
||||||
KTX2_ASTC = 2, // ASTC 4x4: Apple GPUs, the Raspberry Pi 4 and phones
|
KTX2_ASTC = 2, // ASTC 4x4: Apple GPUs, the Raspberry Pi 4 and phones
|
||||||
KTX2_BC3 = 3 // Older desktop GPUs
|
KTX2_BC3 = 3, // Older desktop GPUs
|
||||||
|
KTX2_ETC2 = 4 // ETC2 with EAC alpha: the one block format OpenGL ES 3 guarantees (handhelds, the Pi under GLES)
|
||||||
} Ktx2FormatE;
|
} Ktx2FormatE;
|
||||||
|
|
||||||
typedef struct Ktx2LevelS {
|
typedef struct Ktx2LevelS {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ static basist::transcoder_texture_format _target(Ktx2FormatE format) {
|
||||||
return basist::transcoder_texture_format::cTFASTC_4x4_RGBA;
|
return basist::transcoder_texture_format::cTFASTC_4x4_RGBA;
|
||||||
case KTX2_BC3:
|
case KTX2_BC3:
|
||||||
return basist::transcoder_texture_format::cTFBC3_RGBA;
|
return basist::transcoder_texture_format::cTFBC3_RGBA;
|
||||||
|
case KTX2_ETC2:
|
||||||
|
return basist::transcoder_texture_format::cTFETC2_RGBA;
|
||||||
default:
|
default:
|
||||||
return basist::transcoder_texture_format::cTFRGBA32;
|
return basist::transcoder_texture_format::cTFRGBA32;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -332,6 +332,11 @@ bool rgpuSubmitCommandBuffer(SDL_GPUCommandBuffer *commandBuffer) {
|
||||||
|
|
||||||
|
|
||||||
bool rgpuTextureSupportsFormat(SDL_GPUDevice *device, SDL_GPUTextureFormat format, SDL_GPUTextureType type, SDL_GPUTextureUsageFlags usage) {
|
bool rgpuTextureSupportsFormat(SDL_GPUDevice *device, SDL_GPUTextureFormat format, SDL_GPUTextureType type, SDL_GPUTextureUsageFlags usage) {
|
||||||
|
// A format of our own naming (ETC2) is the GLES backend's alone; SDL's function would index
|
||||||
|
// its tables past the end with it.
|
||||||
|
if ((format >= RGPU_TEXTUREFORMAT_PRIVATE_FIRST) && (_api != RENDER_GLES)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return _backend->textureSupportsFormat(device, format, type, usage);
|
return _backend->textureSupportsFormat(device, format, type, usage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,13 @@ extern "C" {
|
||||||
// code pointer is then the ES source string rather than a blob, and its size is ignored.
|
// code pointer is then the ES source string rather than a blob, and its size is ignored.
|
||||||
#define RGPU_SHADERFORMAT_ESSL (1u << 16)
|
#define RGPU_SHADERFORMAT_ESSL (1u << 16)
|
||||||
|
|
||||||
|
// Texture formats SDL_GPU has no name for, kept past the end of its enum: ETC2 with EAC alpha,
|
||||||
|
// the one block format OpenGL ES 3 guarantees. The GLES backend offers them; the SDL_GPU backend
|
||||||
|
// never sees them (render.c answers no before asking it).
|
||||||
|
#define RGPU_TEXTUREFORMAT_PRIVATE_FIRST ((SDL_GPUTextureFormat)(SDL_GPU_TEXTUREFORMAT_ASTC_12x12_FLOAT + 1))
|
||||||
|
#define RGPU_TEXTUREFORMAT_ETC2_RGBA8_UNORM ((SDL_GPUTextureFormat)(SDL_GPU_TEXTUREFORMAT_ASTC_12x12_FLOAT + 1))
|
||||||
|
#define RGPU_TEXTUREFORMAT_ETC2_RGBA8_UNORM_SRGB ((SDL_GPUTextureFormat)(SDL_GPU_TEXTUREFORMAT_ASTC_12x12_FLOAT + 2))
|
||||||
|
|
||||||
|
|
||||||
typedef struct RenderBackendS {
|
typedef struct RenderBackendS {
|
||||||
SDL_GPUCommandBuffer * (*acquireCommandBuffer)(SDL_GPUDevice *device);
|
SDL_GPUCommandBuffer * (*acquireCommandBuffer)(SDL_GPUDevice *device);
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@
|
||||||
#define GLES_FRAMEBUFFER_CACHE 32
|
#define GLES_FRAMEBUFFER_CACHE 32
|
||||||
#define GLES_LOG_BYTES 1024 // A driver's compile or link log, which is the only clue it gives
|
#define GLES_LOG_BYTES 1024 // A driver's compile or link log, which is the only clue it gives
|
||||||
#define GLES_NAME_BYTES 128 // A uniform block's name as SPIRV-Cross emits it
|
#define GLES_NAME_BYTES 128 // A uniform block's name as SPIRV-Cross emits it
|
||||||
|
#define GLES_BLOCK_SIDE 4 // Pixels along a block's side, ETC2 and ASTC 4x4 alike
|
||||||
|
#define GLES_BLOCK_BYTES 16 // Bytes a block takes in either
|
||||||
|
|
||||||
// A texture's storage. A multisampled colour target is a renderbuffer rather than a texture: the
|
// A texture's storage. A multisampled colour target is a renderbuffer rather than a texture: the
|
||||||
// scene only ever resolves it and never samples it, and multisampled textures are ES 3.1 with an
|
// scene only ever resolves it and never samples it, and multisampled textures are ES 3.1 with an
|
||||||
|
|
@ -153,6 +155,7 @@ typedef struct GlesDeviceS {
|
||||||
uint32_t uniformOffset;
|
uint32_t uniformOffset;
|
||||||
int32_t uniformAlignment;
|
int32_t uniformAlignment;
|
||||||
GLfloat maxAnisotropy; // 0 when the driver has no GL_EXT_texture_filter_anisotropic
|
GLfloat maxAnisotropy; // 0 when the driver has no GL_EXT_texture_filter_anisotropic
|
||||||
|
bool astc; // GL_KHR_texture_compression_astc_ldr: ASTC 4x4 textures
|
||||||
bool uniformWrapped;
|
bool uniformWrapped;
|
||||||
bool ready;
|
bool ready;
|
||||||
} GlesDeviceT;
|
} GlesDeviceT;
|
||||||
|
|
@ -165,6 +168,7 @@ static void _bindTextureUnit(uint32_t unit, GlesTextureT *te
|
||||||
static GLenum _blendFactor(SDL_GPUBlendFactor factor);
|
static GLenum _blendFactor(SDL_GPUBlendFactor factor);
|
||||||
static GLenum _blendOp(SDL_GPUBlendOp op);
|
static GLenum _blendOp(SDL_GPUBlendOp op);
|
||||||
static GLenum _compare(SDL_GPUCompareOp op);
|
static GLenum _compare(SDL_GPUCompareOp op);
|
||||||
|
static uint32_t _compressedBytes(SDL_GPUTextureFormat format, uint32_t width, uint32_t height);
|
||||||
static void _formatToGl(SDL_GPUTextureFormat format, GLenum *internalFormat, GLenum *layout, GLenum *type);
|
static void _formatToGl(SDL_GPUTextureFormat format, GLenum *internalFormat, GLenum *layout, GLenum *type);
|
||||||
static GLuint _framebufferFor(const SDL_GPUColorTargetInfo *colour, uint32_t colourCount, const SDL_GPUDepthStencilTargetInfo *depth, uint32_t *width, uint32_t *height);
|
static GLuint _framebufferFor(const SDL_GPUColorTargetInfo *colour, uint32_t colourCount, const SDL_GPUDepthStencilTargetInfo *depth, uint32_t *width, uint32_t *height);
|
||||||
static SDL_GPUCommandBuffer * _glesAcquireCommandBuffer(SDL_GPUDevice *device);
|
static SDL_GPUCommandBuffer * _glesAcquireCommandBuffer(SDL_GPUDevice *device);
|
||||||
|
|
@ -332,13 +336,42 @@ static GLenum _compare(SDL_GPUCompareOp op) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The bytes a block-compressed level of the given size takes, or zero for a format that is not
|
||||||
|
// block compressed. ETC2 with EAC alpha and ASTC 4x4 both pack a 4x4 block into sixteen bytes.
|
||||||
|
static uint32_t _compressedBytes(SDL_GPUTextureFormat format, uint32_t width, uint32_t height) {
|
||||||
|
bool block = (format == RGPU_TEXTUREFORMAT_ETC2_RGBA8_UNORM) || (format == RGPU_TEXTUREFORMAT_ETC2_RGBA8_UNORM_SRGB) || (format == SDL_GPU_TEXTUREFORMAT_ASTC_4x4_UNORM) || (format == SDL_GPU_TEXTUREFORMAT_ASTC_4x4_UNORM_SRGB);
|
||||||
|
|
||||||
|
if (!block) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((width + GLES_BLOCK_SIDE - 1) / GLES_BLOCK_SIDE) * ((height + GLES_BLOCK_SIDE - 1) / GLES_BLOCK_SIDE) * GLES_BLOCK_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// The formats the engine actually asks for. Anything else reports zero, and rgpuTextureSupportsFormat
|
// The formats the engine actually asks for. Anything else reports zero, and rgpuTextureSupportsFormat
|
||||||
// turns that into a plain no so the caller falls back the way it already does on a weak device.
|
// turns that into a plain no so the caller falls back the way it already does on a weak device.
|
||||||
|
// The block formats: ETC2 is every ES 3 part's, ASTC 4x4 the extension's.
|
||||||
static void _formatToGl(SDL_GPUTextureFormat format, GLenum *internalFormat, GLenum *layout, GLenum *type) {
|
static void _formatToGl(SDL_GPUTextureFormat format, GLenum *internalFormat, GLenum *layout, GLenum *type) {
|
||||||
*internalFormat = 0;
|
*internalFormat = 0;
|
||||||
*layout = GL_RGBA;
|
*layout = GL_RGBA;
|
||||||
*type = GL_UNSIGNED_BYTE;
|
*type = GL_UNSIGNED_BYTE;
|
||||||
|
// (The ETC2 names are ours, past SDL's enum, so they are not cases of its switch.)
|
||||||
|
if (format == RGPU_TEXTUREFORMAT_ETC2_RGBA8_UNORM) {
|
||||||
|
*internalFormat = GL_COMPRESSED_RGBA8_ETC2_EAC;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (format == RGPU_TEXTUREFORMAT_ETC2_RGBA8_UNORM_SRGB) {
|
||||||
|
*internalFormat = GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC;
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (format) {
|
switch (format) {
|
||||||
|
case SDL_GPU_TEXTUREFORMAT_ASTC_4x4_UNORM:
|
||||||
|
*internalFormat = _device.astc ? GL_COMPRESSED_RGBA_ASTC_4x4_KHR : 0;
|
||||||
|
break;
|
||||||
|
case SDL_GPU_TEXTUREFORMAT_ASTC_4x4_UNORM_SRGB:
|
||||||
|
*internalFormat = _device.astc ? GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR : 0;
|
||||||
|
break;
|
||||||
case SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM:
|
case SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM:
|
||||||
*internalFormat = GL_RGBA8;
|
*internalFormat = GL_RGBA8;
|
||||||
break;
|
break;
|
||||||
|
|
@ -1307,13 +1340,20 @@ static void _glesUploadToTexture(SDL_GPUCopyPass *copyPass, const SDL_GPUTexture
|
||||||
GLenum internalFormat;
|
GLenum internalFormat;
|
||||||
GLenum layout;
|
GLenum layout;
|
||||||
GLenum type;
|
GLenum type;
|
||||||
|
uint32_t compressed;
|
||||||
|
|
||||||
(void)copyPass;
|
(void)copyPass;
|
||||||
(void)cycle;
|
(void)cycle;
|
||||||
_formatToGl(texture->format, &internalFormat, &layout, &type);
|
_formatToGl(texture->format, &internalFormat, &layout, &type);
|
||||||
glBindTexture(texture->target, texture->name);
|
glBindTexture(texture->target, texture->name);
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
if (texture->target == GL_TEXTURE_2D) {
|
compressed = _compressedBytes(texture->format, destination->w, destination->h);
|
||||||
|
if (compressed > 0) {
|
||||||
|
// A block-compressed level arrives whole from the transcoder; its size is its block count.
|
||||||
|
GLenum face = (texture->target == GL_TEXTURE_CUBE_MAP) ? (GL_TEXTURE_CUBE_MAP_POSITIVE_X + destination->layer) : GL_TEXTURE_2D;
|
||||||
|
|
||||||
|
glCompressedTexSubImage2D(face, (GLint)destination->mip_level, (GLint)destination->x, (GLint)destination->y, (GLsizei)destination->w, (GLsizei)destination->h, internalFormat, (GLsizei)compressed, transfer->data + source->offset);
|
||||||
|
} else if (texture->target == GL_TEXTURE_2D) {
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, (GLint)destination->mip_level, (GLint)destination->x, (GLint)destination->y, (GLsizei)destination->w, (GLsizei)destination->h, layout, type, transfer->data + source->offset);
|
glTexSubImage2D(GL_TEXTURE_2D, (GLint)destination->mip_level, (GLint)destination->x, (GLint)destination->y, (GLsizei)destination->w, (GLsizei)destination->h, layout, type, transfer->data + source->offset);
|
||||||
} else if (texture->target == GL_TEXTURE_CUBE_MAP) {
|
} else if (texture->target == GL_TEXTURE_CUBE_MAP) {
|
||||||
glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + destination->layer, (GLint)destination->mip_level, (GLint)destination->x, (GLint)destination->y, (GLsizei)destination->w, (GLsizei)destination->h, layout, type, transfer->data + source->offset);
|
glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + destination->layer, (GLint)destination->mip_level, (GLint)destination->x, (GLint)destination->y, (GLsizei)destination->w, (GLsizei)destination->h, layout, type, transfer->data + source->offset);
|
||||||
|
|
@ -1565,6 +1605,8 @@ bool renderGlesStart(void) {
|
||||||
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &_device.maxAnisotropy);
|
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &_device.maxAnisotropy);
|
||||||
}
|
}
|
||||||
utilTrace("Gles: anisotropy %s", _device.maxAnisotropy > 1.0f ? "available" : "not offered");
|
utilTrace("Gles: anisotropy %s", _device.maxAnisotropy > 1.0f ? "available" : "not offered");
|
||||||
|
_device.astc = (extensions != NULL) && (strstr(extensions, "GL_KHR_texture_compression_astc_ldr") != NULL);
|
||||||
|
utilTrace("Gles: block textures: ETC2, %s", _device.astc ? "and ASTC 4x4" : "no ASTC");
|
||||||
}
|
}
|
||||||
glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &_device.uniformAlignment);
|
glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &_device.uniformAlignment);
|
||||||
if (_device.uniformAlignment <= 0) {
|
if (_device.uniformAlignment <= 0) {
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,10 @@ typedef intptr_t GLsizeiptr;
|
||||||
#define GL_COLOR_BUFFER_BIT 0x00004000
|
#define GL_COLOR_BUFFER_BIT 0x00004000
|
||||||
#define GL_COMPARE_REF_TO_TEXTURE 0x884E
|
#define GL_COMPARE_REF_TO_TEXTURE 0x884E
|
||||||
#define GL_COMPILE_STATUS 0x8B81
|
#define GL_COMPILE_STATUS 0x8B81
|
||||||
|
#define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278
|
||||||
|
#define GL_COMPRESSED_RGBA_ASTC_4x4_KHR 0x93B0
|
||||||
|
#define GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR 0x93D0
|
||||||
|
#define GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC 0x9279
|
||||||
#define GL_CONSTANT_COLOR 0x8001
|
#define GL_CONSTANT_COLOR 0x8001
|
||||||
#define GL_CULL_FACE 0x0B44
|
#define GL_CULL_FACE 0x0B44
|
||||||
#define GL_CW 0x0900
|
#define GL_CW 0x0900
|
||||||
|
|
@ -220,6 +224,7 @@ typedef void (GL_APIENTRYP GlesClearDepthfFn)(GLfloat d);
|
||||||
typedef void (GL_APIENTRYP GlesClearStencilFn)(GLint s);
|
typedef void (GL_APIENTRYP GlesClearStencilFn)(GLint s);
|
||||||
typedef void (GL_APIENTRYP GlesColorMaskFn)(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
|
typedef void (GL_APIENTRYP GlesColorMaskFn)(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
|
||||||
typedef void (GL_APIENTRYP GlesCompileShaderFn)(GLuint shader);
|
typedef void (GL_APIENTRYP GlesCompileShaderFn)(GLuint shader);
|
||||||
|
typedef void (GL_APIENTRYP GlesCompressedTexSubImage2DFn)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void *data);
|
||||||
typedef void (GL_APIENTRYP GlesCopyTexSubImage2DFn)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
|
typedef void (GL_APIENTRYP GlesCopyTexSubImage2DFn)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
|
||||||
typedef GLuint (GL_APIENTRYP GlesCreateProgramFn)(void);
|
typedef GLuint (GL_APIENTRYP GlesCreateProgramFn)(void);
|
||||||
typedef GLuint (GL_APIENTRYP GlesCreateShaderFn)(GLenum type);
|
typedef GLuint (GL_APIENTRYP GlesCreateShaderFn)(GLenum type);
|
||||||
|
|
@ -313,6 +318,7 @@ typedef struct GlesApiS {
|
||||||
GlesClearStencilFn clearStencil;
|
GlesClearStencilFn clearStencil;
|
||||||
GlesColorMaskFn colorMask;
|
GlesColorMaskFn colorMask;
|
||||||
GlesCompileShaderFn compileShader;
|
GlesCompileShaderFn compileShader;
|
||||||
|
GlesCompressedTexSubImage2DFn compressedTexSubImage2D;
|
||||||
GlesCopyTexSubImage2DFn copyTexSubImage2D;
|
GlesCopyTexSubImage2DFn copyTexSubImage2D;
|
||||||
GlesCreateProgramFn createProgram;
|
GlesCreateProgramFn createProgram;
|
||||||
GlesCreateShaderFn createShader;
|
GlesCreateShaderFn createShader;
|
||||||
|
|
@ -413,6 +419,7 @@ bool renderGlesLoad(void);
|
||||||
#define glClearStencil _glesApi.clearStencil
|
#define glClearStencil _glesApi.clearStencil
|
||||||
#define glColorMask _glesApi.colorMask
|
#define glColorMask _glesApi.colorMask
|
||||||
#define glCompileShader _glesApi.compileShader
|
#define glCompileShader _glesApi.compileShader
|
||||||
|
#define glCompressedTexSubImage2D _glesApi.compressedTexSubImage2D
|
||||||
#define glCopyTexSubImage2D _glesApi.copyTexSubImage2D
|
#define glCopyTexSubImage2D _glesApi.copyTexSubImage2D
|
||||||
#define glCreateProgram _glesApi.createProgram
|
#define glCreateProgram _glesApi.createProgram
|
||||||
#define glCreateShader _glesApi.createShader
|
#define glCreateShader _glesApi.createShader
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ bool renderGlesLoad(void) {
|
||||||
_glesApi.clearStencil = (GlesClearStencilFn)SDL_GL_GetProcAddress("glClearStencil");
|
_glesApi.clearStencil = (GlesClearStencilFn)SDL_GL_GetProcAddress("glClearStencil");
|
||||||
_glesApi.colorMask = (GlesColorMaskFn)SDL_GL_GetProcAddress("glColorMask");
|
_glesApi.colorMask = (GlesColorMaskFn)SDL_GL_GetProcAddress("glColorMask");
|
||||||
_glesApi.compileShader = (GlesCompileShaderFn)SDL_GL_GetProcAddress("glCompileShader");
|
_glesApi.compileShader = (GlesCompileShaderFn)SDL_GL_GetProcAddress("glCompileShader");
|
||||||
|
_glesApi.compressedTexSubImage2D = (GlesCompressedTexSubImage2DFn)SDL_GL_GetProcAddress("glCompressedTexSubImage2D");
|
||||||
_glesApi.copyTexSubImage2D = (GlesCopyTexSubImage2DFn)SDL_GL_GetProcAddress("glCopyTexSubImage2D");
|
_glesApi.copyTexSubImage2D = (GlesCopyTexSubImage2DFn)SDL_GL_GetProcAddress("glCopyTexSubImage2D");
|
||||||
_glesApi.createProgram = (GlesCreateProgramFn)SDL_GL_GetProcAddress("glCreateProgram");
|
_glesApi.createProgram = (GlesCreateProgramFn)SDL_GL_GetProcAddress("glCreateProgram");
|
||||||
_glesApi.createShader = (GlesCreateShaderFn)SDL_GL_GetProcAddress("glCreateShader");
|
_glesApi.createShader = (GlesCreateShaderFn)SDL_GL_GetProcAddress("glCreateShader");
|
||||||
|
|
@ -148,6 +149,7 @@ bool renderGlesLoad(void) {
|
||||||
"glClearStencil",
|
"glClearStencil",
|
||||||
"glColorMask",
|
"glColorMask",
|
||||||
"glCompileShader",
|
"glCompileShader",
|
||||||
|
"glCompressedTexSubImage2D",
|
||||||
"glCopyTexSubImage2D",
|
"glCopyTexSubImage2D",
|
||||||
"glCreateProgram",
|
"glCreateProgram",
|
||||||
"glCreateShader",
|
"glCreateShader",
|
||||||
|
|
|
||||||
10
src/scene.c
10
src/scene.c
|
|
@ -3560,6 +3560,9 @@ static SDL_GPUTexture *_uploadCompressed(const Ktx2ImageT *image, bool srgb) {
|
||||||
case KTX2_BC3:
|
case KTX2_BC3:
|
||||||
info.format = srgb ? SDL_GPU_TEXTUREFORMAT_BC3_RGBA_UNORM_SRGB : SDL_GPU_TEXTUREFORMAT_BC3_RGBA_UNORM;
|
info.format = srgb ? SDL_GPU_TEXTUREFORMAT_BC3_RGBA_UNORM_SRGB : SDL_GPU_TEXTUREFORMAT_BC3_RGBA_UNORM;
|
||||||
break;
|
break;
|
||||||
|
case KTX2_ETC2:
|
||||||
|
info.format = srgb ? RGPU_TEXTUREFORMAT_ETC2_RGBA8_UNORM_SRGB : RGPU_TEXTUREFORMAT_ETC2_RGBA8_UNORM;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
info.format = srgb ? SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM_SRGB : SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
info.format = srgb ? SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM_SRGB : SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
||||||
break;
|
break;
|
||||||
|
|
@ -5441,15 +5444,20 @@ bool sceneInit(SDL_GPUDevice *device, SDL_Renderer *renderer) {
|
||||||
_scene.depthFormat = _depthFormat();
|
_scene.depthFormat = _depthFormat();
|
||||||
_scene.shadowFormat = _shadowFormat();
|
_scene.shadowFormat = _shadowFormat();
|
||||||
_scene.hdrFormat = _hdrFormat();
|
_scene.hdrFormat = _hdrFormat();
|
||||||
// What KTX2 textures become: the best block format the device has, else plain RGBA.
|
// What KTX2 textures become: the best block format the device has, else plain RGBA. ETC2
|
||||||
|
// comes before BC3 because every OpenGL ES 3 part has it, and a Mali without ASTC would
|
||||||
|
// otherwise fall all the way to RGBA on the devices with the least memory.
|
||||||
_scene.compressedFormat = KTX2_RGBA;
|
_scene.compressedFormat = KTX2_RGBA;
|
||||||
if (rgpuTextureSupportsFormat(device, SDL_GPU_TEXTUREFORMAT_BC7_RGBA_UNORM_SRGB, SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREUSAGE_SAMPLER)) {
|
if (rgpuTextureSupportsFormat(device, SDL_GPU_TEXTUREFORMAT_BC7_RGBA_UNORM_SRGB, SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREUSAGE_SAMPLER)) {
|
||||||
_scene.compressedFormat = KTX2_BC7;
|
_scene.compressedFormat = KTX2_BC7;
|
||||||
} else if (rgpuTextureSupportsFormat(device, SDL_GPU_TEXTUREFORMAT_ASTC_4x4_UNORM_SRGB, SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREUSAGE_SAMPLER)) {
|
} else if (rgpuTextureSupportsFormat(device, SDL_GPU_TEXTUREFORMAT_ASTC_4x4_UNORM_SRGB, SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREUSAGE_SAMPLER)) {
|
||||||
_scene.compressedFormat = KTX2_ASTC;
|
_scene.compressedFormat = KTX2_ASTC;
|
||||||
|
} else if (rgpuTextureSupportsFormat(device, RGPU_TEXTUREFORMAT_ETC2_RGBA8_UNORM_SRGB, SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREUSAGE_SAMPLER)) {
|
||||||
|
_scene.compressedFormat = KTX2_ETC2;
|
||||||
} else if (rgpuTextureSupportsFormat(device, SDL_GPU_TEXTUREFORMAT_BC3_RGBA_UNORM_SRGB, SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREUSAGE_SAMPLER)) {
|
} else if (rgpuTextureSupportsFormat(device, SDL_GPU_TEXTUREFORMAT_BC3_RGBA_UNORM_SRGB, SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREUSAGE_SAMPLER)) {
|
||||||
_scene.compressedFormat = KTX2_BC3;
|
_scene.compressedFormat = KTX2_BC3;
|
||||||
}
|
}
|
||||||
|
utilTrace("Scene: KTX2 textures as %s", (const char *[]){ "RGBA", "BC7", "ASTC 4x4", "BC3", "ETC2" }[_scene.compressedFormat]);
|
||||||
if (!_createShaders()) {
|
if (!_createShaders()) {
|
||||||
sceneQuit();
|
sceneQuit();
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
79
src/singe.c
79
src/singe.c
|
|
@ -1271,6 +1271,7 @@ static int32_t apiNodeSetSpriteFrame(lua_State *L);
|
||||||
static int32_t apiNodeSetText(lua_State *L);
|
static int32_t apiNodeSetText(lua_State *L);
|
||||||
static int32_t apiNodeSetVisible(lua_State *L);
|
static int32_t apiNodeSetVisible(lua_State *L);
|
||||||
static int32_t apiOsClock(lua_State *L);
|
static int32_t apiOsClock(lua_State *L);
|
||||||
|
static int32_t apiOsTime(lua_State *L);
|
||||||
static int32_t apiOverlayBox(lua_State *L);
|
static int32_t apiOverlayBox(lua_State *L);
|
||||||
static int32_t apiOverlayCircle(lua_State *L);
|
static int32_t apiOverlayCircle(lua_State *L);
|
||||||
static int32_t apiOverlayClear(lua_State *L);
|
static int32_t apiOverlayClear(lua_State *L);
|
||||||
|
|
@ -2631,7 +2632,13 @@ static void _deliverKey(bool down, int32_t keysym, int32_t scancode) {
|
||||||
_global.switchHeld[move] = down;
|
_global.switchHeld[move] = down;
|
||||||
// A pad or mouse switch steers a GUI that takes input; a key already reached it as a key.
|
// A pad or mouse switch steers a GUI that takes input; a key already reached it as a key.
|
||||||
if ((scancode < CODE_GAMEPAD_BASE) || !_guiSwitch((InputE)move, down)) {
|
if ((scancode < CODE_GAMEPAD_BASE) || !_guiSwitch((InputE)move, down)) {
|
||||||
_callLua(down ? "onInputPressed" : "onInputReleased", "i", move);
|
// A switch from a mouse or gun names its device too, as Hypseus does, so a
|
||||||
|
// two-gun game can tell its players apart.
|
||||||
|
if (scancode >= CODE_MOUSE_BASE) {
|
||||||
|
_callLua(down ? "onInputPressed" : "onInputReleased", "ii", move, (scancode - CODE_MOUSE_BASE) / CODE_MOUSE_STRIDE);
|
||||||
|
} else {
|
||||||
|
_callLua(down ? "onInputPressed" : "onInputReleased", "i", move);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -7503,7 +7510,12 @@ static SDL_Surface *_spriteTransform(SpriteT *sprite, SDL_Surface *source) {
|
||||||
if ((sprite->angle == 0.0) && (sprite->scaleX == 1.0) && (sprite->scaleY == 1.0)) {
|
if ((sprite->angle == 0.0) && (sprite->scaleX == 1.0) && (sprite->scaleY == 1.0)) {
|
||||||
copy = SDL_DuplicateSurface(source);
|
copy = SDL_DuplicateSurface(source);
|
||||||
} else {
|
} else {
|
||||||
copy = rotoZoomSurface(source, -sprite->angle, sprite->scaleX, sprite->scaleY, sprite->smooth != 0);
|
// A scale that would leave no pixels (a bullet shrinking to nothing) draws as one pixel
|
||||||
|
// rather than failing the transform; the scale the script set is kept as it reads back.
|
||||||
|
double scaleX = fmax(fabs(sprite->scaleX), 1.0 / (double)source->w);
|
||||||
|
double scaleY = fmax(fabs(sprite->scaleY), 1.0 / (double)source->h);
|
||||||
|
|
||||||
|
copy = rotoZoomSurface(source, -sprite->angle, scaleX, scaleY, sprite->smooth != 0);
|
||||||
}
|
}
|
||||||
if ((copy != NULL) && (sprite->flip != SDL_FLIP_NONE)) {
|
if ((copy != NULL) && (sprite->flip != SDL_FLIP_NONE)) {
|
||||||
SDL_FlipSurface(copy, sprite->flip);
|
SDL_FlipSurface(copy, sprite->flip);
|
||||||
|
|
@ -7577,6 +7589,13 @@ static void _startLuaContext(lua_State *L) {
|
||||||
lua_getglobal(L, "os");
|
lua_getglobal(L, "os");
|
||||||
lua_pushcfunction(L, apiOsClock);
|
lua_pushcfunction(L, apiOsClock);
|
||||||
lua_setfield(L, -2, "clock");
|
lua_setfield(L, -2, "clock");
|
||||||
|
// A deterministic run reads the same wall clock every time it plays: games seed their
|
||||||
|
// generator from os.time() when a game starts, and the run must repeat.
|
||||||
|
if (_global.conf->deterministic) {
|
||||||
|
lua_getfield(L, -1, "time");
|
||||||
|
lua_pushcclosure(L, apiOsTime, 1);
|
||||||
|
lua_setfield(L, -2, "time");
|
||||||
|
}
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
|
|
||||||
// Games written for 2.x seed the generator with fractional numbers; Lua 5.4 refuses them.
|
// Games written for 2.x seed the generator with fractional numbers; Lua 5.4 refuses them.
|
||||||
|
|
@ -9856,10 +9875,22 @@ static int32_t apiFontQuality(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// fontSelect(id)
|
// fontSelect(id) A font that was never loaded (a script naming one it misspelled) leaves the
|
||||||
|
// current font as it is with a warning, as Hypseus lets it pass, instead of ending the script.
|
||||||
static int32_t apiFontSelect(lua_State *L) {
|
static int32_t apiFontSelect(lua_State *L) {
|
||||||
|
FontT *font = NULL;
|
||||||
|
int32_t id = 0;
|
||||||
|
|
||||||
_argCheck(L, "fontSelect", 1, 1);
|
_argCheck(L, "fontSelect", 1, 1);
|
||||||
_global.fontCurrent = _argFont(L, "fontSelect", 1);
|
if (lua_isnumber(L, 1)) {
|
||||||
|
id = (int32_t)lua_tointeger(L, 1);
|
||||||
|
HASH_FIND_INT(_global.fontList, &id, font);
|
||||||
|
}
|
||||||
|
if (!font) {
|
||||||
|
utilSay("Warning: fontSelect names no font (%s); the current font stays.", lua_isnumber(L, 1) ? "a bad index" : "not a number");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
_global.fontCurrent = font;
|
||||||
_luaTrace(L, "fontSelect", "%d", _global.fontCurrent->id);
|
_luaTrace(L, "fontSelect", "%d", _global.fontCurrent->id);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -11306,9 +11337,10 @@ static int32_t apiMusicPause(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// musicPlay(id [, loops]) Hypseus extension. loops means what soundPlay means: 0 (the default)
|
// id = musicPlay(id [, loops]) Hypseus extension. loops means what soundPlay means: 0 (the
|
||||||
// plays once, -1 forever, N repeats N more times. Hypseus clamps the count to -1 to 64 and so does
|
// default) plays once, -1 forever, N repeats N more times. Hypseus clamps the count to -1 to 64 and
|
||||||
// this, so a game asking for more gets what it would have got there.
|
// so does this, so a game asking for more gets what it would have got there. Answers the handle it
|
||||||
|
// was given, as Hypseus does: the games keep it (sndHandle = musicPlay(music)) to stop the piece.
|
||||||
static int32_t apiMusicPlay(lua_State *L) {
|
static int32_t apiMusicPlay(lua_State *L) {
|
||||||
MusicT *music = NULL;
|
MusicT *music = NULL;
|
||||||
int32_t loops = 0;
|
int32_t loops = 0;
|
||||||
|
|
@ -11329,8 +11361,9 @@ static int32_t apiMusicPlay(lua_State *L) {
|
||||||
}
|
}
|
||||||
SDL_DestroyProperties(options);
|
SDL_DestroyProperties(options);
|
||||||
_luaTrace(L, "musicPlay", "%d loops %d", music->id, loops);
|
_luaTrace(L, "musicPlay", "%d loops %d", music->id, loops);
|
||||||
|
lua_pushinteger(L, music->id);
|
||||||
|
|
||||||
return 0;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11375,12 +11408,13 @@ static int32_t apiMusicSetVolume(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// musicStop([id [, fadeMs]]) Hypseus extension. With no handle every piece of music stops, as
|
// musicStop([id [, fadeMs]]) Hypseus extension. With no handle (or a nil one) every piece of
|
||||||
// Hypseus stops whatever its music mixer is playing. A fade is in milliseconds.
|
// music stops, as Hypseus stops whatever its music mixer is playing. A fade is in milliseconds.
|
||||||
static int32_t apiMusicStop(lua_State *L) {
|
static int32_t apiMusicStop(lua_State *L) {
|
||||||
MusicT *music = NULL;
|
MusicT *music = NULL;
|
||||||
MusicT *next = NULL;
|
MusicT *next = NULL;
|
||||||
int32_t fade = 0;
|
int32_t fade = 0;
|
||||||
|
bool one = false;
|
||||||
|
|
||||||
_argCheck(L, "musicStop", 0, 2);
|
_argCheck(L, "musicStop", 0, 2);
|
||||||
if (lua_gettop(L) >= 2) {
|
if (lua_gettop(L) >= 2) {
|
||||||
|
|
@ -11389,7 +11423,8 @@ static int32_t apiMusicStop(lua_State *L) {
|
||||||
_luaDie(L, "musicStop", "A fade cannot be negative: %d", fade);
|
_luaDie(L, "musicStop", "A fade cannot be negative: %d", fade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lua_gettop(L) >= 1) {
|
one = !lua_isnoneornil(L, 1);
|
||||||
|
if (one) {
|
||||||
music = _argMusic(L, "musicStop", 1);
|
music = _argMusic(L, "musicStop", 1);
|
||||||
MIX_StopTrack(music->track, MIX_TrackMSToFrames(music->track, fade));
|
MIX_StopTrack(music->track, MIX_TrackMSToFrames(music->track, fade));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -11397,7 +11432,7 @@ static int32_t apiMusicStop(lua_State *L) {
|
||||||
MIX_StopTrack(music->track, MIX_TrackMSToFrames(music->track, fade));
|
MIX_StopTrack(music->track, MIX_TrackMSToFrames(music->track, fade));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_luaTrace(L, "musicStop", "%d fade %d", (lua_gettop(L) >= 1) ? music->id : -1, fade);
|
_luaTrace(L, "musicStop", "%d fade %d", one ? music->id : -1, fade);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -12224,6 +12259,21 @@ static int32_t apiOsClock(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// os.time([table]) In a deterministic run the bare call answers with the virtual clock's seconds
|
||||||
|
// from the option's seed, so a game seeding from the wall clock lands on the same numbers every
|
||||||
|
// run; a call with a table is a date conversion, and Lua's own answers it.
|
||||||
|
static int32_t apiOsTime(lua_State *L) {
|
||||||
|
if (lua_gettop(L) == 0) {
|
||||||
|
lua_pushinteger(L, (lua_Integer)_global.conf->deterministicStep + (lua_Integer)(utilTicks() / MS_PER_SECOND));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
lua_pushvalue(L, lua_upvalueindex(1));
|
||||||
|
lua_insert(L, 1);
|
||||||
|
lua_call(L, lua_gettop(L) - 1, LUA_MULTRET);
|
||||||
|
return lua_gettop(L);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// overlayBox(x1, y1, x2, y2) Outline only.
|
// overlayBox(x1, y1, x2, y2) Outline only.
|
||||||
static int32_t apiOverlayBox(lua_State *L) {
|
static int32_t apiOverlayBox(lua_State *L) {
|
||||||
int32_t x1 = 0;
|
int32_t x1 = 0;
|
||||||
|
|
@ -16836,7 +16886,10 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
// refreshes continuously. Emptying them per rendered frame therefore drew the GUIs and
|
// refreshes continuously. Emptying them per rendered frame therefore drew the GUIs and
|
||||||
// the 2D particles only on the frames the script happened to run on, which is a menu that
|
// the 2D particles only on the frames the script happened to run on, which is a menu that
|
||||||
// blinks over a grid that does not.
|
// blinks over a grid that does not.
|
||||||
if (!_global.frozen && (utilTicks() > frameClock)) {
|
//
|
||||||
|
// The throttle is inclusive: on the virtual clock every loop iteration is exactly one
|
||||||
|
// step, and a strict comparison ran the script every other iteration, a disc frame apart.
|
||||||
|
if (!_global.frozen && (utilTicks() >= frameClock)) {
|
||||||
intReturn = OVERLAY_NOT_UPDATED;
|
intReturn = OVERLAY_NOT_UPDATED;
|
||||||
_global.guiDrawCount = 0;
|
_global.guiDrawCount = 0;
|
||||||
_global.overlayLayerCount = 0;
|
_global.overlayLayerCount = 0;
|
||||||
|
|
|
||||||
|
|
@ -2600,36 +2600,28 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) {
|
||||||
_resetClock(v, now);
|
_resetClock(v, now);
|
||||||
} else {
|
} else {
|
||||||
if (v->playing) {
|
if (v->playing) {
|
||||||
if (utilIsDeterministic()) {
|
// Where in the video should we be? Follow the audio when there is any. A
|
||||||
// Decoding has a clock of its own, which no virtual clock reaches, so a
|
// deterministic run follows the virtual clock instead: the audio's own clock is
|
||||||
// reproducible run steps the disc a frame at a time from wherever it is
|
// real time, which no virtual clock reaches, and the frame loop is paced as a
|
||||||
// parked. A paused disc and a search still settle where they always did.
|
// real one is, so a frame stays on for as many frames as its time is worth.
|
||||||
v->frame++;
|
if (utilIsDeterministic() || (v->audioSourceCount == 0)) {
|
||||||
if (v->frame >= count) {
|
elapsed = (int64_t)(now - v->startTicks) + v->startTime;
|
||||||
|
} else {
|
||||||
|
elapsed = _audioClock(v, now);
|
||||||
|
}
|
||||||
|
// Advance to the last frame whose presentation time has arrived.
|
||||||
|
next = v->frame + 1;
|
||||||
|
while ((next < count) && (_frameTime(v, next) <= elapsed)) {
|
||||||
|
v->frame = next;
|
||||||
|
next++;
|
||||||
|
}
|
||||||
|
// Past the end of the last frame? Loop.
|
||||||
|
if (next >= count) {
|
||||||
|
lastDuration = (int64_t)(MS_PER_SECOND * (double)v->fps.den / (double)v->fps.num);
|
||||||
|
if (elapsed >= _frameTime(v, count - 1) + lastDuration) {
|
||||||
v->frame = 0;
|
v->frame = 0;
|
||||||
_resetClock(v, now);
|
_resetClock(v, now);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Where in the video should we be? Follow the audio when there is any.
|
|
||||||
if (v->audioSourceCount > 0) {
|
|
||||||
elapsed = _audioClock(v, now);
|
|
||||||
} else {
|
|
||||||
elapsed = (int64_t)(now - v->startTicks) + v->startTime;
|
|
||||||
}
|
|
||||||
// Advance to the last frame whose presentation time has arrived.
|
|
||||||
next = v->frame + 1;
|
|
||||||
while ((next < count) && (_frameTime(v, next) <= elapsed)) {
|
|
||||||
v->frame = next;
|
|
||||||
next++;
|
|
||||||
}
|
|
||||||
// Past the end of the last frame? Loop.
|
|
||||||
if (next >= count) {
|
|
||||||
lastDuration = (int64_t)(MS_PER_SECOND * (double)v->fps.den / (double)v->fps.num);
|
|
||||||
if (elapsed >= _frameTime(v, count - 1) + lastDuration) {
|
|
||||||
v->frame = 0;
|
|
||||||
_resetClock(v, now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
testScripts/Menu.sh
Executable file
4
testScripts/Menu.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
"/home/scott/claude/singe/.builddir/linux-gcc/x86_64/singe/Singe-v3.00-Linux-x86_64" -k -w -v Singe/menuBackground.mkv Singe/Menu.singe
|
||||||
|
|
@ -68,3 +68,4 @@ screenshot or two and quits by itself; the results are in screenshots/.
|
||||||
| scene49.singe | 49 | 48.5 | Input over the disc: every device the engine can see on one page -- the four gamepad slots with the name, the six axis values and the fifteen button states of whatever is in each, the mice ManyMouse found with their names, the last mouse position and how many times `SWITCH_MOUSE_DISCONNECT` has fired, the keyboard mode, and the switches the mappings are currently letting through -- with `DEAD_ZONE`, `TRIGGER_THRESHOLD` (raw and as a per cent of full travel) and `joyMouseIsEnabled()` across the top. The phases are timed on the wall clock (shots at 2, 6, 10 and 16 seconds, quitting at 18) so a device can be attached, pressed and unplugged while it runs; the LD_PRELOAD harness in the scratchpad drives an SDL virtual joystick through exactly that. Run plain, with `--joymouse --js_range 20`, with `--gamepad_reorder 10`, with `--trigger_threshold 99.5`, with `--mapjoysticks=false`, with `--manymouse --absolutes_only`, with a `gamecontrollerdb.txt` beside it, and with a `controls.cfg` setting `DEAD_ZONES` |
|
| scene49.singe | 49 | 48.5 | Input over the disc: every device the engine can see on one page -- the four gamepad slots with the name, the six axis values and the fifteen button states of whatever is in each, the mice ManyMouse found with their names, the last mouse position and how many times `SWITCH_MOUSE_DISCONNECT` has fired, the keyboard mode, and the switches the mappings are currently letting through -- with `DEAD_ZONE`, `TRIGGER_THRESHOLD` (raw and as a per cent of full travel) and `joyMouseIsEnabled()` across the top. The phases are timed on the wall clock (shots at 2, 6, 10 and 16 seconds, quitting at 18) so a device can be attached, pressed and unplugged while it runs; the LD_PRELOAD harness in the scratchpad drives an SDL virtual joystick through exactly that. Run plain, with `--joymouse --js_range 20`, with `--gamepad_reorder 10`, with `--trigger_threshold 99.5`, with `--mapjoysticks=false`, with `--manymouse --absolutes_only`, with a `gamecontrollerdb.txt` beside it, and with a `controls.cfg` setting `DEAD_ZONES` |
|
||||||
| scene50.singe | 50 | 48.6 | Subtitles and the disc picture over the disc: `testScripts/subtitles.srt` (four cues, one of them two lines and one carrying a `<bracket>` and an `&` that must reach the screen as text) parsed into disc frames at the disc's own frame rate and shown through the shipped `Singe/subtitle.rml`, with `testScripts/broken.srt` refused (`srtLoad` answers false and loads nothing); the disc searched to a known frame before every shot so a cue is read off the disc's frame number and not the wall clock; `srtPosition` at 15 and 80 per cent; `overlayBanner` taking the cue's place for thirty drawn frames and then giving it back; `srtEnable` off and on; `srtClear`; `vldpSetLuma` at 0, 8, 4 and off; `vldpSetBlend` on, off and together with the luma; and `vldpFlash`, asked for and shot in the same tick because it lasts one drawn frame. 23 screenshots |
|
| scene50.singe | 50 | 48.6 | Subtitles and the disc picture over the disc: `testScripts/subtitles.srt` (four cues, one of them two lines and one carrying a `<bracket>` and an `&` that must reach the screen as text) parsed into disc frames at the disc's own frame rate and shown through the shipped `Singe/subtitle.rml`, with `testScripts/broken.srt` refused (`srtLoad` answers false and loads nothing); the disc searched to a known frame before every shot so a cue is read off the disc's frame number and not the wall clock; `srtPosition` at 15 and 80 per cent; `overlayBanner` taking the cue's place for thirty drawn frames and then giving it back; `srtEnable` off and on; `srtClear`; `vldpSetLuma` at 0, 8, 4 and off; `vldpSetBlend` on, off and together with the luma; and `vldpFlash`, asked for and shot in the same tick because it lasts one drawn frame. 23 screenshots |
|
||||||
| scene51.singe | 51 | 48.6 | Sprites, in-memory assets and music over the disc: `spriteRotateFrame` turning frames 1 to 4 of `testScripts/arrows.png` (a four frame strip whose arrows have a white notch, so a rotation is readable) at 0, 45, 135 and 225 degrees, drawn by `spriteDrawRotatedFrame` plain, at 1.5 and at 2 by 0.75, beside the same frame through `spriteDrawFrame` which ignores both; `spriteScale` before a rotation, so the pair honours the sprite's own scale; an out of range frame falling back to frame 1; `spriteDrawGrid` taking three rectangles out of the same file loaded as one wide still and a fourth out of a sprite `spriteLoadData` built from the file's bytes; `spriteGetFrames` on all three; `controllerHowMany`; `setOverlayOpacity` at 96 and back to 255; and the music family over `testScripts/tune.mp3` (six seconds of 330 Hz, so the built-in mp3 decoder is exercised) -- play looping, `musicSetVolume` at 128 and 16, `soundSetVolume` at 8 with a `soundLoadData` sound played through it to show the two volumes do not move each other, pause, resume, a 250 ms fade out, a play once and an unload. 16 screenshots |
|
| scene51.singe | 51 | 48.6 | Sprites, in-memory assets and music over the disc: `spriteRotateFrame` turning frames 1 to 4 of `testScripts/arrows.png` (a four frame strip whose arrows have a white notch, so a rotation is readable) at 0, 45, 135 and 225 degrees, drawn by `spriteDrawRotatedFrame` plain, at 1.5 and at 2 by 0.75, beside the same frame through `spriteDrawFrame` which ignores both; `spriteScale` before a rotation, so the pair honours the sprite's own scale; an out of range frame falling back to frame 1; `spriteDrawGrid` taking three rectangles out of the same file loaded as one wide still and a fourth out of a sprite `spriteLoadData` built from the file's bytes; `spriteGetFrames` on all three; `controllerHowMany`; `setOverlayOpacity` at 96 and back to 255; and the music family over `testScripts/tune.mp3` (six seconds of 330 Hz, so the built-in mp3 decoder is exercised) -- play looping, `musicSetVolume` at 128 and 16, `soundSetVolume` at 8 with a `soundLoadData` sound played through it to show the two volumes do not move each other, pause, resume, a 250 ms fade out, a play once and an unload. 16 screenshots |
|
||||||
|
| scene99.singe | 52 | 50 | Formats: a lossless JPEG XL of crate.png (crate.jxl) drawn beside the PNG, which it must match, and the chiptune and tracker decoders played as music and as a sound (chip.nsf, tracker.xm, tracker.it, tracker.s3m); the run's log says what played |
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
-- Fandango's shape. The rules, the dialogue, the verbs, and the parser are the 2D game's own,
|
-- Fandango's shape. The rules, the dialogue, the verbs, and the parser are the 2D game's own,
|
||||||
-- taken from its description; only the types and the rooms are new. That the rules do not
|
-- taken from its description; only the types and the rooms are new. That the rules do not
|
||||||
-- change between a painting and a scene is the point.
|
-- change between a painting and a scene is the point.
|
||||||
local base = dofile("testScripts/author/adventure2d.game")
|
local base = dofile("testScripts/author/adventure2d.forge")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title = "Author test: adventure in 3D",
|
title = "Author test: adventure in 3D",
|
||||||
34
testScripts/author/mash.forge
Normal file
34
testScripts/author/mash.forge
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
-- Mash and hold on the branching track: one window wants RIGHT six times, the next wants UP
|
||||||
|
-- held for twenty frames, the last wants RIGHT mashed again and gets too few.
|
||||||
|
return {
|
||||||
|
title = "Author test: mash and hold",
|
||||||
|
layers = { { kind = "disc", file = "Singe/menuBackground.mkv", start = 100 }, { kind = "overlay" } },
|
||||||
|
vars = { taken = 0, missed = 0, wanted = 0, opened = "" },
|
||||||
|
|
||||||
|
types = {
|
||||||
|
dirk = { look = { kind = "none" }, behaviours = { { kind = "branching", track = "moves" } } }
|
||||||
|
},
|
||||||
|
|
||||||
|
rooms = {
|
||||||
|
{ name = "cavern",
|
||||||
|
entities = { { type = "dirk", id = "dirk", x = 0, y = 0 } },
|
||||||
|
tracks = {
|
||||||
|
{ name = "moves", key = "frame",
|
||||||
|
branches = { { from = 120, to = 160, move = "RIGHT", mash = 6, success = 200, fail = 200 },
|
||||||
|
{ from = 200, to = 240, move = "UP", hold = 20, success = 280, fail = 280 },
|
||||||
|
{ from = 280, to = 320, move = "RIGHT", mash = 6, success = 340, fail = 340 } } }
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
|
||||||
|
rules = {
|
||||||
|
{ note = "A window opens: remember what it wants",
|
||||||
|
on = "branchOpen",
|
||||||
|
act = { { "setVar", name = "wanted", value = "event.mash or event.hold or 0" }, { "setVar", name = "opened", value = "opened .. event.move .. \" \"" } } },
|
||||||
|
{ note = "Made it",
|
||||||
|
on = "branchTaken",
|
||||||
|
act = { { "addVar", name = "taken", amount = 1 } } },
|
||||||
|
{ note = "Missed it",
|
||||||
|
on = "branchMissed",
|
||||||
|
act = { { "addVar", name = "missed", amount = 1 } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
47
testScripts/author/overlayBits.forge
Normal file
47
testScripts/author/overlayBits.forge
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
-- The pieces the ActionMax port asked for, over the menu video: a text look with a font, size,
|
||||||
|
-- and anchor of its own; a sprite drawn in copies (a row of coins); a crosshair on the pointer;
|
||||||
|
-- the disc paused and played by rules; and pointerX in an expression.
|
||||||
|
return {
|
||||||
|
title = "Author test: overlay bits",
|
||||||
|
players = 1,
|
||||||
|
size = { w = 720, h = 480 },
|
||||||
|
layers = { { kind = "disc", file = "Singe/menuBackground.mkv", start = 100 }, { kind = "overlay" } },
|
||||||
|
vars = { coins = 3, side = "", held = 0 },
|
||||||
|
|
||||||
|
types = {
|
||||||
|
title = { look = { kind = "text", text = "OVERLAY BITS", r = 255, g = 220, b = 80, font = "Singe/FreeSansBold.ttf", size = 36, anchor = "top" } },
|
||||||
|
foot = { look = { kind = "text", text = "standing on the bottom", r = 200, g = 200, b = 255, anchor = "feet" } },
|
||||||
|
mid = { look = { kind = "text", text = "", r = 255, g = 255, b = 255, anchor = "centre" } },
|
||||||
|
coins = { look = { kind = "sprite", file = "Forge/kit/coin.png", step = 40 }, vars = { copies = 3 } },
|
||||||
|
cross = { look = { kind = "box", w = 9, h = 9, r = 255, g = 80, b = 80 }, behaviours = { { kind = "pointer", player = 1 } } }
|
||||||
|
},
|
||||||
|
|
||||||
|
rooms = {
|
||||||
|
{ name = "board",
|
||||||
|
entities = {
|
||||||
|
{ type = "title", id = "title", x = 360, y = 10 },
|
||||||
|
{ type = "foot", id = "foot", x = 360, y = 480 },
|
||||||
|
{ type = "mid", id = "mid", x = 360, y = 240 },
|
||||||
|
{ type = "coins", id = "coins", x = 40, y = 40 },
|
||||||
|
{ type = "cross", id = "cross", x = 0, y = 0 }
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
|
||||||
|
rules = {
|
||||||
|
{ note = "The coins follow the var",
|
||||||
|
on = "frame",
|
||||||
|
act = { { "setVar", entity = "coins", name = "copies", value = "coins" } } },
|
||||||
|
{ note = "Which side the pointer is on",
|
||||||
|
on = "frame", when = { { "test", expr = "pointerX(1) < 360" } },
|
||||||
|
act = { { "setVar", name = "side", value = "\"left\"" }, { "setText", entity = "mid", text = "\"pointer on the \" .. side" } } },
|
||||||
|
{ note = "Or the other",
|
||||||
|
on = "frame", when = { { "test", expr = "pointerX(1) >= 360" } },
|
||||||
|
act = { { "setVar", name = "side", value = "\"right\"" }, { "setText", entity = "mid", text = "\"pointer on the \" .. side" } } },
|
||||||
|
{ note = "SPACE holds the disc",
|
||||||
|
on = "pressed", key = "SPACE",
|
||||||
|
act = { { "discPause" }, { "setVar", name = "held", value = "frame" } } },
|
||||||
|
{ note = "RETURN lets it go",
|
||||||
|
on = "pressed", key = "RETURN",
|
||||||
|
act = { { "discPlay" } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
32
testScripts/author/vocab.forge
Normal file
32
testScripts/author/vocab.forge
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
-- A game with a vocabulary of its own: the bob behaviour, the bobbing condition, and the setBob
|
||||||
|
-- action live in vocab.singe beside this file, not in Forge.
|
||||||
|
return {
|
||||||
|
title = "Author test: a game's own vocabulary",
|
||||||
|
players = 1,
|
||||||
|
size = { w = 720, h = 480 },
|
||||||
|
layers = { { kind = "overlay" } },
|
||||||
|
vocabulary = "vocab.singe",
|
||||||
|
vars = { seen = 0 },
|
||||||
|
|
||||||
|
types = {
|
||||||
|
buoy = { look = { kind = "box", w = 30, h = 30, r = 80, g = 200, b = 255 }, behaviours = { { kind = "bob", height = 40, speed = 3 } } },
|
||||||
|
rock = { look = { kind = "box", w = 30, h = 30, r = 120, g = 120, b = 120 } }
|
||||||
|
},
|
||||||
|
|
||||||
|
rooms = {
|
||||||
|
{ name = "sea",
|
||||||
|
entities = {
|
||||||
|
{ type = "buoy", id = "buoy", x = 200, y = 240 },
|
||||||
|
{ type = "rock", id = "rock", x = 500, y = 240 }
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
|
||||||
|
rules = {
|
||||||
|
{ note = "Counts the frames the buoy bobs",
|
||||||
|
on = "frame", when = { { "bobbing", entity = "buoy" } },
|
||||||
|
act = { { "setVar", name = "seen", value = "seen + 1" } } },
|
||||||
|
{ note = "SPACE makes it bob higher",
|
||||||
|
on = "pressed", key = "SPACE",
|
||||||
|
act = { { "setBob", entity = "buoy", height = 100 } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
47
testScripts/author/vocab.singe
Normal file
47
testScripts/author/vocab.singe
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
-- A game's own vocabulary: a behaviour, a condition, and an action this game alone knows,
|
||||||
|
-- written as Author.singe writes its own manifest entries. The description names this file
|
||||||
|
-- (vocabulary = "vocab.singe"); nothing else in Forge has heard of a bob.
|
||||||
|
|
||||||
|
-- A bob: the entity rises and falls on a sine wave about where it was put.
|
||||||
|
AUTHOR.behaviours.bob = {
|
||||||
|
help = "Rises and falls about its place, height pixels either way, speed times a second.",
|
||||||
|
params = { height = "number", speed = "number" },
|
||||||
|
attach = function(instance, params)
|
||||||
|
local _, y = authorPosition(instance)
|
||||||
|
|
||||||
|
instance.bob = { height = params.height or 20, speed = params.speed or 2, base = y, at = 0 }
|
||||||
|
end,
|
||||||
|
step = function(instance, dt)
|
||||||
|
local bob = instance.bob
|
||||||
|
local x, _, z = authorPosition(instance)
|
||||||
|
|
||||||
|
bob.at = bob.at + dt * bob.speed
|
||||||
|
nodeSetPosition(instance.node, x, bob.base + math.sin(bob.at) * bob.height, z)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Whether an instance bobs: a condition of this game's own.
|
||||||
|
AUTHOR.conditions.bobbing = {
|
||||||
|
help = "An instance has a bob.",
|
||||||
|
params = { entity = "entity" },
|
||||||
|
emit = function(p) return "vocabBobbing(" .. p.entity .. ")" end
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Sets how high an instance bobs: an action of this game's own.
|
||||||
|
AUTHOR.actions.setBob = {
|
||||||
|
help = "Changes how high an instance bobs.",
|
||||||
|
params = { entity = "entity", height = "number" },
|
||||||
|
emit = function(p) return "vocabSetBob(" .. p.entity .. ", " .. p.height .. ")" end
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function vocabBobbing(instance)
|
||||||
|
return (instance ~= nil) and (instance.bob ~= nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function vocabSetBob(instance, height)
|
||||||
|
if instance and instance.bob then
|
||||||
|
instance.bob.height = height
|
||||||
|
end
|
||||||
|
end
|
||||||
BIN
testScripts/chip.nsf
(Stored with Git LFS)
Normal file
BIN
testScripts/chip.nsf
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
testScripts/crate.jxl
(Stored with Git LFS)
Normal file
BIN
testScripts/crate.jxl
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -541,4 +541,15 @@ GAMES = {
|
||||||
DEVELOPER = "Test",
|
DEVELOPER = "Test",
|
||||||
PUBLISHER = "Test",
|
PUBLISHER = "Test",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
TITLE = "Formats: JPEG XL and Chiptunes",
|
||||||
|
SCRIPT = "testScripts/scene99.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "A JPEG XL picture (crate.jxl, lossless from crate.png) drawn beside its PNG, which it must match pixel for pixel, and the chiptune and tracker decoders exercised as music and as a sound: game-music-emu's NSF (chip.nsf), libxmp's XM, IT and S3M (tracker.*).",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
testScripts/ports/README
Normal file
30
testScripts/ports/README
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
Port comparisons (FORGE.md section 14): each pair runs an original game from the test library
|
||||||
|
and its Forge port with the same scripted inputs, on the deterministic clock, and prints TRACE
|
||||||
|
lines; the port passes when the two traces agree. They need the library beside the repo
|
||||||
|
(SINGE_LIBRARY, or ~/claude/singetest) and are not part of the reference sweep.
|
||||||
|
|
||||||
|
ActionMax: actionMaxOriginal.singe and actionMaxPort.singe, with
|
||||||
|
-v <library>/ActionMax/frame_<game>.txt and PORT_GAME=<game>
|
||||||
|
(38AmbushAlley, BlueThunder, Hydrosub2021, PopsGhostly, SonicFury);
|
||||||
|
the descriptions come from util/forgePortActionMax.py.
|
||||||
|
|
||||||
|
compare.sh <family> <game> runs a pair and says whether the traces agree:
|
||||||
|
testScripts/ports/compare.sh actionMax 38AmbushAlley
|
||||||
|
|
||||||
|
Karis: karisOriginal.singe and karisPort.singe, driven by karisDrive.singe, with
|
||||||
|
-v the video the game's games.dat names, PORT_GAME=<directory> and
|
||||||
|
PORT_SCRIPT=<main script>; the descriptions come from util/forgePortKaris.lua:
|
||||||
|
testScripts/ports/compare.sh karis TimeGal/Timegal.singe
|
||||||
|
|
||||||
|
ALG: algOriginal.singe and algPort.singe, driven by algDrive.singe, for the American
|
||||||
|
Laser Games titles of the Hypseus library (Mad Dog McCree, Mad Dog II, Crime
|
||||||
|
Patrol, Drug Wars, The Last Bounty Hunter, Space Pirates, both Who Shot Johnny
|
||||||
|
Rocks, and the typing edition of Mad Dog II), with the title's folder as the game:
|
||||||
|
testScripts/ports/compare.sh alg maddog-hd
|
||||||
|
PORT_FRAMES=<n> sets the run's length in engine frames (15000 by default) and
|
||||||
|
PORT_PERFECT=1 plays without a wrong or a missed shot, to walk the whole game.
|
||||||
|
The views hand the drive each level's shoot window and the first hitbox of the
|
||||||
|
frame (aimed at the overlap with the next frame's box, since a press is judged a
|
||||||
|
frame later), the targets of the games' menus and puzzles, and, for the typing
|
||||||
|
edition, the key the game waits for; the drive shoots, misses on schedule, coins
|
||||||
|
and starts, continues once, and types.
|
||||||
36
testScripts/ports/actionMaxInputs.singe
Normal file
36
testScripts/ports/actionMaxInputs.singe
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- The inputs both the original ActionMax emulator and its Forge port are driven with, by frame
|
||||||
|
-- of the script's own clock, so the two can be compared move for move (FORGE.md section 14.1).
|
||||||
|
-- A shot is a pointer position and a trigger pull. The title takes one pull, the menu one on
|
||||||
|
-- the left half (a standard game), and then the picture is shot at from a dozen places while
|
||||||
|
-- it plays.
|
||||||
|
PORT_INPUTS = {
|
||||||
|
{ at = 40, x = 300, y = 200 }, -- Title: pull to start.
|
||||||
|
{ at = 140, x = 100, y = 240 }, -- Menu: the left half, a standard game.
|
||||||
|
}
|
||||||
|
-- In .38 Ambush Alley the picture flickers with the sensor -- targets -- at 240,140 from disc
|
||||||
|
-- frame 2968, at 220,140 from 2591, and at 220,80 from 2577, all left of the sensor's own corner,
|
||||||
|
-- which the game reaches about 2740 frames in. Most shots go there, a few elsewhere, so good
|
||||||
|
-- and bad judgments both come up.
|
||||||
|
local TARGETS = { { x = 240, y = 140 }, { x = 220, y = 140 }, { x = 220, y = 80 } }
|
||||||
|
for shot = 1, 40 do
|
||||||
|
local target = TARGETS[(shot % 3) + 1]
|
||||||
|
local input = { at = 2740 + shot * 28, x = target.x, y = target.y }
|
||||||
|
|
||||||
|
if shot % 4 == 0 then
|
||||||
|
input.x, input.y = 40 + (shot * 97) % 240, 20 + (shot * 53) % 150
|
||||||
|
end
|
||||||
|
PORT_INPUTS[#PORT_INPUTS + 1] = input
|
||||||
|
end
|
||||||
|
PORT_LAST = 2740 + 41 * 28 + 200
|
||||||
|
|
||||||
|
|
||||||
|
-- The input due this frame, if any.
|
||||||
|
function portInputAt(frame)
|
||||||
|
for _, input in ipairs(PORT_INPUTS) do
|
||||||
|
if input.at == frame then
|
||||||
|
return input
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
52
testScripts/ports/actionMaxOriginal.singe
Normal file
52
testScripts/ports/actionMaxOriginal.singe
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
-- The original ActionMax emulator, driven by the shared inputs, tracing what a port must match:
|
||||||
|
-- its state, its counts, and the disc frame at every input and every change of state. Run
|
||||||
|
-- with -v <library>/ActionMax/frame_38AmbushAlley.txt; PORT_GAME names another of the five.
|
||||||
|
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest/ported")
|
||||||
|
local game = os.getenv("PORT_GAME") or "38AmbushAlley"
|
||||||
|
local script = library .. "/ActionMax/" .. game .. ".singe"
|
||||||
|
|
||||||
|
-- The inputs sit beside this script, wherever the engine was started from.
|
||||||
|
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
|
||||||
|
|
||||||
|
dofile(here .. "actionMaxInputs.singe")
|
||||||
|
|
||||||
|
-- The emulator finds its files by its own script's path, and draws sprites in the 2.10 order,
|
||||||
|
-- which its games.dat entry would have said.
|
||||||
|
SINGE_LEGACY_SPRITE_ARGS = true
|
||||||
|
local realScriptPath = singeGetScriptPath
|
||||||
|
singeGetScriptPath = function() return script end
|
||||||
|
dofile(script)
|
||||||
|
singeGetScriptPath = realScriptPath
|
||||||
|
|
||||||
|
local realUpdate = onOverlayUpdate
|
||||||
|
local frames = 0
|
||||||
|
local lastState = nil
|
||||||
|
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
local input = portInputAt(frames)
|
||||||
|
|
||||||
|
if input then
|
||||||
|
onMouseMoved(input.x, input.y, 0, 0)
|
||||||
|
onInputPressed(SWITCH_BUTTON3)
|
||||||
|
debugPrint(string.format("TRACE input %d at %d,%d: state %d frame %d shots %d good %d bad %d", frames, input.x, input.y, currentState, discGetFrame(), shotsFired, shotsGood, shotsBad))
|
||||||
|
end
|
||||||
|
local r = realUpdate()
|
||||||
|
|
||||||
|
if os.getenv("PORT_SENSOR_TRACE") and (frames >= tonumber(os.getenv("PORT_SENSOR_TRACE"))) and (frames < tonumber(os.getenv("PORT_SENSOR_TRACE")) + 12) then
|
||||||
|
debugPrint(string.format("SENSOR frame %d disc %d pulled %d spot %s/%s gun %s/%s mouse %d,%d", frames, discGetFrame(), triggerPulled, tostring(sensorLastState), tostring(sensorState), tostring(gunLastState), tostring(gunState), mouseX, mouseY))
|
||||||
|
end
|
||||||
|
-- The startup and setup states are the emulator's own housekeeping; the title is the first
|
||||||
|
-- state a port has.
|
||||||
|
if (currentState ~= lastState) and (currentState >= 2) then
|
||||||
|
lastState = currentState
|
||||||
|
debugPrint(string.format("TRACE state %d from frame %d: disc %d shots %d good %d bad %d", currentState, frames, discGetFrame(), shotsFired, shotsGood, shotsBad))
|
||||||
|
end
|
||||||
|
if frames >= PORT_LAST then
|
||||||
|
debugPrint(string.format("TRACE end: state %d disc %d shots %d good %d bad %d", currentState, discGetFrame(), shotsFired, shotsGood, shotsBad))
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return r
|
||||||
|
end
|
||||||
52
testScripts/ports/actionMaxPort.singe
Normal file
52
testScripts/ports/actionMaxPort.singe
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
-- The Forge port of an ActionMax game, driven by the same inputs as the original and tracing
|
||||||
|
-- the same things, in the original's terms: its state numbers stand for the port's rooms.
|
||||||
|
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest/ported")
|
||||||
|
local game = os.getenv("PORT_GAME") or "38AmbushAlley"
|
||||||
|
local STATES = { startup = 0, title = 2, menu = 3, intro = 4, playing = 5, gameOver = 6 }
|
||||||
|
|
||||||
|
dofile("Forge/AuthorCompile.singe")
|
||||||
|
-- The inputs sit beside this script, wherever the engine was started from.
|
||||||
|
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
|
||||||
|
|
||||||
|
dofile(here .. "actionMaxInputs.singe")
|
||||||
|
dofile(authorBuild(library .. "/ActionMax/" .. game .. ".forge", singeGetDataPath() .. game .. ".singe"))
|
||||||
|
|
||||||
|
local realUpdate = onOverlayUpdate
|
||||||
|
local frames = 0
|
||||||
|
local lastRoom = nil
|
||||||
|
|
||||||
|
|
||||||
|
local function counts()
|
||||||
|
return string.format("shots %d good %d bad %d", AUTHOR_VARS.shots, AUTHOR_VARS.good, AUTHOR_VARS.bad)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
local input = portInputAt(frames)
|
||||||
|
|
||||||
|
if input then
|
||||||
|
authorSetPointer(1, input.x, input.y, true)
|
||||||
|
authorSwitchDown(SWITCH_BUTTON3)
|
||||||
|
authorSwitchUp(SWITCH_BUTTON3)
|
||||||
|
debugPrint(string.format("TRACE input %d at %d,%d: state %d frame %d %s", frames, input.x, input.y, STATES[AUTHOR_ROOM.name] or -1, discGetFrame(), counts()))
|
||||||
|
end
|
||||||
|
local r = realUpdate()
|
||||||
|
|
||||||
|
if os.getenv("PORT_SENSOR_TRACE") and (frames >= tonumber(os.getenv("PORT_SENSOR_TRACE"))) and (frames < tonumber(os.getenv("PORT_SENSOR_TRACE")) + 12) then
|
||||||
|
local sensor = authorEntity("sensor")
|
||||||
|
local gx, gy = authorPointer(1)
|
||||||
|
|
||||||
|
debugPrint(string.format("SENSOR frame %d disc %d pulled %d spot %s gun %s now %s/%s pointer %d,%d", frames, discGetFrame(), sensor.pulled, tostring(sensor.spotWas), tostring(sensor.gunWas), authorPixelState(312, 205, 127, 20), authorPixelState(gx, gy, 127, 20), gx, gy))
|
||||||
|
end
|
||||||
|
if AUTHOR_ROOM.name ~= lastRoom then
|
||||||
|
lastRoom = AUTHOR_ROOM.name
|
||||||
|
debugPrint(string.format("TRACE state %d from frame %d: disc %d %s", STATES[AUTHOR_ROOM.name] or -1, frames, discGetFrame(), counts()))
|
||||||
|
end
|
||||||
|
if frames >= PORT_LAST then
|
||||||
|
debugPrint(string.format("TRACE end: state %d disc %d %s", STATES[AUTHOR_ROOM.name] or -1, discGetFrame(), counts()))
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return r
|
||||||
|
end
|
||||||
263
testScripts/ports/algDrive.singe
Normal file
263
testScripts/ports/algDrive.singe
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
-- Drives an American Laser Games title (the original or its port) from what the game shows,
|
||||||
|
-- the same way for both. The driver file defines what it takes: a view() of the game's state
|
||||||
|
-- and algPress(switch, device, down) / algPointer(device, x, y) to press a gun's trigger or
|
||||||
|
-- put its pointer; this file decides. The trace lines it prints are what compare.sh diffs.
|
||||||
|
--
|
||||||
|
-- The policy: the first gun skips the intro on frame 300 of the run and joins at the player
|
||||||
|
-- select; at the town select it points at the first unbeaten quadrant and shoots; in a level
|
||||||
|
-- it shoots the first hitbox of the current window's frame three frames into the window (a
|
||||||
|
-- miss well outside it on every fifth move, no shot on every seventh), reloads by pointing
|
||||||
|
-- offscreen when its ammo is out, and takes the continue.
|
||||||
|
|
||||||
|
ALG_FRAMES = tonumber(os.getenv("PORT_FRAMES")) or 60000
|
||||||
|
|
||||||
|
local answered = {} -- Windows shot at, by "level/move/lives".
|
||||||
|
local tried = {} -- Moves met before, by "level/move".
|
||||||
|
local frames = 0
|
||||||
|
local joined = false
|
||||||
|
local coined = false
|
||||||
|
local continued = false
|
||||||
|
local continueCoined = false
|
||||||
|
local seeks = 0
|
||||||
|
SWITCH_RETURN_KEY = 13 -- The return key's keysym, as the typing edition names it.
|
||||||
|
local wrongTyped = {} -- Moves already given their wrong letter.
|
||||||
|
local pendingRelease = nil -- { switch, device } pressed this frame, let go the next.
|
||||||
|
|
||||||
|
|
||||||
|
function algTraceSeek(frame)
|
||||||
|
seeks = seeks + 1
|
||||||
|
debugPrint(string.format("TRACE seek %d", frame))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- PORT_PERFECT=1 plays without a wrong or a missed shot, to walk the whole game in one run.
|
||||||
|
local perfect = (os.getenv("PORT_PERFECT") == "1")
|
||||||
|
|
||||||
|
-- PORT_SERVICE=1 opens the service screen at the attract loop before the game, steps the
|
||||||
|
-- pointer down it a row at a time with a shot on each (or, on a screen the stick drives, a
|
||||||
|
-- press and a step down in turn), then leaves it; the trace carries the option under the
|
||||||
|
-- cursor and every dip while the screen shows. PORT_SHOTS=1 takes a screenshot at the
|
||||||
|
-- attract loop, on the service screen, in a level just after a shot, at the continue, and
|
||||||
|
-- at the board, each once, named for the moment.
|
||||||
|
local walkService = (os.getenv("PORT_SERVICE") == "1")
|
||||||
|
local shots = (os.getenv("PORT_SHOTS") == "1")
|
||||||
|
local walked = false
|
||||||
|
local walkStep = 0
|
||||||
|
local walkAt = 0
|
||||||
|
local taken = {}
|
||||||
|
local shotAt = nil
|
||||||
|
|
||||||
|
|
||||||
|
local function shot(name)
|
||||||
|
if shots and not taken[name] then
|
||||||
|
taken[name] = true
|
||||||
|
singeScreenshot("shot-" .. name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function wrongMove(view)
|
||||||
|
return (not perfect) and (view.move % 5 == 0) and not view.retry
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function missedMove(view)
|
||||||
|
return (not perfect) and (view.move % 7 == 0) and (view.move % 5 ~= 0) and not view.retry
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Presses a gun's trigger for one frame.
|
||||||
|
local function tap(switch, device)
|
||||||
|
algPress(switch, device, true)
|
||||||
|
pendingRelease = { switch, device }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- view: { screen, step, move, frame, frames, score, lives, ammo, active, window = { from, to },
|
||||||
|
-- box = { x1, y1, x2, y2 } (the first hitbox of this frame, in the game's pointer
|
||||||
|
-- coordinates), quadrants = { {x, y}, ... } (the town select's unbeaten choices),
|
||||||
|
-- offscreen = { x, y } (a pointer position that counts as off the picture),
|
||||||
|
-- spare (bullets the game never judges, kept back), reloadSwitch (what reloads) }
|
||||||
|
function algDrive(view)
|
||||||
|
if pendingRelease then
|
||||||
|
algPress(pendingRelease[1], pendingRelease[2], false)
|
||||||
|
pendingRelease = nil
|
||||||
|
end
|
||||||
|
frames = frames + 1
|
||||||
|
local line = string.format("TRACE screen %s step %s move %s score %s lives %s ammo %s", tostring(view.screen), tostring(view.step), tostring(view.move), tostring(view.score), tostring(view.lives), tostring(view.ammo))
|
||||||
|
|
||||||
|
if view.service then
|
||||||
|
line = line .. " opt " .. tostring(view.opt) .. " dips " .. tostring(view.dips)
|
||||||
|
end
|
||||||
|
if line ~= lastLine then
|
||||||
|
local detail = ""
|
||||||
|
|
||||||
|
-- PORT_DEBUG=1 adds what the drive decides by: the window, the spare, the free shot, and
|
||||||
|
-- the pause.
|
||||||
|
if os.getenv("PORT_DEBUG") == "1" then
|
||||||
|
detail = string.format(" win %s spare %s free %s paused %s", view.window and (view.window[1] .. "-" .. view.window[2]) or "nil", tostring(view.spare), tostring(view.free), tostring(view.paused))
|
||||||
|
end
|
||||||
|
debugPrint(line .. detail .. " disc " .. tostring(view.frame))
|
||||||
|
lastLine = line
|
||||||
|
end
|
||||||
|
if frames >= ALG_FRAMES then
|
||||||
|
debugPrint("TRACE end after " .. frames .. " frames")
|
||||||
|
singeQuit()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- A typing game (the typing edition of Mad Dog II): the key the view names is released, one
|
||||||
|
-- a frame; with misses on, every fifth move gets a wrong letter first and every seventh is
|
||||||
|
-- left untyped (a death), each once per move. The continue is taken once, by the return key.
|
||||||
|
if view.typed then
|
||||||
|
if view.continuing then
|
||||||
|
if (not continued) and (frames % 30 == 0) then
|
||||||
|
continued = true
|
||||||
|
algKey(SWITCH_RETURN_KEY)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if view.typing then
|
||||||
|
local moveKey = tostring(view.screen) .. "/" .. tostring(view.move)
|
||||||
|
|
||||||
|
if (not perfect) and (view.move % 7 == 0) and (view.move % 5 ~= 0) and (view.typing ~= SWITCH_RETURN_KEY) then
|
||||||
|
-- Untyped: the game holds the disc and takes the life.
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if (not perfect) and (view.move % 5 == 0) and (view.typing ~= SWITCH_RETURN_KEY) and not wrongTyped[moveKey] then
|
||||||
|
wrongTyped[moveKey] = true
|
||||||
|
algKey((view.typing == 122) and 121 or 122)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
algKey(view.typing)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- The service screen, walked once before the game.
|
||||||
|
if walkService and not walked then
|
||||||
|
if view.service then
|
||||||
|
if (frames - walkAt) >= 30 then
|
||||||
|
walkAt = frames
|
||||||
|
walkStep = walkStep + 1
|
||||||
|
if walkStep == 3 then
|
||||||
|
shot("service")
|
||||||
|
end
|
||||||
|
if view.serviceKeys then
|
||||||
|
-- The stick's screen: a press on the option, then a step down; the last option
|
||||||
|
-- is the way out.
|
||||||
|
if walkStep % 2 == 1 then
|
||||||
|
tap(SWITCH_BUTTON1, 0)
|
||||||
|
else
|
||||||
|
tap(SWITCH_DOWN, 0)
|
||||||
|
end
|
||||||
|
if walkStep > 26 then
|
||||||
|
walked = true
|
||||||
|
end
|
||||||
|
elseif walkStep <= 9 then
|
||||||
|
algPointer(0, view.width * 0.5, view.height * (walkStep - 0.5) / 9)
|
||||||
|
tap(SWITCH_BUTTON3, 0)
|
||||||
|
else
|
||||||
|
walked = true
|
||||||
|
tap(SWITCH_BUTTON1, 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
elseif view.intro and (frames > 200) then
|
||||||
|
walkAt = frames
|
||||||
|
tap(SWITCH_SERVICE, nil)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if shots and view.intro and (frames == 330) then
|
||||||
|
shot("attract")
|
||||||
|
end
|
||||||
|
if shots and view.continuing then
|
||||||
|
shot("continue")
|
||||||
|
end
|
||||||
|
if shots and view.board then
|
||||||
|
shot("board")
|
||||||
|
end
|
||||||
|
if shotAt and (frames == shotAt + 3) then
|
||||||
|
shot("level")
|
||||||
|
end
|
||||||
|
-- The player select: START for the first gun, once.
|
||||||
|
if (view.screen == -2) and not joined and (frames > 60) then
|
||||||
|
joined = true
|
||||||
|
tap(SWITCH_START1, 0)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- The intro: a coin, then START begins a game (the tutorial or the first level by dip).
|
||||||
|
-- (A game with no player select, the one-player Johnny Rock, counts as joined from the start.)
|
||||||
|
if view.intro and (joined or view.noSelect) then
|
||||||
|
if not coined and (frames > 200) then
|
||||||
|
coined = true
|
||||||
|
tap(SWITCH_COIN1, nil)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if coined and (frames > 300) and (frames % 40 == 0) then
|
||||||
|
tap(SWITCH_START1, 0)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- The continue: taken once, with a coin first (Mad Dog II's continue wants a credit).
|
||||||
|
if view.continuing and not continued and (frames % 30 == 0) then
|
||||||
|
if not continueCoined then
|
||||||
|
continueCoined = true
|
||||||
|
tap(SWITCH_COIN1, nil)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
continued = true
|
||||||
|
tap(SWITCH_START1, 0)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- The town select: the pointer to the first unbeaten quadrant, then the trigger.
|
||||||
|
if (view.screen == 100) and view.quadrants and view.quadrants[1] and (view.step == 2) and (frames % 20 == 0) then
|
||||||
|
algPointer(0, view.quadrants[1][1], view.quadrants[1][2])
|
||||||
|
tap(SWITCH_BUTTON3, 0)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- A screen with targets and no windows (the signpost, the mine, Mad Dog II's menu, item, and
|
||||||
|
-- skull): the first target shot, every twenty frames, while its state waits for shots (the
|
||||||
|
-- running state unless the view names another).
|
||||||
|
if view.targets and view.targets[1] and (view.step == (view.targetStep or 2)) and (frames % 20 == 0) then
|
||||||
|
if (view.ammo ~= nil) and (view.ammo <= (view.spare or 0)) and view.offscreen then
|
||||||
|
algPointer(0, view.offscreen[1], view.offscreen[2])
|
||||||
|
tap(view.reloadSwitch or SWITCH_BUTTON3, 0)
|
||||||
|
else
|
||||||
|
algPointer(0, view.targets[1][1], view.targets[1][2])
|
||||||
|
tap(SWITCH_BUTTON3, 0)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- A level: ammo out (or down to the spare an edition never judges) means a reload by pointing
|
||||||
|
-- offscreen and pressing the switch the view names (the trigger, or a reload button).
|
||||||
|
if view.window and (view.ammo ~= nil) and (view.ammo <= (view.spare or 0)) and view.offscreen then
|
||||||
|
algPointer(0, view.offscreen[1], view.offscreen[2])
|
||||||
|
tap(view.reloadSwitch or SWITCH_BUTTON3, 0)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if view.window and view.box then
|
||||||
|
-- (A move is answered once per life, and once more per life in the game the continue
|
||||||
|
-- starts over.)
|
||||||
|
local moveKey = tostring(view.screen) .. "/" .. tostring(view.move)
|
||||||
|
local key = moveKey .. "/" .. tostring(view.lives) .. (continued and "/c" or "")
|
||||||
|
|
||||||
|
view.retry = (tried[moveKey] ~= nil)
|
||||||
|
if (view.frame >= view.window[1] + 3) and (view.frame <= view.window[2]) and not answered[key] then
|
||||||
|
answered[key] = true
|
||||||
|
tried[moveKey] = true
|
||||||
|
if missedMove(view) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local cx, cy = (view.box[1] + view.box[3]) / 2, (view.box[2] + view.box[4]) / 2
|
||||||
|
|
||||||
|
if wrongMove(view) then
|
||||||
|
cx, cy = view.box[3] + 60, view.box[4] + 60
|
||||||
|
end
|
||||||
|
algPointer(0, cx, cy)
|
||||||
|
tap(SWITCH_BUTTON3, 0)
|
||||||
|
if shotAt == nil then
|
||||||
|
shotAt = frames
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
515
testScripts/ports/algOriginal.singe
Normal file
515
testScripts/ports/algOriginal.singe
Normal file
|
|
@ -0,0 +1,515 @@
|
||||||
|
-- An American Laser Games title as it ships, driven by algDrive.singe and traced. PORT_GAME
|
||||||
|
-- names the title's folder under the library and PORT_SCRIPT its script under it (the Hypseus
|
||||||
|
-- layout: singe/<Title>/<Title>.singe); run with -v the title's framefile.
|
||||||
|
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
|
||||||
|
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest/hypseus")
|
||||||
|
local game = os.getenv("PORT_GAME") or "maddog-hd"
|
||||||
|
local script = library .. "/" .. game .. "/" .. (os.getenv("PORT_SCRIPT") or ("singe/" .. game .. "/" .. game .. ".singe"))
|
||||||
|
|
||||||
|
dofile(here .. "algDrive.singe")
|
||||||
|
|
||||||
|
SINGE_LEGACY_SPRITE_ARGS = true
|
||||||
|
local realTime = os.time
|
||||||
|
os.time = function(...) return 12345 end
|
||||||
|
local realScriptPath = singeGetScriptPath
|
||||||
|
singeGetScriptPath = function() return script end
|
||||||
|
local realSkip = discSkipToFrame
|
||||||
|
discSkipToFrame = function(frame)
|
||||||
|
algTraceSeek(frame)
|
||||||
|
return realSkip(frame)
|
||||||
|
end
|
||||||
|
local realSearch = discSearch
|
||||||
|
discSearch = function(frame)
|
||||||
|
algTraceSeek(frame)
|
||||||
|
return realSearch(frame)
|
||||||
|
end
|
||||||
|
-- One mouse: a single gun joins by START, as the game does with one mouse.
|
||||||
|
local realMice = mouseHowMany
|
||||||
|
mouseHowMany = function() return 1 end
|
||||||
|
dofile(script)
|
||||||
|
singeGetScriptPath = realScriptPath
|
||||||
|
|
||||||
|
local realUpdate = onOverlayUpdate
|
||||||
|
|
||||||
|
-- PORT_HOOK=1 names the line the script is on every ten million instructions: where a run
|
||||||
|
-- that never ends is spinning.
|
||||||
|
if os.getenv("PORT_HOOK") == "1" then
|
||||||
|
debug.sethook(function()
|
||||||
|
local where = debug.getinfo(2, "Sl")
|
||||||
|
|
||||||
|
debugPrint("HOOK " .. tostring(where and where.short_src) .. ":" .. tostring(where and where.currentline))
|
||||||
|
end, "", 10000000)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function algPress(switch, device, down)
|
||||||
|
if down then
|
||||||
|
onInputPressed(switch, device)
|
||||||
|
else
|
||||||
|
onInputReleased(switch, device)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A key released, as the typing edition reads it (its keysym, in MODE_FULL).
|
||||||
|
function algKey(keysym)
|
||||||
|
onInputReleased(keysym)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The pointer as the game's onMouseMoved computes it: a raw position that lands on the point
|
||||||
|
-- asked for, in the game's own coordinates.
|
||||||
|
function algPointer(device, x, y)
|
||||||
|
local rx = (x + (ratioxOffset or 0)) / ((ratiox or 1) * (gunscale or 1))
|
||||||
|
local ry = (y + (ratioyOffset or 0)) / ((ratioy or 1) * (gunscale or 1))
|
||||||
|
|
||||||
|
onMouseMoved(math.floor(rx), math.floor(ry), 0, 0, device)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The hitmap's coordinates scaled as the game scales them, where it does (the multiplayer
|
||||||
|
-- editions); the one-player editions aim in the hitmap's own.
|
||||||
|
local function hmx(v)
|
||||||
|
if ResHMx then
|
||||||
|
return ResHMx(v)
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function hmy(v)
|
||||||
|
if ResHMy then
|
||||||
|
return ResHMy(v)
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The hitboxes of a frame in the current move's hitmap range, in pointer coordinates.
|
||||||
|
local function boxesAt(rel)
|
||||||
|
local boxes = {}
|
||||||
|
|
||||||
|
for k = (move[currentMove][hitmapStart or hitboxStart] or 1), (hitmapTotal or 0) do
|
||||||
|
local row = hitmap[k]
|
||||||
|
|
||||||
|
if row and (row[hitmapFrame] == rel) and (row[hitmapCount] > 0) then
|
||||||
|
for j = row[hitmapIndex], row[hitmapIndex] + row[hitmapCount] - 1 do
|
||||||
|
local b = hitbox[j]
|
||||||
|
|
||||||
|
-- (A box given back to front -- Platoon's hitbox lines are, often -- can never be
|
||||||
|
-- hit by the game's own test, so it is no aim.)
|
||||||
|
if b and (b[bbx1] <= b[bbx2]) and (b[bby1] <= b[bby2]) then
|
||||||
|
boxes[#boxes + 1] = { hmx(b[bbx1]), hmy(b[bby1]), hmx(b[bbx2]), hmy(b[bby2]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return boxes
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function firstBox()
|
||||||
|
-- The first hitbox of this frame, aimed at a point that also lies inside a box of the next
|
||||||
|
-- frame when the two overlap: the press the drive makes now is judged on this frame or the
|
||||||
|
-- next, as the clock falls. Without an overlap (a fast target) the box of this frame stands.
|
||||||
|
if (move == nil) or (move[currentMove] == nil) or (hitmap == nil) then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local rel = currentFrame - (thisOffset or 0)
|
||||||
|
local now = boxesAt(rel)
|
||||||
|
local b = now[1]
|
||||||
|
|
||||||
|
if b == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
for _, n in ipairs(boxesAt(rel + 1)) do
|
||||||
|
local x1, y1, x2, y2 = math.max(b[1], n[1]), math.max(b[2], n[2]), math.min(b[3], n[3]), math.min(b[4], n[4])
|
||||||
|
|
||||||
|
if (x1 <= x2) and (y1 <= y2) then
|
||||||
|
return { x1, y1, x2, y2 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return b
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function view()
|
||||||
|
local window = nil
|
||||||
|
|
||||||
|
-- A level of Mad Dog's (101 to 121) or of Mad Dog II's (its trails, and levelStart); Mad Dog II's
|
||||||
|
-- move rows have no shoot window of their own, so the move's frames stand in.
|
||||||
|
local inLevel = (currentLevel ~= nil) and (((frameShootStart ~= nil) and (currentLevel >= 101) and (currentLevel <= 121)) or ((frameShootStart == nil) and (((currentLevel >= 1) and (currentLevel <= 30)) or (currentLevel == levelStart))))
|
||||||
|
|
||||||
|
if ((practiceHit ~= nil) or (fRndPopOrder ~= nil)) and (currentLevel == levelPractice) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
if (levelSigns ~= nil) and (currentLevel == levelSigns) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
if (levelTieIntro ~= nil) and ((currentLevel == levelTie) or (currentLevel == levelTieIntro)) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Space Pirates's puzzles and crystals are targets, not windows: the band and Ursula's colour
|
||||||
|
-- at the doors, Fellina's skull and arrow, the sockets of the crystal build.
|
||||||
|
if (levelCrystalBuild ~= nil) and ((currentLevel == levelCrystalBuild) or ((currentLevel == levelDoor2) and (lvlState == lvlPlayClip4)) or ((currentLevel == levelDoor3) and (lvlState == lvlPlayClip3)) or ((currentLevel == levelFellina1) and ((lvlState == lvlPlayPuzzle1) or (lvlState == lvlPlayPuzzle2)))) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Who Shot Johnny Rock's store, clue items, and safe are targets, not windows.
|
||||||
|
if (GetMapArray01 ~= nil) and ((currentLevel == levelStore) or (currentLevel == levelMansion2) or (currentLevel == levelMansion4)) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Space Pirates's Reaper wants exactly as many hits as he laughed: once the drive has them,
|
||||||
|
-- the moves left are let pass.
|
||||||
|
if (levelReaper2 ~= nil) and (currentLevel == levelReaper2) and (currentMove > 1) and (iReaperHits >= iReaperLaughs) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The typing edition of Mad Dog II plays on the keyboard: the key the game waits for now (the
|
||||||
|
-- next letter of the word, a sprinkled letter, the return key at a menu), and no gun at all.
|
||||||
|
local typing = nil
|
||||||
|
|
||||||
|
if validKey ~= nil then
|
||||||
|
inLevel = false
|
||||||
|
local function nextOf(word)
|
||||||
|
if word and (iKeyIndex or 1) <= string.len(word) then
|
||||||
|
return string.byte(word, iKeyIndex or 1)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
-- (The game starts with the key slot at zero, not at none, and never clears it.)
|
||||||
|
local waiting = (iKey == SWITCH_NONE) or (iKey == 0)
|
||||||
|
|
||||||
|
if not waiting then
|
||||||
|
typing = nil
|
||||||
|
elseif (currentLevel == levelTitle) and (lvlState == lvlRunning) then
|
||||||
|
typing = SWITCH_RETURN
|
||||||
|
elseif (currentLevel == levelMenu) and (lvlState == lvlRunning) then
|
||||||
|
typing = SWITCH_RETURN
|
||||||
|
elseif (currentLevel == levelHighScore) and (lvlState == lvlRunning) then
|
||||||
|
-- (Once, before the name's frames run out; the return key seeks to the entry's end.)
|
||||||
|
if discGetFrame() < offsetMenus + 3275 then
|
||||||
|
typing = SWITCH_RETURN
|
||||||
|
end
|
||||||
|
elseif bShowSprinkles and sprinkles then
|
||||||
|
for k = 1, (iSprinkleMax or 0) do
|
||||||
|
if sprinkles[k] and (sprinkles[k][4] == true) then
|
||||||
|
typing = string.byte(sprinkles[k][3])
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (currentLevel == levelItem1) and (lvlState == lvlPlayClip3) then
|
||||||
|
typing = nextOf(sMagicWord)
|
||||||
|
elseif move and move[currentMove] and ((lvlState == lvlPauseAction) or ((lvlState == lvlRunning) and (discGetFrame() >= move[currentMove][moveFrameStart]) and (discGetFrame() < move[currentMove][moveFrameEnd]))) then
|
||||||
|
typing = nextOf(move[currentMove][(currentLevel == levelItem2) and moveType or wordval])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Platoon's patrols open with a clip the first press of the trigger jumps past, whatever the
|
||||||
|
-- state: no window until that press is in, and a target anywhere to make it.
|
||||||
|
local skipping = (levelCharlie ~= nil) and bOnce and (currentLevel ~= nil) and (currentLevel >= 1) and (currentLevel <= 6) and (lvlState == lvlRunning)
|
||||||
|
|
||||||
|
if skipping then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A good guy's move (the Crime Patrol family marks the row) is no window: shooting him is a
|
||||||
|
-- death, and the drive would take it at every level that has one.
|
||||||
|
if inLevel and move and move[currentMove] and (rndMoveType ~= nil) and (GOODGUY ~= nil) and (move[currentMove][rndMoveType] == GOODGUY) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
if inLevel and move and move[currentMove] then
|
||||||
|
window = { move[currentMove][frameShootStart or moveFrameStart], move[currentMove][frameShootEnd or moveFrameEnd] }
|
||||||
|
end
|
||||||
|
local quadrants = nil
|
||||||
|
|
||||||
|
if (currentLevel == levelTownSelect) and stage then
|
||||||
|
quadrants = {}
|
||||||
|
local w, h = resX or 360, resY or 240
|
||||||
|
|
||||||
|
-- In the order the game itself takes when the select times out: the saloon before the
|
||||||
|
-- sheriff, whose level needs the key from it.
|
||||||
|
if not stage[STAGE_CORRAL] then quadrants[#quadrants + 1] = { w * 0.25, h * 0.25 } end
|
||||||
|
if not stage[STAGE_SALOON] then quadrants[#quadrants + 1] = { w * 0.75, h * 0.25 } end
|
||||||
|
if not stage[STAGE_SHERIFF] then quadrants[#quadrants + 1] = { w * 0.25, h * 0.75 } end
|
||||||
|
if not stage[STAGE_BANK] then quadrants[#quadrants + 1] = { w * 0.75, h * 0.75 } end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The signpost's sign and the mine's lantern and item, as a list of points to shoot in
|
||||||
|
-- order (the middle line of each, scaled as the game scales them).
|
||||||
|
local targets = nil
|
||||||
|
local targetStep = nil
|
||||||
|
local function lineMiddle(rows, count)
|
||||||
|
local row = rows and rows[math.floor((count + 1) / 2)]
|
||||||
|
|
||||||
|
if row then
|
||||||
|
return { hmx((row[lineStartX] + row[lineEndX]) / 2), hmy(row[lineY]) }
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if skipping then
|
||||||
|
targets = { { 180, 120 } }
|
||||||
|
targetStep = lvlRunning
|
||||||
|
elseif (currentLevel == levelSignSelect) and sign then
|
||||||
|
targets = {}
|
||||||
|
if stage[STAGE_FUSE] == false then
|
||||||
|
targets[1] = lineMiddle(sign[SIGN_PROSPECTOR], 48)
|
||||||
|
elseif stage[STAGE_MINE] == false then
|
||||||
|
targets[1] = lineMiddle(sign[SIGN_MINE], 28)
|
||||||
|
else
|
||||||
|
targets[1] = lineMiddle(sign[SIGN_HIDEOUT], 58)
|
||||||
|
end
|
||||||
|
elseif (currentLevel == levelMine) and mine then
|
||||||
|
targets = {}
|
||||||
|
local counts = { [MINE_SIGN] = 21, [MINE_LANTERN] = 20, [MINE_PAN] = 12, [MINE_PITCHFORK] = 23, [MINE_SKULL] = 25 }
|
||||||
|
|
||||||
|
if not bLanternHit then targets[#targets + 1] = lineMiddle(mine[MINE_LANTERN], counts[MINE_LANTERN]) end
|
||||||
|
if not bItemHit then targets[#targets + 1] = lineMiddle(mine[iWhatToHit], counts[iWhatToHit]) end
|
||||||
|
elseif (currentLevel == levelMaze) and maze and maze[curMazePos] and (maze[curMazePos][mazeType] > 0) and sign then
|
||||||
|
-- A maze sign: the sign for the way to go.
|
||||||
|
local frame, dir = maze[curMazePos][signType], maze[curMazePos][signDirection]
|
||||||
|
local counts = { [32] = { 51, 49 }, [175] = { 59, 17 }, [340] = { 67, 90 }, [500] = { 24, 42 }, [630] = { 11, 30 }, [446] = { 59, 64 } }
|
||||||
|
local pair = counts[frame]
|
||||||
|
|
||||||
|
if pair and sign[dir] then
|
||||||
|
targets = { lineMiddle(sign[dir], pair[dir]) }
|
||||||
|
end
|
||||||
|
elseif (levelItem1 ~= nil) and (ResHMx ~= nil) and (currentLevel == levelItem1) and (lvlState == lvlPlayClip3) then
|
||||||
|
-- Mad Dog II's first item level: the item the guide asked for, from the game's own
|
||||||
|
-- rectangles.
|
||||||
|
local boxes = { [ITEM_LANTERN] = { 57, 66, 117, 129 }, [ITEM_AMMOBOX] = { 120, 148, 111, 132 }, [ITEM_MOONSHINE] = { 260, 272, 60, 72 } }
|
||||||
|
local b = boxes[iThisItem]
|
||||||
|
|
||||||
|
if b then
|
||||||
|
targets = { { hmx((b[1] + b[2]) / 2), hmy((b[3] + b[4]) / 2) } }
|
||||||
|
targetStep = lvlPlayClip3
|
||||||
|
end
|
||||||
|
elseif (levelProspector1 ~= nil) and (ResHMx ~= nil) and (currentLevel == levelProspector1) and (lvlState == lvlPlayClip4) then
|
||||||
|
-- Mad Dog II's prospector: the skull he chose, from the game's own rectangles.
|
||||||
|
local boxes = { { 305, 315, 59, 76 }, { 68, 75, 73, 82 }, { 147, 154, 58, 67 }, { 119, 127, 101, 115 } }
|
||||||
|
local b = boxes[iSkull] or boxes[4]
|
||||||
|
|
||||||
|
targets = { { hmx((b[1] + b[2]) / 2), hmy((b[3] + b[4]) / 2) } }
|
||||||
|
targetStep = lvlPlayClip4
|
||||||
|
elseif (panelHit ~= nil) and (currentLevel == levelMenu) and (lvlState == lvlRunning) and area then
|
||||||
|
-- Crime Patrol's mission menus: the first panel whose stage is open, by the middle line
|
||||||
|
-- of its area (the count of lines is the game's own).
|
||||||
|
local MENUS = {}
|
||||||
|
|
||||||
|
if MENU_SIERRA ~= nil then
|
||||||
|
MENUS[MENU_SIERRA] = { { PANEL_PRACTICE, STAGE_PRACTICE, 99 }, { PANEL_BAR, STAGE_BAR, 99 }, { PANEL_CARCHASE, STAGE_CARCHASE, 99 }, { PANEL_HIDEOUT, STAGE_HIDEOUT, 99 } }
|
||||||
|
MENUS[MENU_CHICAGO] = { { PANEL_HELI, STAGE_HELI, 111 }, { PANEL_COURTROOM, STAGE_COURTROOM, 111 }, { PANEL_BUSCHASE, STAGE_BUSCHASE, 111 } }
|
||||||
|
MENUS[MENU_BORDER] = { { PANEL_SHIPYARD, STAGE_SHIPYARD, 111 }, { PANEL_BOATHOUSE, STAGE_BOATHOUSE, 111 }, { PANEL_BEACH, STAGE_BEACH, 111 } }
|
||||||
|
MENUS[MENU_SA] = { { PANEL_BASE, STAGE_BASE, 111 }, { PANEL_NIGHTVISION, STAGE_NIGHTVISION, 111 }, { PANEL_TOWN, STAGE_TOWN, 111 } }
|
||||||
|
end
|
||||||
|
if MENU_ROOKIE ~= nil then
|
||||||
|
MENUS = { [MENU_ROOKIE] = { { PANEL_PRACTICE, STAGE_PRACTICE, 98 }, { PANEL_STORE, STAGE_STORE, 99 }, { PANEL_GANG, STAGE_GANG, 99 }, { PANEL_WAREHOUSE, STAGE_WAREHOUSE, 97 } },
|
||||||
|
[MENU_UNDERCOVER] = { { PANEL_AIRPORT, STAGE_AIRPORT, 104 }, { PANEL_GARAGE, STAGE_GARAGE, 104 }, { PANEL_STRIPCLUB, STAGE_STRIPCLUB, 104 } },
|
||||||
|
[MENU_SWAT] = { { PANEL_DRUGS1, STAGE_DRUGS1, 111 }, { PANEL_DRUGS2, STAGE_DRUGS2, 111 }, { PANEL_BANK, STAGE_ROBBERY, 103 } },
|
||||||
|
[MENU_DELTA] = { { PANEL_HANGAR, STAGE_HANGAR, 108 }, { PANEL_AMBUSH, STAGE_AMBUSH, 103 }, { PANEL_AIRTERRORISTS, STAGE_AIRTERRORISTS, 109 } } }
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, entry in ipairs(MENUS[iCurrentMenu] or {}) do
|
||||||
|
if not panel[entry[2]] then
|
||||||
|
targets = { lineMiddle(area[entry[1]], entry[3]) }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (GetArrayPanels ~= nil) and (currentLevel == levelMenu) and (lvlState == lvlRunning) and area then
|
||||||
|
-- The Last Bounty Hunter's wanted posters: the first outlaw still at large, by the middle of
|
||||||
|
-- his panel's rectangle.
|
||||||
|
for _, stage in ipairs({ STAGE_HARRY, STAGE_DAN, STAGE_LOCO, STAGE_CACTUS }) do
|
||||||
|
local r = (panel[stage] == ATLARGE) and area[stage] and area[stage][1]
|
||||||
|
|
||||||
|
if r then
|
||||||
|
targets = { { hmx((r[lineStartX] + r[lineEndX]) / 2), hmy((r[lineStartY] + r[lineEndY]) / 2) } }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (levelSigns ~= nil) and (currentLevel == levelSigns) and (lvlState == lvlEnd) then
|
||||||
|
-- The Last Bounty Hunter's signs: the practice range, in the game's own pointer coordinates.
|
||||||
|
targets = { { 175, 101 } }
|
||||||
|
targetStep = lvlEnd
|
||||||
|
elseif (iLastMove ~= nil) and (levelFinal ~= nil) and (currentLevel == levelFinal) and (lvlState == lvlPlayClip2) then
|
||||||
|
-- Drug Wars's last stand: the tank, the blanket, or the gas cans, in the game's own
|
||||||
|
-- pointer coordinates.
|
||||||
|
local STAND = { { 262, 329, 133, 172 }, { 101, 111, 160, 188 }, { 209, 243, 201, 215 } }
|
||||||
|
local b = STAND[iLastMove] or STAND[3]
|
||||||
|
|
||||||
|
targets = { { (b[1] + b[2]) / 2, (b[3] + b[4]) / 2 } }
|
||||||
|
targetStep = lvlPlayClip2
|
||||||
|
elseif (practiceHit ~= nil) and (currentLevel == levelPractice) and (lvlState == lvlRunning) then
|
||||||
|
-- Crime Patrol's practice range: the first standing target of the still, in the game's
|
||||||
|
-- own pointer coordinates, or the cutout of the move (the middle line of its outline).
|
||||||
|
if currentMove == 0 then
|
||||||
|
local STILL = { { "bTopRight", 261, 300, 9, 60 }, { "bTopLeft", 78, 119, 70, 120 }, { "bMiddle", 205, 243, 70, 123 }, { "bBottomLeft", 120, 158, 124, 181 }, { "bBottomRight", 268, 297, 132, 183 } }
|
||||||
|
|
||||||
|
for _, t in ipairs(STILL) do
|
||||||
|
if _G[t[1]] then
|
||||||
|
targets = { { (t[2] + t[3]) / 2, (t[4] + t[5]) / 2 } }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (currentMove >= 1) and move and move[currentMove] and target then
|
||||||
|
local kind = move[currentMove][rndMoveType]
|
||||||
|
local counts = { [BADGUY01] = 98, [CIV01] = 82, [BADGUY02] = 97, [CIV02] = 107, [BADGUY03] = 79, [CIV03] = 148 }
|
||||||
|
|
||||||
|
if kind and counts[kind] and target[kind] then
|
||||||
|
targets = { lineMiddle(target[kind], counts[kind]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (levelFirePractice ~= nil) and (currentLevel == levelMenu) and (lvlState == lvlRunning) and panel then
|
||||||
|
-- Space Pirates's planet menu: the first panel not yet done, in the order the game takes
|
||||||
|
-- when the clip runs out, by the middle of its quadrant (pointer coordinates scaled as the
|
||||||
|
-- game scales them).
|
||||||
|
local PANELS = { { STAGE_FELLINA, 807, 1280, 565, 916 }, { STAGE_REAPER, 245, 720, 565, 916 }, { STAGE_SCRAPYARD, 807, 1280, 100, 450 }, { STAGE_MOUNTAIN, 245, 720, 100, 450 } }
|
||||||
|
|
||||||
|
for _, p in ipairs(PANELS) do
|
||||||
|
if panel[p[1]] == false then
|
||||||
|
targets = { { UnX * (p[2] + p[3]) / 2, UnY * (p[4] + p[5]) / 2 } }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (levelDoor2 ~= nil) and (currentLevel == levelDoor2) and (lvlState == lvlPlayClip4) and band then
|
||||||
|
-- Space Pirates's band of colour: the right one, within its frames, by the middle of its
|
||||||
|
-- line.
|
||||||
|
local row = band[iBand] and band[iBand][1]
|
||||||
|
|
||||||
|
if row and (discGetFrame() >= bandFrameStart + 3) and (discGetFrame() < bandFrameEnd) then
|
||||||
|
targets = { { hmx((row[lineStartX] + row[lineEndX]) / 2), hmy((row[lineStartY] + row[lineEndY]) / 2) } }
|
||||||
|
targetStep = lvlPlayClip4
|
||||||
|
end
|
||||||
|
elseif (levelDoor3 ~= nil) and (currentLevel == levelDoor3) and (lvlState == lvlPlayClip3) and (ursie or (ursulaHit ~= nil)) then
|
||||||
|
-- Space Pirates's commander: Ursula shot within her colour's frames.
|
||||||
|
local WINDOWS = { [COLOR_RED] = { 2243, 2306 }, [COLOR_GREEN] = { 2312, 2371 }, [COLOR_BLUE] = { 2377, 2439 } }
|
||||||
|
local w = WINDOWS[iUrsulaColor]
|
||||||
|
local rel = discGetFrame() - thisOffset
|
||||||
|
|
||||||
|
if w and (rel >= w[1] + 3) and (rel <= w[2] - 3) then
|
||||||
|
-- (The one-player edition builds her rectangles inside its hit test, so before the first
|
||||||
|
-- shot the game's own numbers stand in.)
|
||||||
|
local box = (ursie and ursie[1]) or { 152, 137, 210, 238 }
|
||||||
|
|
||||||
|
targets = { { hmx((box[1] + box[3]) / 2), hmy((box[2] + box[4]) / 2) } }
|
||||||
|
targetStep = lvlPlayClip3
|
||||||
|
end
|
||||||
|
elseif (levelFellina1 ~= nil) and (currentLevel == levelFellina1) and ((lvlState == lvlPlayPuzzle1) or (lvlState == lvlPlayPuzzle2)) and thingie then
|
||||||
|
-- Space Pirates's Fellina puzzles: the skull she chose, then the arrow, by the middle of
|
||||||
|
-- the game's own line.
|
||||||
|
local which = (lvlState == lvlPlayPuzzle1) and iFellinaSkull or iFellinaArrow
|
||||||
|
local row = thingie[which] and thingie[which][1]
|
||||||
|
|
||||||
|
if row and ((lvlState == lvlPlayPuzzle2) or (discGetFrame() >= iSkullStart + 3)) then
|
||||||
|
targets = { { hmx((row[lineStartX] + row[lineEndX]) / 2), hmy((row[lineStartY] + row[lineEndY]) / 2) } }
|
||||||
|
targetStep = lvlState
|
||||||
|
end
|
||||||
|
elseif (levelCrystalBuild ~= nil) and (currentLevel == levelCrystalBuild) and (lvlState == lvlRunning) and hole and socket then
|
||||||
|
-- Space Pirates's crystal build: the next socket in the order shown.
|
||||||
|
local ORDERS = { [ORDER_RGB] = { SOCKET_RED, SOCKET_GREEN, SOCKET_BLUE }, [ORDER_GBR] = { SOCKET_GREEN, SOCKET_BLUE, SOCKET_RED }, [ORDER_BRG] = { SOCKET_BLUE, SOCKET_RED, SOCKET_GREEN } }
|
||||||
|
|
||||||
|
for _, which in ipairs(ORDERS[iCrystalOrder] or {}) do
|
||||||
|
if not socket[which] then
|
||||||
|
local row = hole[which] and hole[which][1]
|
||||||
|
|
||||||
|
if row then
|
||||||
|
targets = { { hmx((row[lineStartX] + row[lineEndX]) / 2), hmy((row[lineStartY] + row[lineEndY]) / 2) } }
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (GetMapArray01 ~= nil) and (currentLevel == levelMenu) and (lvlState == lvlRunning) and stage then
|
||||||
|
-- Who Shot Johnny Rock's city map: the killer's place once revealed, the mansion once the
|
||||||
|
-- clues are all had, else the first place still open (the casino, the pool hall, the
|
||||||
|
-- warehouse, the garage), by the middle of its panel.
|
||||||
|
local which = nil
|
||||||
|
|
||||||
|
if bKillerRevealed then
|
||||||
|
which = ({ [JILL] = PLACE_CASINO, [MUMPS] = PLACE_POOL, [MEASLES] = PLACE_WAREHOUSE, [POX] = PLACE_GARAGE })[iKiller]
|
||||||
|
elseif bGotAllClues then
|
||||||
|
which = PLACE_MANSION
|
||||||
|
elseif not stage[levelCasino] then
|
||||||
|
which = PLACE_CASINO
|
||||||
|
elseif not stage[levelPool] then
|
||||||
|
which = PLACE_POOL
|
||||||
|
elseif not (stage[levelWarehouse1] and stage[levelWarehouse2] and stage[levelWarehouse3]) then
|
||||||
|
which = PLACE_WAREHOUSE
|
||||||
|
elseif not stage[levelGarage] then
|
||||||
|
which = PLACE_GARAGE
|
||||||
|
end
|
||||||
|
if which then
|
||||||
|
GetMapArray01()
|
||||||
|
local row = place[which] and place[which][1]
|
||||||
|
|
||||||
|
if row then
|
||||||
|
targets = { { hmx((row[lineStartX] + row[lineEndX]) / 2), hmy((row[lineStartY] + row[lineEndY]) / 2) } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (GetMapArray01 ~= nil) and (currentLevel == levelMansion2) and (lvlState == lvlRunning) and clue and thisItem then
|
||||||
|
-- Who Shot Johnny Rock's clue items: the item of the clue asked for, in the game's second
|
||||||
|
-- scale.
|
||||||
|
GetArrayItems()
|
||||||
|
local row = item[clue[thisItem]] and item[clue[thisItem]][1]
|
||||||
|
|
||||||
|
if row then
|
||||||
|
targets = { { ResHMx2((row[lineStartX] + row[lineEndX]) / 2), ResHMy2((row[lineStartY] + row[lineEndY]) / 2) } }
|
||||||
|
end
|
||||||
|
elseif (GetMapArray01 ~= nil) and (currentLevel == levelMansion4) and (lvlState == lvlRunning) and combination and iSafeNumber then
|
||||||
|
-- Who Shot Johnny Rock's safe: the dial shot while it shows the combination's next digit
|
||||||
|
-- (on this frame and the next, since the press is judged on the next).
|
||||||
|
local digit = tonumber(string.sub(combination, iSafeNumber, iSafeNumber))
|
||||||
|
local rel = discGetFrame() - thisOffset
|
||||||
|
|
||||||
|
if (getSafeNumber(rel) == digit) and (getSafeNumber(rel + 1) == digit) then
|
||||||
|
targets = { { hmx((151 + 207) / 2), hmy((64 + 88) / 2) } }
|
||||||
|
end
|
||||||
|
elseif (GUIDE_BEAVER ~= nil) and (ResHMx ~= nil) and (currentLevel == levelMenu) and panel then
|
||||||
|
-- Mad Dog II's guide menu: the first guide whose trail is not done, by its panel.
|
||||||
|
targets = {}
|
||||||
|
local x = (panel[GUIDE_BEAVER] == false) and 90 or ((panel[GUIDE_BONNIE] == false) and 186 or ((panel[GUIDE_PROF] == false) and 282 or nil))
|
||||||
|
|
||||||
|
if x then
|
||||||
|
targets[1] = { hmx(x), hmy(121) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- At the attract loop: Mad Dog's state machine names it currentState, Mad Dog II's gameflow.
|
||||||
|
local playing = ((currentState or gameflow) == statePlaying)
|
||||||
|
local intro = playing and (currentLevel == (lvlIntro or levelIntro))
|
||||||
|
|
||||||
|
-- The service screen: the option under the cursor and every dip, for the trace; a screen
|
||||||
|
-- the stick drives rather than the pointer (Hologram Time Traveler).
|
||||||
|
local dips = {}
|
||||||
|
|
||||||
|
for key, value in pairs(_G) do
|
||||||
|
if (type(key) == "string") and (key:sub(1, 4) == "dip_") and (type(value) ~= "function") then
|
||||||
|
dips[#dips + 1] = key:sub(5) .. "=" .. tostring(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(dips)
|
||||||
|
|
||||||
|
return { screen = currentLevel, step = lvlState, move = currentMove, frame = discGetFrame(), frames = frames, score = iScore, lives = iLives or iMoney, ammo = iBullets, active = p1Active, intro = intro,
|
||||||
|
service = (levelService ~= nil) and (currentLevel == levelService), serviceKeys = (TURNRIGHTSHOOT ~= nil), opt = optSel, dips = table.concat(dips, ","), width = overlayGetWidth(), height = overlayGetHeight(),
|
||||||
|
board = (levelHighScore ~= nil) and (currentLevel == levelHighScore),
|
||||||
|
window = window, box = window and firstBox() or nil, quadrants = quadrants, targets = targets, targetStep = targetStep, continuing = (currentLevel == levelContinue), noSelect = (mouseIndexSelect == nil), typing = typing, typed = (validKey ~= nil), offscreen = { -5, -5 }, free = bAmmo, paused = bPause,
|
||||||
|
-- The one-player editions (no player select, lives rather than money) spend a shot
|
||||||
|
-- before the level tests for ammo, so their last shot is never judged: a spare to keep.
|
||||||
|
spare = ((mouseIndexSelect == nil) and (iMoney == nil) and (iBullets ~= nil) and ((panelHit == nil) or (resX ~= nil)) and (levelCharlie == nil)) and 1 or 0,
|
||||||
|
-- The library's one-player Crime Patrol family judges a shot before the release takes its
|
||||||
|
-- bullet (its HD twins take it on the press, as the others do), and reloads by its second
|
||||||
|
-- button (immediately, as its config ships); so does Platoon.
|
||||||
|
reloadSwitch = (((panelHit ~= nil) and (mouseIndexSelect == nil)) or (levelCharlie ~= nil)) and SWITCH_BUTTON1 or SWITCH_BUTTON3 }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
onOverlayUpdate = function(...)
|
||||||
|
local result = realUpdate(...)
|
||||||
|
|
||||||
|
algDrive(view())
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
503
testScripts/ports/algPort.singe
Normal file
503
testScripts/ports/algPort.singe
Normal file
|
|
@ -0,0 +1,503 @@
|
||||||
|
-- The Forge port of an American Laser Games title, driven and traced as the original is.
|
||||||
|
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
|
||||||
|
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest/hypseus")
|
||||||
|
local game = os.getenv("PORT_GAME") or "maddog-hd"
|
||||||
|
local name = (os.getenv("PORT_SCRIPT") or ("singe/" .. game .. "/" .. game .. ".singe")):gsub("%.singe$", "")
|
||||||
|
local stem = name:match("([^/\\]+)$")
|
||||||
|
|
||||||
|
dofile("Forge/AuthorCompile.singe")
|
||||||
|
dofile(here .. "algDrive.singe")
|
||||||
|
|
||||||
|
-- The game seeds its random draws from os.time at load; the original's driver pins it, and so
|
||||||
|
-- does this, to the same number.
|
||||||
|
os.time = function(...) return 12345 end
|
||||||
|
|
||||||
|
local realSkip = discSkipToFrame
|
||||||
|
discSkipToFrame = function(frame)
|
||||||
|
algTraceSeek(frame)
|
||||||
|
return realSkip(frame)
|
||||||
|
end
|
||||||
|
local realSearch = discSearch
|
||||||
|
discSearch = function(frame)
|
||||||
|
algTraceSeek(frame)
|
||||||
|
return realSearch(frame)
|
||||||
|
end
|
||||||
|
dofile(authorBuild(library .. "/" .. game .. "/" .. name .. ".forge", singeGetDataPath() .. stem .. ".port.singe"))
|
||||||
|
|
||||||
|
local realUpdate = onOverlayUpdate
|
||||||
|
|
||||||
|
|
||||||
|
function algPress(switch, device, down)
|
||||||
|
if down then
|
||||||
|
authorSwitchDown(switch, device)
|
||||||
|
else
|
||||||
|
authorSwitchUp(switch, device)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A key released, as the typing edition reads it: the runtime hands a keysym over as a switch.
|
||||||
|
function algKey(keysym)
|
||||||
|
authorSwitchUp(keysym)
|
||||||
|
end
|
||||||
|
|
||||||
|
function algPointer(device, x, y)
|
||||||
|
local q = authorEntity("game").q
|
||||||
|
local rx = (x + (q.ratioxOffset or 0)) / ((q.ratiox or 1) * (q.gunscale or 1))
|
||||||
|
local ry = (y + (q.ratioyOffset or 0)) / ((q.ratioy or 1) * (q.gunscale or 1))
|
||||||
|
|
||||||
|
onMouseMoved(math.floor(rx), math.floor(ry), 0, 0, device)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The hitmap's coordinates scaled as the game scales them, where it does (the multiplayer
|
||||||
|
-- editions); the one-player editions aim in the hitmap's own.
|
||||||
|
local function hmx(v)
|
||||||
|
local q = authorEntity("game").q
|
||||||
|
|
||||||
|
if q.ResHMx then
|
||||||
|
return q.ResHMx(v)
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function hmy(v)
|
||||||
|
local q = authorEntity("game").q
|
||||||
|
|
||||||
|
if q.ResHMy then
|
||||||
|
return q.ResHMy(v)
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The hitboxes of a frame in the current move's hitmap range, in pointer coordinates.
|
||||||
|
local function boxesAt(q, rel)
|
||||||
|
local boxes = {}
|
||||||
|
|
||||||
|
for k = (q.move[q.currentMove][q.hitmapStart or q.hitboxStart] or 1), (q.hitmapTotal or 0) do
|
||||||
|
local row = q.hitmap[k]
|
||||||
|
|
||||||
|
if row and (row[q.hitmapFrame] == rel) and (row[q.hitmapCount] > 0) then
|
||||||
|
for j = row[q.hitmapIndex], row[q.hitmapIndex] + row[q.hitmapCount] - 1 do
|
||||||
|
local b = q.hitbox[j]
|
||||||
|
|
||||||
|
-- (A box given back to front -- Platoon's hitbox lines are, often -- can never be
|
||||||
|
-- hit by the game's own test, so it is no aim.)
|
||||||
|
if b and (b[q.bbx1] <= b[q.bbx2]) and (b[q.bby1] <= b[q.bby2]) then
|
||||||
|
boxes[#boxes + 1] = { hmx(b[q.bbx1]), hmy(b[q.bby1]), hmx(b[q.bbx2]), hmy(b[q.bby2]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return boxes
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function firstBox(q)
|
||||||
|
-- The first hitbox of this frame, aimed at a point that also lies inside a box of the next
|
||||||
|
-- frame when the two overlap: the press the drive makes now is judged on this frame or the
|
||||||
|
-- next, as the clock falls. Without an overlap (a fast target) the box of this frame stands.
|
||||||
|
if (q.move == nil) or (q.move[q.currentMove] == nil) or (q.hitmap == nil) then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local rel = q.currentFrame - (q.thisOffset or 0)
|
||||||
|
local now = boxesAt(q, rel)
|
||||||
|
local b = now[1]
|
||||||
|
|
||||||
|
if b == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
for _, n in ipairs(boxesAt(q, rel + 1)) do
|
||||||
|
local x1, y1, x2, y2 = math.max(b[1], n[1]), math.max(b[2], n[2]), math.min(b[3], n[3]), math.min(b[4], n[4])
|
||||||
|
|
||||||
|
if (x1 <= x2) and (y1 <= y2) then
|
||||||
|
return { x1, y1, x2, y2 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return b
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function view()
|
||||||
|
local q = authorEntity("game").q
|
||||||
|
local window = nil
|
||||||
|
|
||||||
|
-- A level of Mad Dog's (101 to 121) or of Mad Dog II's (its trails, and levelStart); Mad Dog II's
|
||||||
|
-- move rows have no shoot window of their own, so the move's frames stand in.
|
||||||
|
local inLevel = (q.currentLevel ~= nil) and (((q.frameShootStart ~= nil) and (q.currentLevel >= 101) and (q.currentLevel <= 121)) or ((q.frameShootStart == nil) and (((q.currentLevel >= 1) and (q.currentLevel <= 30)) or (q.currentLevel == q.levelStart))))
|
||||||
|
|
||||||
|
if ((q.practiceHit ~= nil) or (q.fRndPopOrder ~= nil)) and (q.currentLevel == q.levelPractice) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
if (q.levelSigns ~= nil) and (q.currentLevel == q.levelSigns) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
if (q.levelTieIntro ~= nil) and ((q.currentLevel == q.levelTie) or (q.currentLevel == q.levelTieIntro)) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Space Pirates's puzzles and crystals are targets, not windows: the band and Ursula's colour
|
||||||
|
-- at the doors, Fellina's skull and arrow, the sockets of the crystal build.
|
||||||
|
if (q.levelCrystalBuild ~= nil) and ((q.currentLevel == q.levelCrystalBuild) or ((q.currentLevel == q.levelDoor2) and (q.lvlState == q.lvlPlayClip4)) or ((q.currentLevel == q.levelDoor3) and (q.lvlState == q.lvlPlayClip3)) or ((q.currentLevel == q.levelFellina1) and ((q.lvlState == q.lvlPlayPuzzle1) or (q.lvlState == q.lvlPlayPuzzle2)))) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Who Shot Johnny Rock's store, clue items, and safe are targets, not windows.
|
||||||
|
if (q.GetMapArray01 ~= nil) and ((q.currentLevel == q.levelStore) or (q.currentLevel == q.levelMansion2) or (q.currentLevel == q.levelMansion4)) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Space Pirates's Reaper wants exactly as many hits as he laughed: once the drive has them,
|
||||||
|
-- the moves left are let pass.
|
||||||
|
if (q.levelReaper2 ~= nil) and (q.currentLevel == q.levelReaper2) and (q.currentMove > 1) and (q.iReaperHits >= q.iReaperLaughs) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The typing edition of Mad Dog II plays on the keyboard: the key the game waits for now (the
|
||||||
|
-- next letter of the word, a sprinkled letter, the return key at a menu), and no gun at all.
|
||||||
|
local typing = nil
|
||||||
|
|
||||||
|
if q.validKey ~= nil then
|
||||||
|
inLevel = false
|
||||||
|
local function nextOf(word)
|
||||||
|
if word and (q.iKeyIndex or 1) <= string.len(word) then
|
||||||
|
return string.byte(word, q.iKeyIndex or 1)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
-- (The game starts with the key slot at zero, not at none, and never clears it.)
|
||||||
|
local waiting = (q.iKey == q.SWITCH_NONE) or (q.iKey == 0)
|
||||||
|
|
||||||
|
if not waiting then
|
||||||
|
typing = nil
|
||||||
|
elseif (q.currentLevel == q.levelTitle) and (q.lvlState == q.lvlRunning) then
|
||||||
|
typing = q.SWITCH_RETURN
|
||||||
|
elseif (q.currentLevel == q.levelMenu) and (q.lvlState == q.lvlRunning) then
|
||||||
|
typing = q.SWITCH_RETURN
|
||||||
|
elseif (q.currentLevel == q.levelHighScore) and (q.lvlState == q.lvlRunning) then
|
||||||
|
-- (Once, before the name's frames run out; the return key seeks to the entry's end.)
|
||||||
|
if discGetFrame() < q.offsetMenus + 3275 then
|
||||||
|
typing = q.SWITCH_RETURN
|
||||||
|
end
|
||||||
|
elseif q.bShowSprinkles and q.sprinkles then
|
||||||
|
for k = 1, (q.iSprinkleMax or 0) do
|
||||||
|
if q.sprinkles[k] and (q.sprinkles[k][4] == true) then
|
||||||
|
typing = string.byte(q.sprinkles[k][3])
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (q.currentLevel == q.levelItem1) and (q.lvlState == q.lvlPlayClip3) then
|
||||||
|
typing = nextOf(q.sMagicWord)
|
||||||
|
elseif q.move and q.move[q.currentMove] and ((q.lvlState == q.lvlPauseAction) or ((q.lvlState == q.lvlRunning) and (discGetFrame() >= q.move[q.currentMove][q.moveFrameStart]) and (discGetFrame() < q.move[q.currentMove][q.moveFrameEnd]))) then
|
||||||
|
typing = nextOf(q.move[q.currentMove][(q.currentLevel == q.levelItem2) and q.moveType or q.wordval])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Platoon's patrols open with a clip the first press of the trigger jumps past, whatever the
|
||||||
|
-- state: no window until that press is in, and a target anywhere to make it.
|
||||||
|
local skipping = (q.levelCharlie ~= nil) and q.bOnce and (q.currentLevel ~= nil) and (q.currentLevel >= 1) and (q.currentLevel <= 6) and (q.lvlState == q.lvlRunning)
|
||||||
|
|
||||||
|
if skipping then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A good guy's move (the Crime Patrol family marks the row) is no window: shooting him is a
|
||||||
|
-- death, and the drive would take it at every level that has one.
|
||||||
|
if inLevel and q.move and q.move[q.currentMove] and (q.rndMoveType ~= nil) and (q.GOODGUY ~= nil) and (q.move[q.currentMove][q.rndMoveType] == q.GOODGUY) then
|
||||||
|
inLevel = false
|
||||||
|
end
|
||||||
|
if inLevel and q.move and q.move[q.currentMove] then
|
||||||
|
window = { q.move[q.currentMove][q.frameShootStart or q.moveFrameStart], q.move[q.currentMove][q.frameShootEnd or q.moveFrameEnd] }
|
||||||
|
end
|
||||||
|
local quadrants = nil
|
||||||
|
|
||||||
|
if (q.currentLevel == q.levelTownSelect) and q.stage then
|
||||||
|
quadrants = {}
|
||||||
|
local w, h = q.resX or 360, q.resY or 240
|
||||||
|
|
||||||
|
-- In the order the game itself takes when the select times out: the saloon before the
|
||||||
|
-- sheriff, whose level needs the key from it.
|
||||||
|
if not q.stage[q.STAGE_CORRAL] then quadrants[#quadrants + 1] = { w * 0.25, h * 0.25 } end
|
||||||
|
if not q.stage[q.STAGE_SALOON] then quadrants[#quadrants + 1] = { w * 0.75, h * 0.25 } end
|
||||||
|
if not q.stage[q.STAGE_SHERIFF] then quadrants[#quadrants + 1] = { w * 0.25, h * 0.75 } end
|
||||||
|
if not q.stage[q.STAGE_BANK] then quadrants[#quadrants + 1] = { w * 0.75, h * 0.75 } end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The signpost's sign and the mine's lantern and item, as a list of points to shoot in
|
||||||
|
-- order (the middle line of each, scaled as the game scales them).
|
||||||
|
local targets = nil
|
||||||
|
local targetStep = nil
|
||||||
|
local function lineMiddle(rows, count)
|
||||||
|
local row = rows and rows[math.floor((count + 1) / 2)]
|
||||||
|
|
||||||
|
if row then
|
||||||
|
return { hmx((row[q.lineStartX] + row[q.lineEndX]) / 2), hmy(row[q.lineY]) }
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if skipping then
|
||||||
|
targets = { { 180, 120 } }
|
||||||
|
targetStep = q.lvlRunning
|
||||||
|
elseif (q.currentLevel == q.levelSignSelect) and q.sign then
|
||||||
|
targets = {}
|
||||||
|
if q.stage[q.STAGE_FUSE] == false then
|
||||||
|
targets[1] = lineMiddle(q.sign[q.SIGN_PROSPECTOR], 48)
|
||||||
|
elseif q.stage[q.STAGE_MINE] == false then
|
||||||
|
targets[1] = lineMiddle(q.sign[q.SIGN_MINE], 28)
|
||||||
|
else
|
||||||
|
targets[1] = lineMiddle(q.sign[q.SIGN_HIDEOUT], 58)
|
||||||
|
end
|
||||||
|
elseif (q.currentLevel == q.levelMine) and q.mine then
|
||||||
|
targets = {}
|
||||||
|
local counts = { [q.MINE_SIGN] = 21, [q.MINE_LANTERN] = 20, [q.MINE_PAN] = 12, [q.MINE_PITCHFORK] = 23, [q.MINE_SKULL] = 25 }
|
||||||
|
|
||||||
|
if not q.bLanternHit then targets[#targets + 1] = lineMiddle(q.mine[q.MINE_LANTERN], counts[q.MINE_LANTERN]) end
|
||||||
|
if not q.bItemHit then targets[#targets + 1] = lineMiddle(q.mine[q.iWhatToHit], counts[q.iWhatToHit]) end
|
||||||
|
elseif (q.currentLevel == q.levelMaze) and q.maze and q.maze[q.curMazePos] and (q.maze[q.curMazePos][q.mazeType] > 0) and q.sign then
|
||||||
|
-- A maze sign: the sign for the way to go.
|
||||||
|
local frame, dir = q.maze[q.curMazePos][q.signType], q.maze[q.curMazePos][q.signDirection]
|
||||||
|
local counts = { [32] = { 51, 49 }, [175] = { 59, 17 }, [340] = { 67, 90 }, [500] = { 24, 42 }, [630] = { 11, 30 }, [446] = { 59, 64 } }
|
||||||
|
local pair = counts[frame]
|
||||||
|
|
||||||
|
if pair and q.sign[dir] then
|
||||||
|
targets = { lineMiddle(q.sign[dir], pair[dir]) }
|
||||||
|
end
|
||||||
|
elseif (q.levelItem1 ~= nil) and (q.ResHMx ~= nil) and (q.currentLevel == q.levelItem1) and (q.lvlState == q.lvlPlayClip3) then
|
||||||
|
-- Mad Dog II's first item level: the item the guide asked for, from the game's own
|
||||||
|
-- rectangles.
|
||||||
|
local boxes = { [q.ITEM_LANTERN] = { 57, 66, 117, 129 }, [q.ITEM_AMMOBOX] = { 120, 148, 111, 132 }, [q.ITEM_MOONSHINE] = { 260, 272, 60, 72 } }
|
||||||
|
local b = boxes[q.iThisItem]
|
||||||
|
|
||||||
|
if b then
|
||||||
|
targets = { { hmx((b[1] + b[2]) / 2), hmy((b[3] + b[4]) / 2) } }
|
||||||
|
targetStep = q.lvlPlayClip3
|
||||||
|
end
|
||||||
|
elseif (q.levelProspector1 ~= nil) and (q.ResHMx ~= nil) and (q.currentLevel == q.levelProspector1) and (q.lvlState == q.lvlPlayClip4) then
|
||||||
|
-- Mad Dog II's prospector: the skull he chose, from the game's own rectangles.
|
||||||
|
local boxes = { { 305, 315, 59, 76 }, { 68, 75, 73, 82 }, { 147, 154, 58, 67 }, { 119, 127, 101, 115 } }
|
||||||
|
local b = boxes[q.iSkull] or boxes[4]
|
||||||
|
|
||||||
|
targets = { { hmx((b[1] + b[2]) / 2), hmy((b[3] + b[4]) / 2) } }
|
||||||
|
targetStep = q.lvlPlayClip4
|
||||||
|
elseif (q.panelHit ~= nil) and (q.currentLevel == q.levelMenu) and (q.lvlState == q.lvlRunning) and q.area then
|
||||||
|
-- Crime Patrol's mission menus: the first panel whose stage is open, by the middle line
|
||||||
|
-- of its area (the count of lines is the game's own).
|
||||||
|
local MENUS = {}
|
||||||
|
|
||||||
|
if q.MENU_SIERRA ~= nil then
|
||||||
|
MENUS[q.MENU_SIERRA] = { { q.PANEL_PRACTICE, q.STAGE_PRACTICE, 99 }, { q.PANEL_BAR, q.STAGE_BAR, 99 }, { q.PANEL_CARCHASE, q.STAGE_CARCHASE, 99 }, { q.PANEL_HIDEOUT, q.STAGE_HIDEOUT, 99 } }
|
||||||
|
MENUS[q.MENU_CHICAGO] = { { q.PANEL_HELI, q.STAGE_HELI, 111 }, { q.PANEL_COURTROOM, q.STAGE_COURTROOM, 111 }, { q.PANEL_BUSCHASE, q.STAGE_BUSCHASE, 111 } }
|
||||||
|
MENUS[q.MENU_BORDER] = { { q.PANEL_SHIPYARD, q.STAGE_SHIPYARD, 111 }, { q.PANEL_BOATHOUSE, q.STAGE_BOATHOUSE, 111 }, { q.PANEL_BEACH, q.STAGE_BEACH, 111 } }
|
||||||
|
MENUS[q.MENU_SA] = { { q.PANEL_BASE, q.STAGE_BASE, 111 }, { q.PANEL_NIGHTVISION, q.STAGE_NIGHTVISION, 111 }, { q.PANEL_TOWN, q.STAGE_TOWN, 111 } }
|
||||||
|
end
|
||||||
|
if q.MENU_ROOKIE ~= nil then
|
||||||
|
MENUS = { [q.MENU_ROOKIE] = { { q.PANEL_PRACTICE, q.STAGE_PRACTICE, 98 }, { q.PANEL_STORE, q.STAGE_STORE, 99 }, { q.PANEL_GANG, q.STAGE_GANG, 99 }, { q.PANEL_WAREHOUSE, q.STAGE_WAREHOUSE, 97 } },
|
||||||
|
[q.MENU_UNDERCOVER] = { { q.PANEL_AIRPORT, q.STAGE_AIRPORT, 104 }, { q.PANEL_GARAGE, q.STAGE_GARAGE, 104 }, { q.PANEL_STRIPCLUB, q.STAGE_STRIPCLUB, 104 } },
|
||||||
|
[q.MENU_SWAT] = { { q.PANEL_DRUGS1, q.STAGE_DRUGS1, 111 }, { q.PANEL_DRUGS2, q.STAGE_DRUGS2, 111 }, { q.PANEL_BANK, q.STAGE_ROBBERY, 103 } },
|
||||||
|
[q.MENU_DELTA] = { { q.PANEL_HANGAR, q.STAGE_HANGAR, 108 }, { q.PANEL_AMBUSH, q.STAGE_AMBUSH, 103 }, { q.PANEL_AIRTERRORISTS, q.STAGE_AIRTERRORISTS, 109 } } }
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, entry in ipairs(MENUS[q.iCurrentMenu] or {}) do
|
||||||
|
if not q.panel[entry[2]] then
|
||||||
|
targets = { lineMiddle(q.area[entry[1]], entry[3]) }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (q.GetArrayPanels ~= nil) and (q.currentLevel == q.levelMenu) and (q.lvlState == q.lvlRunning) and q.area then
|
||||||
|
-- The Last Bounty Hunter's wanted posters: the first outlaw still at large, by the middle of
|
||||||
|
-- his panel's rectangle.
|
||||||
|
for _, stage in ipairs({ q.STAGE_HARRY, q.STAGE_DAN, q.STAGE_LOCO, q.STAGE_CACTUS }) do
|
||||||
|
local r = (q.panel[stage] == q.ATLARGE) and q.area[stage] and q.area[stage][1]
|
||||||
|
|
||||||
|
if r then
|
||||||
|
targets = { { hmx((r[q.lineStartX] + r[q.lineEndX]) / 2), hmy((r[q.lineStartY] + r[q.lineEndY]) / 2) } }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (q.levelSigns ~= nil) and (q.currentLevel == q.levelSigns) and (q.lvlState == q.lvlEnd) then
|
||||||
|
-- The Last Bounty Hunter's signs: the practice range, in the game's own pointer coordinates.
|
||||||
|
targets = { { 175, 101 } }
|
||||||
|
targetStep = q.lvlEnd
|
||||||
|
elseif (q.iLastMove ~= nil) and (q.levelFinal ~= nil) and (q.currentLevel == q.levelFinal) and (q.lvlState == q.lvlPlayClip2) then
|
||||||
|
-- Drug Wars's last stand: the tank, the blanket, or the gas cans, in the game's own
|
||||||
|
-- pointer coordinates.
|
||||||
|
local STAND = { { 262, 329, 133, 172 }, { 101, 111, 160, 188 }, { 209, 243, 201, 215 } }
|
||||||
|
local b = STAND[q.iLastMove] or STAND[3]
|
||||||
|
|
||||||
|
targets = { { (b[1] + b[2]) / 2, (b[3] + b[4]) / 2 } }
|
||||||
|
targetStep = q.lvlPlayClip2
|
||||||
|
elseif (q.practiceHit ~= nil) and (q.currentLevel == q.levelPractice) and (q.lvlState == q.lvlRunning) then
|
||||||
|
-- Crime Patrol's practice range: the first standing target of the still, in the game's
|
||||||
|
-- own pointer coordinates, or the cutout of the move (the middle line of its outline).
|
||||||
|
if q.currentMove == 0 then
|
||||||
|
local STILL = { { "bTopRight", 261, 300, 9, 60 }, { "bTopLeft", 78, 119, 70, 120 }, { "bMiddle", 205, 243, 70, 123 }, { "bBottomLeft", 120, 158, 124, 181 }, { "bBottomRight", 268, 297, 132, 183 } }
|
||||||
|
|
||||||
|
for _, t in ipairs(STILL) do
|
||||||
|
if q[t[1]] then
|
||||||
|
targets = { { (t[2] + t[3]) / 2, (t[4] + t[5]) / 2 } }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (q.currentMove >= 1) and q.move and q.move[q.currentMove] and q.target then
|
||||||
|
local kind = q.move[q.currentMove][q.rndMoveType]
|
||||||
|
local counts = { [q.BADGUY01] = 98, [q.CIV01] = 82, [q.BADGUY02] = 97, [q.CIV02] = 107, [q.BADGUY03] = 79, [q.CIV03] = 148 }
|
||||||
|
|
||||||
|
if kind and counts[kind] and q.target[kind] then
|
||||||
|
targets = { lineMiddle(q.target[kind], counts[kind]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (q.levelFirePractice ~= nil) and (q.currentLevel == q.levelMenu) and (q.lvlState == q.lvlRunning) and q.panel then
|
||||||
|
-- Space Pirates's planet menu: the first panel not yet done, in the order the game takes
|
||||||
|
-- when the clip runs out, by the middle of its quadrant (pointer coordinates scaled as the
|
||||||
|
-- game scales them).
|
||||||
|
local PANELS = { { q.STAGE_FELLINA, 807, 1280, 565, 916 }, { q.STAGE_REAPER, 245, 720, 565, 916 }, { q.STAGE_SCRAPYARD, 807, 1280, 100, 450 }, { q.STAGE_MOUNTAIN, 245, 720, 100, 450 } }
|
||||||
|
|
||||||
|
for _, p in ipairs(PANELS) do
|
||||||
|
if q.panel[p[1]] == false then
|
||||||
|
targets = { { q.UnX * (p[2] + p[3]) / 2, q.UnY * (p[4] + p[5]) / 2 } }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (q.levelDoor2 ~= nil) and (q.currentLevel == q.levelDoor2) and (q.lvlState == q.lvlPlayClip4) and q.band then
|
||||||
|
-- Space Pirates's band of colour: the right one, within its frames, by the middle of its
|
||||||
|
-- line.
|
||||||
|
local row = q.band[q.iBand] and q.band[q.iBand][1]
|
||||||
|
|
||||||
|
if row and (discGetFrame() >= q.bandFrameStart + 3) and (discGetFrame() < q.bandFrameEnd) then
|
||||||
|
targets = { { hmx((row[q.lineStartX] + row[q.lineEndX]) / 2), hmy((row[q.lineStartY] + row[q.lineEndY]) / 2) } }
|
||||||
|
targetStep = q.lvlPlayClip4
|
||||||
|
end
|
||||||
|
elseif (q.levelDoor3 ~= nil) and (q.currentLevel == q.levelDoor3) and (q.lvlState == q.lvlPlayClip3) and (q.ursie or (q.ursulaHit ~= nil)) then
|
||||||
|
-- Space Pirates's commander: Ursula shot within her colour's frames.
|
||||||
|
local WINDOWS = { [q.COLOR_RED] = { 2243, 2306 }, [q.COLOR_GREEN] = { 2312, 2371 }, [q.COLOR_BLUE] = { 2377, 2439 } }
|
||||||
|
local w = WINDOWS[q.iUrsulaColor]
|
||||||
|
local rel = discGetFrame() - q.thisOffset
|
||||||
|
|
||||||
|
if w and (rel >= w[1] + 3) and (rel <= w[2] - 3) then
|
||||||
|
-- (The one-player edition builds her rectangles inside its hit test, so before the first
|
||||||
|
-- shot the game's own numbers stand in.)
|
||||||
|
local box = (q.ursie and q.ursie[1]) or { 152, 137, 210, 238 }
|
||||||
|
|
||||||
|
targets = { { hmx((box[1] + box[3]) / 2), hmy((box[2] + box[4]) / 2) } }
|
||||||
|
targetStep = q.lvlPlayClip3
|
||||||
|
end
|
||||||
|
elseif (q.levelFellina1 ~= nil) and (q.currentLevel == q.levelFellina1) and ((q.lvlState == q.lvlPlayPuzzle1) or (q.lvlState == q.lvlPlayPuzzle2)) and q.thingie then
|
||||||
|
-- Space Pirates's Fellina puzzles: the skull she chose, then the arrow, by the middle of
|
||||||
|
-- the game's own line.
|
||||||
|
local which = (q.lvlState == q.lvlPlayPuzzle1) and q.iFellinaSkull or q.iFellinaArrow
|
||||||
|
local row = q.thingie[which] and q.thingie[which][1]
|
||||||
|
|
||||||
|
if row and ((q.lvlState == q.lvlPlayPuzzle2) or (discGetFrame() >= q.iSkullStart + 3)) then
|
||||||
|
targets = { { hmx((row[q.lineStartX] + row[q.lineEndX]) / 2), hmy((row[q.lineStartY] + row[q.lineEndY]) / 2) } }
|
||||||
|
targetStep = q.lvlState
|
||||||
|
end
|
||||||
|
elseif (q.levelCrystalBuild ~= nil) and (q.currentLevel == q.levelCrystalBuild) and (q.lvlState == q.lvlRunning) and q.hole and q.socket then
|
||||||
|
-- Space Pirates's crystal build: the next socket in the order shown.
|
||||||
|
local ORDERS = { [q.ORDER_RGB] = { q.SOCKET_RED, q.SOCKET_GREEN, q.SOCKET_BLUE }, [q.ORDER_GBR] = { q.SOCKET_GREEN, q.SOCKET_BLUE, q.SOCKET_RED }, [q.ORDER_BRG] = { q.SOCKET_BLUE, q.SOCKET_RED, q.SOCKET_GREEN } }
|
||||||
|
|
||||||
|
for _, which in ipairs(ORDERS[q.iCrystalOrder] or {}) do
|
||||||
|
if not q.socket[which] then
|
||||||
|
local row = q.hole[which] and q.hole[which][1]
|
||||||
|
|
||||||
|
if row then
|
||||||
|
targets = { { hmx((row[q.lineStartX] + row[q.lineEndX]) / 2), hmy((row[q.lineStartY] + row[q.lineEndY]) / 2) } }
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (q.GetMapArray01 ~= nil) and (q.currentLevel == q.levelMenu) and (q.lvlState == q.lvlRunning) and q.stage then
|
||||||
|
-- Who Shot Johnny Rock's city map: the killer's place once revealed, the mansion once the
|
||||||
|
-- clues are all had, else the first place still open (the casino, the pool hall, the
|
||||||
|
-- warehouse, the garage), by the middle of its panel.
|
||||||
|
local which = nil
|
||||||
|
|
||||||
|
if q.bKillerRevealed then
|
||||||
|
which = ({ [q.JILL] = q.PLACE_CASINO, [q.MUMPS] = q.PLACE_POOL, [q.MEASLES] = q.PLACE_WAREHOUSE, [q.POX] = q.PLACE_GARAGE })[q.iKiller]
|
||||||
|
elseif q.bGotAllClues then
|
||||||
|
which = q.PLACE_MANSION
|
||||||
|
elseif not q.stage[q.levelCasino] then
|
||||||
|
which = q.PLACE_CASINO
|
||||||
|
elseif not q.stage[q.levelPool] then
|
||||||
|
which = q.PLACE_POOL
|
||||||
|
elseif not (q.stage[q.levelWarehouse1] and q.stage[q.levelWarehouse2] and q.stage[q.levelWarehouse3]) then
|
||||||
|
which = q.PLACE_WAREHOUSE
|
||||||
|
elseif not q.stage[q.levelGarage] then
|
||||||
|
which = q.PLACE_GARAGE
|
||||||
|
end
|
||||||
|
if which then
|
||||||
|
q.GetMapArray01()
|
||||||
|
local row = q.place[which] and q.place[which][1]
|
||||||
|
|
||||||
|
if row then
|
||||||
|
targets = { { hmx((row[q.lineStartX] + row[q.lineEndX]) / 2), hmy((row[q.lineStartY] + row[q.lineEndY]) / 2) } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (q.GetMapArray01 ~= nil) and (q.currentLevel == q.levelMansion2) and (q.lvlState == q.lvlRunning) and q.clue and q.thisItem then
|
||||||
|
-- Who Shot Johnny Rock's clue items: the item of the clue asked for, in the game's second
|
||||||
|
-- scale.
|
||||||
|
q.GetArrayItems()
|
||||||
|
local row = q.item[q.clue[q.thisItem]] and q.item[q.clue[q.thisItem]][1]
|
||||||
|
|
||||||
|
if row then
|
||||||
|
targets = { { q.ResHMx2((row[q.lineStartX] + row[q.lineEndX]) / 2), q.ResHMy2((row[q.lineStartY] + row[q.lineEndY]) / 2) } }
|
||||||
|
end
|
||||||
|
elseif (q.GetMapArray01 ~= nil) and (q.currentLevel == q.levelMansion4) and (q.lvlState == q.lvlRunning) and q.combination and q.iSafeNumber then
|
||||||
|
-- Who Shot Johnny Rock's safe: the dial shot while it shows the combination's next digit
|
||||||
|
-- (on this frame and the next, since the press is judged on the next).
|
||||||
|
local digit = tonumber(string.sub(q.combination, q.iSafeNumber, q.iSafeNumber))
|
||||||
|
local rel = discGetFrame() - q.thisOffset
|
||||||
|
|
||||||
|
if (q.getSafeNumber(rel) == digit) and (q.getSafeNumber(rel + 1) == digit) then
|
||||||
|
targets = { { hmx((151 + 207) / 2), hmy((64 + 88) / 2) } }
|
||||||
|
end
|
||||||
|
elseif (q.GUIDE_BEAVER ~= nil) and (q.ResHMx ~= nil) and (q.currentLevel == q.levelMenu) and q.panel then
|
||||||
|
-- Mad Dog II's guide menu: the first guide whose trail is not done, by its panel.
|
||||||
|
targets = {}
|
||||||
|
local x = (q.panel[q.GUIDE_BEAVER] == false) and 90 or ((q.panel[q.GUIDE_BONNIE] == false) and 186 or ((q.panel[q.GUIDE_PROF] == false) and 282 or nil))
|
||||||
|
|
||||||
|
if x then
|
||||||
|
targets[1] = { hmx(x), hmy(121) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local playing = ((q.currentState or q.gameflow) == q.statePlaying)
|
||||||
|
local intro = playing and (q.currentLevel == (q.lvlIntro or q.levelIntro))
|
||||||
|
|
||||||
|
-- The service screen: the option under the cursor and every dip, for the trace; a screen
|
||||||
|
-- the stick drives rather than the pointer (Hologram Time Traveler).
|
||||||
|
local dips = {}
|
||||||
|
|
||||||
|
for key, value in pairs(q) do
|
||||||
|
if (type(key) == "string") and (key:sub(1, 4) == "dip_") and (type(value) ~= "function") then
|
||||||
|
dips[#dips + 1] = key:sub(5) .. "=" .. tostring(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(dips)
|
||||||
|
|
||||||
|
return { screen = q.currentLevel, step = q.lvlState, move = q.currentMove, frame = discGetFrame(), frames = frames, score = q.iScore, lives = q.iLives or q.iMoney, ammo = q.iBullets, active = q.p1Active, intro = intro,
|
||||||
|
service = (q.levelService ~= nil) and (q.currentLevel == q.levelService), serviceKeys = (q.TURNRIGHTSHOOT ~= nil), opt = q.optSel, dips = table.concat(dips, ","), width = overlayGetWidth(), height = overlayGetHeight(),
|
||||||
|
board = (q.levelHighScore ~= nil) and (q.currentLevel == q.levelHighScore),
|
||||||
|
window = window, box = window and firstBox(q) or nil, quadrants = quadrants, targets = targets, targetStep = targetStep, continuing = (q.currentLevel == q.levelContinue), noSelect = (q.mouseIndexSelect == nil), typing = typing, typed = (q.validKey ~= nil), offscreen = { -5, -5 }, free = q.bAmmo, paused = q.bPause,
|
||||||
|
-- The one-player editions (no player select, lives rather than money) spend a shot
|
||||||
|
-- before the level tests for ammo, so their last shot is never judged: a spare to keep.
|
||||||
|
spare = ((q.mouseIndexSelect == nil) and (q.iMoney == nil) and (q.iBullets ~= nil) and ((q.panelHit == nil) or (q.resX ~= nil)) and (q.levelCharlie == nil)) and 1 or 0,
|
||||||
|
-- The library's one-player Crime Patrol family judges a shot before the release takes its
|
||||||
|
-- bullet (its HD twins take it on the press, as the others do), and reloads by its second
|
||||||
|
-- button (immediately, as its config ships); so does Platoon.
|
||||||
|
reloadSwitch = (((q.panelHit ~= nil) and (q.mouseIndexSelect == nil)) or (q.levelCharlie ~= nil)) and SWITCH_BUTTON1 or SWITCH_BUTTON3 }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
onOverlayUpdate = function(...)
|
||||||
|
local result = realUpdate(...)
|
||||||
|
|
||||||
|
algDrive(view())
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
208
testScripts/ports/compare.sh
Executable file
208
testScripts/ports/compare.sh
Executable file
|
|
@ -0,0 +1,208 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Runs an original game from the test library and its Forge port on the same inputs, headless
|
||||||
|
# and on the deterministic clock, and says whether their traces agree (FORGE.md section 14.1).
|
||||||
|
#
|
||||||
|
# testScripts/ports/compare.sh <family> <game>
|
||||||
|
#
|
||||||
|
# SINGE the engine binary (default: the newest build in .builddir)
|
||||||
|
# SINGE_LIBRARY the test library (default: ~/claude/singetest/ported, where the
|
||||||
|
# ported originals live; the hypseus families look under its hypseus/)
|
||||||
|
# SINGE_RUN a directory to run in, with Singe/ and Forge/ beside each other
|
||||||
|
# (default: a fresh one under /tmp, with assets/Forge copied in)
|
||||||
|
#
|
||||||
|
# Families: actionMax (games 38AmbushAlley, BlueThunder, Hydrosub2021, PopsGhostly, SonicFury);
|
||||||
|
# karis (a game as directory/script, e.g. TimeGal/Timegal.singe);
|
||||||
|
# hypseus (a Karis-framework title from the Hypseus library by its folder, e.g.
|
||||||
|
# Astroboy: the script is the description's namesake under singe/, the video its
|
||||||
|
# framefile);
|
||||||
|
# rdg (a map-mode title from the Hypseus library, laid out the same way; the engine's
|
||||||
|
# deterministic clock gives both runs the same seed for the game's random draws);
|
||||||
|
# alg (an American Laser Games title, Hypseus layout or the library's);
|
||||||
|
# videoland (Adventures in Videoland: Rollercoaster, by its folder);
|
||||||
|
# timetraveler (Hologram Time Traveler, by its folder).
|
||||||
|
set -u
|
||||||
|
family=${1:?usage: compare.sh <family> <game>}
|
||||||
|
game=${2:?usage: compare.sh <family> <game>}
|
||||||
|
repo=$(cd "$(dirname "$0")/../.." && pwd)
|
||||||
|
library=${SINGE_LIBRARY:-$HOME/claude/singetest/ported}
|
||||||
|
binary=${SINGE:-$(ls -t "$repo"/.builddir/linux-gcc/*/singe/Singe-v* "$repo"/.builddir/linux/x86_64/singe/Singe-v* 2>/dev/null | head -1)}
|
||||||
|
run=${SINGE_RUN:-}
|
||||||
|
if [[ -z "$run" ]]; then
|
||||||
|
run=$(mktemp -d /tmp/forgePort.XXXXXX)
|
||||||
|
fi
|
||||||
|
# The run directory gets Forge fresh from the repository every time, so an edit to Author.singe
|
||||||
|
# is what the port plays.
|
||||||
|
mkdir -p "$run"
|
||||||
|
rm -rf "$run/Forge"
|
||||||
|
cp -r "$repo/assets/Forge" "$run/Forge"
|
||||||
|
if [[ -z "$binary" || ! -x "$binary" ]]; then
|
||||||
|
echo "compare.sh: no engine binary; set SINGE" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
case "$family" in
|
||||||
|
actionMax)
|
||||||
|
video="$library/ActionMax/frame_$game.txt"
|
||||||
|
original="$repo/testScripts/ports/actionMaxOriginal.singe"
|
||||||
|
port="$repo/testScripts/ports/actionMaxPort.singe"
|
||||||
|
;;
|
||||||
|
karis)
|
||||||
|
# The game's directory and its main script, as "TimeGal/Timegal.singe"; the video is
|
||||||
|
# what its games.dat names.
|
||||||
|
export PORT_SCRIPT=${game#*/}
|
||||||
|
game=${game%%/*}
|
||||||
|
video=$library/$(grep -o 'VIDEO *= *"[^"]*"' "$library/$game/games.dat" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||||
|
original="$repo/testScripts/ports/karisOriginal.singe"
|
||||||
|
port="$repo/testScripts/ports/karisPort.singe"
|
||||||
|
;;
|
||||||
|
hypseus)
|
||||||
|
# A Hypseus title's script hardcodes BASEDIR = "singe", so the run directory gets the
|
||||||
|
# title's singe/ beside it, and its own copy of Forge.
|
||||||
|
library=${SINGE_LIBRARY:-$HOME/claude/singetest/ported/hypseus}
|
||||||
|
# The script is the description's namesake, wherever under singe/ the title keeps it
|
||||||
|
# (Sintel's folder is not named as its title is).
|
||||||
|
desc=$(find -L "$library/$game/singe" -mindepth 2 -maxdepth 2 -name "*.forge" 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$desc" ]]; then
|
||||||
|
export PORT_SCRIPT=${desc#"$library/$game/"}
|
||||||
|
PORT_SCRIPT=${PORT_SCRIPT%.forge}.singe
|
||||||
|
else
|
||||||
|
export PORT_SCRIPT=singe/$game/$game.singe
|
||||||
|
fi
|
||||||
|
# The framefile is at the top of the title, or beside the script.
|
||||||
|
video=$library/$game/$game.txt
|
||||||
|
[[ -f "$video" ]] || video=$library/$game/${PORT_SCRIPT%.singe}.txt
|
||||||
|
original="$repo/testScripts/ports/karisOriginal.singe"
|
||||||
|
port="$repo/testScripts/ports/karisPort.singe"
|
||||||
|
run=$run/$game
|
||||||
|
mkdir -p "$run"
|
||||||
|
rm -rf "$run/Forge"
|
||||||
|
cp -r "$repo/assets/Forge" "$run/Forge"
|
||||||
|
ln -sfn "$library/$game/singe" "$run/singe"
|
||||||
|
;;
|
||||||
|
rdg)
|
||||||
|
library=${SINGE_LIBRARY:-$HOME/claude/singetest/ported/hypseus}
|
||||||
|
desc=$(find -L "$library/$game/singe" -mindepth 2 -maxdepth 2 -name "*.forge" 2>/dev/null | head -1)
|
||||||
|
export PORT_SCRIPT=${desc#"$library/$game/"}
|
||||||
|
PORT_SCRIPT=${PORT_SCRIPT%.forge}.singe
|
||||||
|
video=$library/$game/$game.txt
|
||||||
|
[[ -f "$video" ]] || video=$library/$game/${PORT_SCRIPT%.singe}.txt
|
||||||
|
original="$repo/testScripts/ports/karisOriginal.singe"
|
||||||
|
port="$repo/testScripts/ports/karisPort.singe"
|
||||||
|
run=$run/$game
|
||||||
|
mkdir -p "$run"
|
||||||
|
rm -rf "$run/Forge"
|
||||||
|
cp -r "$repo/assets/Forge" "$run/Forge"
|
||||||
|
ln -sfn "$library/$game/singe" "$run/singe"
|
||||||
|
;;
|
||||||
|
alg)
|
||||||
|
# An American Laser Games title from the Hypseus library: the same layout as the
|
||||||
|
# map-mode ones, its own drivers.
|
||||||
|
library=${SINGE_LIBRARY:-$HOME/claude/singetest/hypseus}
|
||||||
|
if [[ -d "$library/$game/Script" ]]; then
|
||||||
|
# The library's own copy (Script/, Cfg/, Video/ with the framefile beside the video;
|
||||||
|
# Platoon keeps its Video/ under Script/): the script is the description's namesake,
|
||||||
|
# the video the framefile in Video/.
|
||||||
|
desc=$(ls "$library/$game/Script"/*.forge 2>/dev/null | head -1)
|
||||||
|
export PORT_SCRIPT=Script/$(basename "${desc%.forge}").singe
|
||||||
|
video=$(ls "$library/$game/Video"/*.txt "$library/$game/Script/Video"/*.txt 2>/dev/null | head -1)
|
||||||
|
else
|
||||||
|
desc=$(find -L "$library/$game/singe" -mindepth 2 -maxdepth 2 -name "*.forge" 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$desc" ]]; then
|
||||||
|
export PORT_SCRIPT=${desc#"$library/$game/"}
|
||||||
|
PORT_SCRIPT=${PORT_SCRIPT%.forge}.singe
|
||||||
|
else
|
||||||
|
export PORT_SCRIPT=singe/$game/$game.singe
|
||||||
|
fi
|
||||||
|
video=$library/$game/$game.txt
|
||||||
|
fi
|
||||||
|
original="$repo/testScripts/ports/algOriginal.singe"
|
||||||
|
port="$repo/testScripts/ports/algPort.singe"
|
||||||
|
run=$run/$game
|
||||||
|
mkdir -p "$run"
|
||||||
|
rm -rf "$run/Forge"
|
||||||
|
cp -r "$repo/assets/Forge" "$run/Forge"
|
||||||
|
ln -sfn "$library/$game/singe" "$run/singe"
|
||||||
|
;;
|
||||||
|
videoland|timetraveler)
|
||||||
|
# A one-off of the library (Adventures in Videoland: Rollercoaster; Hologram Time
|
||||||
|
# Traveler): its games.dat names the video and the script; the description is beside
|
||||||
|
# the script; the drivers are the family's own.
|
||||||
|
library=${SINGE_LIBRARY:-$HOME/claude/singetest}
|
||||||
|
export PORT_SCRIPT=$(grep -o 'SCRIPT *= *"[^"]*"' "$library/$game/games.dat" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||||
|
video=$library/$(grep -o 'VIDEO *= *"[^"]*"' "$library/$game/games.dat" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||||
|
original="$repo/testScripts/ports/${family}Original.singe"
|
||||||
|
port="$repo/testScripts/ports/${family}Port.singe"
|
||||||
|
run=$run/$game
|
||||||
|
mkdir -p "$run"
|
||||||
|
rm -rf "$run/Forge"
|
||||||
|
cp -r "$repo/assets/Forge" "$run/Forge"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "compare.sh: no family called $family" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
export SINGE_LIBRARY=$library PORT_GAME=$game SDL_VIDEODRIVER=${SDL_VIDEODRIVER:-offscreen} SDL_AUDIO_DRIVER=dummy
|
||||||
|
trace() {
|
||||||
|
local name=$1 script=$2 work
|
||||||
|
work=$(mktemp -d "$run/$name.XXXXXX")
|
||||||
|
( cd "$run" && timeout 3600 "$binary" -w -p -k --deterministic -d "$work" -v "$video" "$script" ) > "$work/run.log" 2>&1
|
||||||
|
grep -a '^TRACE' "$work/run.log" | grep -v 'state 2 from' > "$work/trace"
|
||||||
|
grep -a 'rror\|Author:' "$work/run.log" | head -3 >&2
|
||||||
|
# The screenshots a drive took (PORT_SHOTS=1) land in the run's data directory; with
|
||||||
|
# SINGE_SHOTS set they are gathered there, named for the run.
|
||||||
|
if [[ -n "${SINGE_SHOTS:-}" ]]; then
|
||||||
|
mkdir -p "$SINGE_SHOTS"
|
||||||
|
for png in "$work"/*.png; do
|
||||||
|
[[ -f "$png" ]] && cp "$png" "$SINGE_SHOTS/$name-$(basename "$png")"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo "$work/trace"
|
||||||
|
}
|
||||||
|
# The originals autosave into their own Cfg folder as they play, which would give the port's
|
||||||
|
# snapshot and the next run different saves to read: the folder is put back as it was.
|
||||||
|
cfgDir=""
|
||||||
|
if [[ -n "${PORT_SCRIPT:-}" ]]; then
|
||||||
|
cfgDir=$library/$game/$(dirname "$PORT_SCRIPT")/Cfg
|
||||||
|
[[ -d "$cfgDir" ]] || cfgDir=$library/$game/Cfg
|
||||||
|
[[ -d "$cfgDir" ]] || cfgDir=""
|
||||||
|
fi
|
||||||
|
if [[ -n "$cfgDir" ]]; then
|
||||||
|
cfgKeep=$(mktemp -d "$run/cfg.XXXXXX")
|
||||||
|
cp -r "$cfgDir/." "$cfgKeep/"
|
||||||
|
fi
|
||||||
|
# An American Laser Games title keeps its dips and board in a .cfg beside its script, which
|
||||||
|
# the original writes at every board: kept and put back the same way.
|
||||||
|
cfgFiles=""
|
||||||
|
if [[ "$family" == "alg" ]]; then
|
||||||
|
cfgFiles=$(ls "$library/$game/$(dirname "$PORT_SCRIPT")"/*.cfg 2>/dev/null)
|
||||||
|
if [[ -n "$cfgFiles" ]]; then
|
||||||
|
cfgKeepFiles=$(mktemp -d "$run/cfgfiles.XXXXXX")
|
||||||
|
cp $cfgFiles "$cfgKeepFiles/"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# The originals' files go back however the comparison ends, a kill included: a run cut short
|
||||||
|
# would otherwise leave the game's own writes (its board, its pool index) for the next run and
|
||||||
|
# the description to disagree on.
|
||||||
|
restoreConfig() {
|
||||||
|
if [[ -n "$cfgDir" ]]; then
|
||||||
|
rm -rf "$cfgDir"
|
||||||
|
cp -r "$cfgKeep" "$cfgDir"
|
||||||
|
fi
|
||||||
|
if [[ -n "$cfgFiles" ]]; then
|
||||||
|
cp "$cfgKeepFiles"/*.cfg "$library/$game/$(dirname "$PORT_SCRIPT")/"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap restoreConfig EXIT
|
||||||
|
a=$(trace original "$original")
|
||||||
|
b=$(trace port "$port")
|
||||||
|
if [[ ! -s "$a" || ! -s "$b" ]]; then
|
||||||
|
echo "$family $game: a run produced no trace; see $(dirname "$a")/run.log and $(dirname "$b")/run.log" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if diff -q "$a" "$b" > /dev/null; then
|
||||||
|
echo "$family $game: the port plays as the original does ($(wc -l < "$a") trace lines; $(tail -1 "$a"))"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "$family $game: the traces differ"
|
||||||
|
diff "$a" "$b" | head -20
|
||||||
|
exit 1
|
||||||
498
testScripts/ports/karisDrive.singe
Normal file
498
testScripts/ports/karisDrive.singe
Normal file
|
|
@ -0,0 +1,498 @@
|
||||||
|
-- How a KarisFramework game and its Forge port are driven for comparison (FORGE.md section
|
||||||
|
-- 14.1): the same policy, read off the same state in either. A run presses START on the
|
||||||
|
-- attract loop, then answers each move a few frames into its window -- the right answer, or a
|
||||||
|
-- wrong one for every fifth move so deaths and continues are exercised -- and takes the one
|
||||||
|
-- continue offered. Both scripts hand this file a view of their state (karisView) and a way
|
||||||
|
-- to press and release a switch (karisPress); this file does the deciding.
|
||||||
|
--
|
||||||
|
-- The trace both print: every disc seek, and every change of the framework's screen, step,
|
||||||
|
-- level, scene, move, score, and lives, in the framework's numbers.
|
||||||
|
KARIS_SEED = 20260914
|
||||||
|
|
||||||
|
local lastLine = nil
|
||||||
|
local pressed = nil -- The switch held down this frame, released the next.
|
||||||
|
local started = false
|
||||||
|
local coined = false
|
||||||
|
local continued = false
|
||||||
|
local answered = {} -- Windows already answered, by "level/scene/move/lives".
|
||||||
|
local tried = {} -- Moves met before, by "level/scene/move": a retry after a death.
|
||||||
|
|
||||||
|
-- What the drive does with a move, so that every path of a loop is walked: most moves are
|
||||||
|
-- answered right; every fifth is answered wrong (a stray input, a wrong option, a mash
|
||||||
|
-- broken by a stray input); every seventh is left unanswered (the window runs out). A move
|
||||||
|
-- met again after a death is answered right. PORT_WRONG_EVERY and PORT_MISS_EVERY set the
|
||||||
|
-- two counts, for a run that goes deeper into a game with few lives.
|
||||||
|
local WRONG_EVERY = tonumber(os.getenv("PORT_WRONG_EVERY")) or 5
|
||||||
|
local MISS_EVERY = tonumber(os.getenv("PORT_MISS_EVERY")) or 7
|
||||||
|
local function wrongMove(view)
|
||||||
|
return (view.move % WRONG_EVERY == 0) and not view.retry
|
||||||
|
end
|
||||||
|
local function missedMove(view)
|
||||||
|
return (view.move % MISS_EVERY == 0) and (view.move % WRONG_EVERY ~= 0) and not view.retry
|
||||||
|
end
|
||||||
|
local seeks = 0
|
||||||
|
local plan = nil -- Kimmy: the presses planned for the window being answered.
|
||||||
|
local held = {} -- Kimmy: the switches the plan holds down, by switch.
|
||||||
|
local menuAt = 0 -- Kimmy: the frame a menu was last answered on.
|
||||||
|
KARIS_FRAMES = tonumber(os.getenv("PORT_FRAMES")) or 60000
|
||||||
|
|
||||||
|
-- The framework's move numbers as switches; Kimmy's diagonals are two of them.
|
||||||
|
local SWITCH_OF = { [1] = SWITCH_UP, [2] = SWITCH_DOWN, [3] = SWITCH_LEFT, [4] = SWITCH_RIGHT, [5] = SWITCH_BUTTON1, [6] = SWITCH_BUTTON2, [7] = SWITCH_BUTTON3 }
|
||||||
|
local PAIR_OF = { [9] = { 1, 3 }, [10] = { 1, 4 }, [11] = { 2, 3 }, [12] = { 2, 4 } }
|
||||||
|
local KIMMY = { COMBO = 20, MULTI = 21, LOOP = 22, MASH = 23, HOLD = 24, LETGO = 25, PATH = 51, YESNO = 52, TIMED = 53, ANYTHING = 54, NOTHING = 55, UD = 151, LR = 153, B1B2 = 155 }
|
||||||
|
|
||||||
|
-- The KarisFramework's compound kinds, and the MazescaterFramework's (LINEA), as what they
|
||||||
|
-- take, by the framework's numbers: a sequence's inputs in turn (a circle is five, back to
|
||||||
|
-- its start), a mash's input (or the two it alternates), a hold's input, and the inputs a
|
||||||
|
-- combination wants together.
|
||||||
|
local SEQ_OF = { [35] = { 3, 2, 4, 1, 3 }, [36] = { 4, 2, 3, 1, 4 },
|
||||||
|
[156] = { 1, 3 }, [157] = { 1, 4 }, [158] = { 2, 3 }, [159] = { 2, 4 }, [160] = { 3, 1 }, [161] = { 4, 1 }, [162] = { 3, 2 }, [163] = { 4, 2 },
|
||||||
|
[170] = { 4, 3 }, [171] = { 3, 4 }, [172] = { 1, 2 }, [173] = { 2, 1 },
|
||||||
|
[149] = { 2, 3, 5 }, [150] = { 2, 4, 5 }, [151] = { 1, 3, 5 }, [152] = { 1, 4, 5 }, [164] = { 3, 1, 5 }, [165] = { 4, 1, 5 }, [166] = { 4, 2, 5 }, [167] = { 3, 2, 5 },
|
||||||
|
[168] = { 2, 3, 6 }, [169] = { 2, 4, 6 }, [174] = { 1, 3, 6 }, [175] = { 1, 4, 6 }, [176] = { 3, 1, 6 }, [177] = { 4, 1, 6 }, [178] = { 4, 2, 6 }, [179] = { 3, 2, 6 },
|
||||||
|
[180] = { 2, 3, 1 }, [181] = { 2, 4, 1 }, [182] = { 3, 2, 4 }, [183] = { 3, 1, 4 }, [184] = { 4, 2, 3 }, [185] = { 4, 1, 3 }, [186] = { 1, 3, 2 }, [187] = { 1, 4, 2 },
|
||||||
|
[216] = { 3, 2, 4, 1, 3 }, [219] = { 2, 4, 1, 3, 2 }, [218] = { 4, 1, 3, 2, 4 }, [217] = { 1, 3, 2, 4, 1 },
|
||||||
|
[212] = { 3, 1, 4, 2, 3 }, [213] = { 1, 4, 2, 3, 1 }, [214] = { 4, 2, 3, 1, 4 }, [215] = { 2, 3, 1, 4, 2 } }
|
||||||
|
local MASH_OF = { [17] = { 5 }, [18] = { 5 }, [19] = { 5 }, [27] = { 3, 4 }, [28] = { 3, 4 }, [29] = { 3, 4 }, [30] = { 5, 6 }, [31] = { 5, 6 }, [32] = { 5, 6 },
|
||||||
|
[153] = { 5, 7 }, [154] = { 5, 7 }, [155] = { 5, 7 }, [188] = { 6 }, [189] = { 6 }, [190] = { 6 }, [192] = { 7 }, [193] = { 7 }, [194] = { 7 },
|
||||||
|
[196] = { 3 }, [197] = { 3 }, [198] = { 3 }, [200] = { 4 }, [201] = { 4 }, [202] = { 4 }, [204] = { 1 }, [205] = { 1 }, [206] = { 1 }, [208] = { 2 }, [209] = { 2 }, [210] = { 2 } }
|
||||||
|
local HOLD_OF = { [21] = 1, [22] = 2, [23] = 3, [24] = 4, [25] = 5 }
|
||||||
|
local TOGETHER = { [9] = { 1, 3 }, [10] = { 1, 4 }, [11] = { 2, 3 }, [12] = { 2, 4 }, [13] = { 5, 1 }, [14] = { 5, 2 }, [15] = { 5, 3 }, [16] = { 5, 4 },
|
||||||
|
[220] = { 6, 1 }, [221] = { 6, 2 }, [222] = { 6, 3 }, [223] = { 6, 4 }, [224] = { 7, 1 }, [225] = { 7, 2 }, [226] = { 7, 3 }, [227] = { 7, 4 },
|
||||||
|
[228] = { 5, 1, 3 }, [229] = { 5, 1, 4 }, [230] = { 5, 2, 3 }, [231] = { 5, 2, 4 }, [232] = { 6, 1, 3 }, [233] = { 6, 1, 4 }, [234] = { 6, 2, 3 }, [235] = { 6, 2, 4 },
|
||||||
|
[236] = { 7, 1, 3 }, [237] = { 7, 1, 4 }, [238] = { 7, 2, 3 }, [239] = { 7, 2, 4 } }
|
||||||
|
local KARIS_LETGO, KARIS_MULTI = 20, 33
|
||||||
|
|
||||||
|
|
||||||
|
-- The switches an input number stands for.
|
||||||
|
local function switchesOf(kind)
|
||||||
|
if PAIR_OF[kind] then
|
||||||
|
return { SWITCH_OF[PAIR_OF[kind][1]], SWITCH_OF[PAIR_OF[kind][2]] }
|
||||||
|
end
|
||||||
|
|
||||||
|
return { SWITCH_OF[kind] }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A plan's steps: what to press and let go on which frame.
|
||||||
|
local function planPress(steps, frame, switches)
|
||||||
|
steps[frame] = steps[frame] or { press = {}, release = {} }
|
||||||
|
for _, s in ipairs(switches) do
|
||||||
|
table.insert(steps[frame].press, s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function planRelease(steps, frame, switches)
|
||||||
|
steps[frame] = steps[frame] or { press = {}, release = {} }
|
||||||
|
for _, s in ipairs(switches) do
|
||||||
|
table.insert(steps[frame].release, s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function planTap(steps, frame, switches)
|
||||||
|
planPress(steps, frame, switches)
|
||||||
|
planRelease(steps, frame + 1, switches)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A switch a move does not accept, for a wrong answer.
|
||||||
|
local function wrongFor(accepted)
|
||||||
|
for _, candidate in ipairs({ 2, 1, 3, 4, 6 }) do
|
||||||
|
local ok = true
|
||||||
|
|
||||||
|
for _, a in ipairs(accepted) do
|
||||||
|
if a == candidate then
|
||||||
|
ok = false
|
||||||
|
end
|
||||||
|
if PAIR_OF[a] and ((PAIR_OF[a][1] == candidate) or (PAIR_OF[a][2] == candidate)) then
|
||||||
|
ok = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if ok then
|
||||||
|
return { SWITCH_OF[candidate] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return { SWITCH_BUTTON2 }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A plan: what to press and let go on which frames, for one window.
|
||||||
|
local function planFor(view)
|
||||||
|
local w = view.window
|
||||||
|
local at = w.from + 3
|
||||||
|
local steps = {}
|
||||||
|
local wrong = wrongMove(view)
|
||||||
|
local kind = w.kind
|
||||||
|
|
||||||
|
if missedMove(view) then
|
||||||
|
-- Left unanswered: the window runs out.
|
||||||
|
elseif ((kind >= 1) and (kind <= 7)) or ((kind >= 9) and (kind <= 12)) then
|
||||||
|
if wrong then
|
||||||
|
planTap(steps, at, wrongFor({ kind, w.alt }))
|
||||||
|
else
|
||||||
|
planTap(steps, at, switchesOf(kind))
|
||||||
|
end
|
||||||
|
elseif (kind == KIMMY.MASH) and wrong then
|
||||||
|
-- A mash broken by a stray input.
|
||||||
|
planTap(steps, at, wrongFor({ w.alt or 5 }))
|
||||||
|
elseif kind == KIMMY.COMBO then
|
||||||
|
local both = switchesOf(w.alt)
|
||||||
|
|
||||||
|
for _, s in ipairs(switchesOf(w.alt2)) do
|
||||||
|
both[#both + 1] = s
|
||||||
|
end
|
||||||
|
planTap(steps, at, both)
|
||||||
|
elseif kind == KIMMY.HOLD then
|
||||||
|
planPress(steps, at, switchesOf(w.alt))
|
||||||
|
planRelease(steps, w.to, switchesOf(w.alt))
|
||||||
|
elseif kind == KIMMY.LETGO then
|
||||||
|
planPress(steps, w.from - 4, { SWITCH_BUTTON1 })
|
||||||
|
planRelease(steps, w.from + 2, { SWITCH_BUTTON1 })
|
||||||
|
elseif kind == KIMMY.MASH then
|
||||||
|
local turn = 0
|
||||||
|
|
||||||
|
for frame = at, w.to - 2, 2 do
|
||||||
|
local which
|
||||||
|
|
||||||
|
if w.alt == KIMMY.UD then
|
||||||
|
which = (turn % 2 == 0) and { SWITCH_UP } or { SWITCH_DOWN }
|
||||||
|
elseif w.alt == KIMMY.LR then
|
||||||
|
which = (turn % 2 == 0) and { SWITCH_LEFT } or { SWITCH_RIGHT }
|
||||||
|
elseif w.alt == KIMMY.B1B2 then
|
||||||
|
which = (turn % 2 == 0) and { SWITCH_BUTTON1 } or { SWITCH_BUTTON2 }
|
||||||
|
else
|
||||||
|
which = switchesOf(w.alt or 5)
|
||||||
|
end
|
||||||
|
planTap(steps, frame, which)
|
||||||
|
turn = turn + 1
|
||||||
|
end
|
||||||
|
elseif (kind == KIMMY.MULTI) or (kind == KIMMY.LOOP) then
|
||||||
|
local start = at + ((kind == KIMMY.MULTI) and (w.alt2 or 0) or 0)
|
||||||
|
|
||||||
|
for i, step in ipairs(view.multi or {}) do
|
||||||
|
planTap(steps, start + 2 * (i - 1), switchesOf(step))
|
||||||
|
end
|
||||||
|
elseif kind == KIMMY.PATH then
|
||||||
|
local p = view.path or {}
|
||||||
|
|
||||||
|
if wrong then
|
||||||
|
planTap(steps, at, wrongFor({ p[1], p[3], p[5], p[7] }))
|
||||||
|
elseif p[1] and (p[1] > 0) then
|
||||||
|
planTap(steps, at, switchesOf(p[1]))
|
||||||
|
end
|
||||||
|
elseif kind == KIMMY.YESNO then
|
||||||
|
if view.move % 2 == 0 then
|
||||||
|
planTap(steps, at, { SWITCH_BUTTON1 })
|
||||||
|
end
|
||||||
|
elseif kind == KIMMY.TIMED then
|
||||||
|
local first = (view.timed or {})[w.alt or view.move]
|
||||||
|
|
||||||
|
if first and (first[2] + 1 >= w.from) and (first[2] + 1 <= w.to) then
|
||||||
|
planTap(steps, first[2] + 1, switchesOf(first[1]))
|
||||||
|
end
|
||||||
|
elseif kind == KIMMY.ANYTHING then
|
||||||
|
planTap(steps, at, { SWITCH_BUTTON1 })
|
||||||
|
elseif kind == KIMMY.NOTHING then
|
||||||
|
if wrong then
|
||||||
|
planTap(steps, at, { SWITCH_BUTTON1 })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return { steps = steps, from = w.from - 6, to = w.to + 2 }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A map-mode plan: the ancestor's kinds. A single input tapped; MASH (8) tapped every other
|
||||||
|
-- frame; an ACT combo (9 to 12) as the button with a direction; a diagonal (13 to 16) as two
|
||||||
|
-- directions; SKIP (17) left alone. Every fifth move is answered wrong.
|
||||||
|
local RDG_ACT = { [9] = { 5, 1 }, [10] = { 5, 2 }, [11] = { 5, 3 }, [12] = { 5, 4 }, [13] = { 1, 3 }, [14] = { 1, 4 }, [15] = { 2, 3 }, [16] = { 2, 4 } }
|
||||||
|
|
||||||
|
local function planForRdg(view)
|
||||||
|
local w = view.window
|
||||||
|
local at = w.from + 3
|
||||||
|
local steps = {}
|
||||||
|
local wrong = wrongMove(view)
|
||||||
|
local kind = w.kind
|
||||||
|
|
||||||
|
|
||||||
|
if missedMove(view) then
|
||||||
|
-- Left unanswered: the window runs out.
|
||||||
|
elseif (kind >= 1) and (kind <= 7) then
|
||||||
|
if wrong then
|
||||||
|
planTap(steps, at, { (kind == 1) and SWITCH_DOWN or SWITCH_UP })
|
||||||
|
else
|
||||||
|
planTap(steps, at, { SWITCH_OF[kind] })
|
||||||
|
end
|
||||||
|
elseif (kind == (view.mashKind or 8)) and wrong then
|
||||||
|
-- A mash broken by a stray direction.
|
||||||
|
planTap(steps, at, { SWITCH_UP })
|
||||||
|
elseif kind == (view.mashKind or 8) then
|
||||||
|
for frame = at, w.to - 2, 2 do
|
||||||
|
planTap(steps, frame, { SWITCH_BUTTON1 })
|
||||||
|
end
|
||||||
|
elseif (view.pairs or RDG_ACT)[kind] and view.zone then
|
||||||
|
-- A shot: the pointer into the zone (or well outside it) the frame before the button,
|
||||||
|
-- in the raw coordinates the game scales by its ratios.
|
||||||
|
local z = view.zone
|
||||||
|
local r = view.mouse or { 1, 1, 0, 0 }
|
||||||
|
local mx = wrong and (z[3] + 200) or ((z[1] + z[3]) / 2)
|
||||||
|
local my = wrong and (z[4] + 200) or ((z[2] + z[4]) / 2)
|
||||||
|
|
||||||
|
steps[at - 1] = steps[at - 1] or { press = {}, release = {} }
|
||||||
|
steps[at - 1].mouse = { math.floor((mx + r[3]) / r[1]), math.floor((my + r[4]) / r[2]) }
|
||||||
|
planTap(steps, at, { SWITCH_BUTTON1 })
|
||||||
|
elseif (view.pairs or RDG_ACT)[kind] then
|
||||||
|
local pair = (view.pairs or RDG_ACT)[kind]
|
||||||
|
|
||||||
|
if wrong then
|
||||||
|
planTap(steps, at, { SWITCH_BUTTON2 })
|
||||||
|
else
|
||||||
|
planTap(steps, at, { SWITCH_OF[pair[1]], SWITCH_OF[pair[2]] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return { steps = steps, from = w.from - 6, to = w.to + 2 }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A KarisFramework plan for a compound move: a hold pressed from three frames into the
|
||||||
|
-- window to its end; a let-go held from before the window and released within it; a mash
|
||||||
|
-- tapped every other frame (a run, MASH2, and MASH3 alternating their two); a sequence or a
|
||||||
|
-- circle a step every other frame; a multi tapped as many times as its row asks; a
|
||||||
|
-- combination pressed together. Every fifth move is answered wrong (a stray input, or
|
||||||
|
-- nothing for a let-go); every seventh is left unanswered.
|
||||||
|
local function planForKaris(view)
|
||||||
|
local w = view.window
|
||||||
|
local at = w.from + 3
|
||||||
|
local steps = {}
|
||||||
|
local wrong = wrongMove(view)
|
||||||
|
local kind = w.kind
|
||||||
|
|
||||||
|
local function switches(list)
|
||||||
|
local result = {}
|
||||||
|
|
||||||
|
for _, m in ipairs(list) do
|
||||||
|
result[#result + 1] = SWITCH_OF[m]
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
if missedMove(view) then
|
||||||
|
-- Left unanswered: the window runs out.
|
||||||
|
elseif HOLD_OF[kind] then
|
||||||
|
if wrong then
|
||||||
|
planTap(steps, at, wrongFor({ HOLD_OF[kind] }))
|
||||||
|
else
|
||||||
|
planPress(steps, at, { SWITCH_OF[HOLD_OF[kind]] })
|
||||||
|
planRelease(steps, w.to, { SWITCH_OF[HOLD_OF[kind]] })
|
||||||
|
end
|
||||||
|
elseif kind == KARIS_LETGO then
|
||||||
|
if not wrong then
|
||||||
|
planPress(steps, w.from - 4, { SWITCH_BUTTON1 })
|
||||||
|
planRelease(steps, w.from + 2, { SWITCH_BUTTON1 })
|
||||||
|
end
|
||||||
|
elseif MASH_OF[kind] then
|
||||||
|
if wrong then
|
||||||
|
planTap(steps, at, wrongFor(MASH_OF[kind]))
|
||||||
|
else
|
||||||
|
local turn = 0
|
||||||
|
|
||||||
|
for frame = at, w.to - 2, 2 do
|
||||||
|
planTap(steps, frame, { SWITCH_OF[MASH_OF[kind][(turn % #MASH_OF[kind]) + 1]] })
|
||||||
|
turn = turn + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif SEQ_OF[kind] then
|
||||||
|
if wrong then
|
||||||
|
planTap(steps, at, wrongFor(SEQ_OF[kind]))
|
||||||
|
else
|
||||||
|
for i, m in ipairs(SEQ_OF[kind]) do
|
||||||
|
planTap(steps, at + 2 * (i - 1), { SWITCH_OF[m] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif kind == KARIS_MULTI then
|
||||||
|
if wrong then
|
||||||
|
planTap(steps, at, wrongFor({ w.alt }))
|
||||||
|
else
|
||||||
|
for i = 1, (w.alt2 or 1) do
|
||||||
|
planTap(steps, at + 2 * (i - 1), { SWITCH_OF[w.alt] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif TOGETHER[kind] then
|
||||||
|
if wrong then
|
||||||
|
planTap(steps, at, wrongFor(TOGETHER[kind]))
|
||||||
|
else
|
||||||
|
planTap(steps, at, switches(TOGETHER[kind]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return { steps = steps, from = w.from - 6, to = w.to + 2 }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- A choice: step down to the right option (a wrong one every fifth move), then take it.
|
||||||
|
function answerChoice(view, key, w)
|
||||||
|
if (view.frame >= w.from + 3) and (view.frame <= w.to) then
|
||||||
|
local want = nil
|
||||||
|
local wrong = wrongMove(view)
|
||||||
|
|
||||||
|
for at, index in ipairs(view.order or {}) do
|
||||||
|
if view.choices[index] and (view.choices[index].right ~= wrong) and (want == nil or not wrong) then
|
||||||
|
want = at
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if want and (view.choice < want) then
|
||||||
|
pressed = SWITCH_DOWN
|
||||||
|
karisPress(pressed, true)
|
||||||
|
elseif want and (view.choice == want) and not answered[key] then
|
||||||
|
answered[key] = true
|
||||||
|
pressed = SWITCH_BUTTON1
|
||||||
|
karisPress(pressed, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Runs the plan for this frame.
|
||||||
|
local function followPlan(frame)
|
||||||
|
local step = plan and plan.steps[frame]
|
||||||
|
|
||||||
|
if step then
|
||||||
|
if step.mouse then
|
||||||
|
karisMouse(step.mouse[1], step.mouse[2])
|
||||||
|
end
|
||||||
|
for _, s in ipairs(step.press) do
|
||||||
|
if not held[s] then
|
||||||
|
held[s] = true
|
||||||
|
karisPress(s, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, s in ipairs(step.release) do
|
||||||
|
if held[s] then
|
||||||
|
held[s] = nil
|
||||||
|
karisPress(s, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if plan and (frame > plan.to) then
|
||||||
|
for s in pairs(held) do
|
||||||
|
karisPress(s, false)
|
||||||
|
end
|
||||||
|
held = {}
|
||||||
|
plan = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function karisTraceSeek(frame)
|
||||||
|
seeks = seeks + 1
|
||||||
|
debugPrint(string.format("TRACE seek %d", frame))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Presses what the policy says this frame. view: { screen, step, level, scene, move, frame,
|
||||||
|
-- window = { from, to, kind, death, alt }, choices = { { right }, ... }, order, choice }.
|
||||||
|
function karisDrive(view)
|
||||||
|
if pressed then
|
||||||
|
karisPress(pressed, false)
|
||||||
|
pressed = nil
|
||||||
|
end
|
||||||
|
local line = string.format("TRACE screen %s step %s level %s scene %s move %s score %s lives %s", tostring(view.screen), tostring(view.step), tostring(view.level), tostring(view.scene), tostring(view.move), tostring(view.score), tostring(view.lives))
|
||||||
|
|
||||||
|
if line ~= lastLine then
|
||||||
|
lastLine = line
|
||||||
|
debugPrint(line .. " disc " .. tostring(view.frame))
|
||||||
|
end
|
||||||
|
-- A coin on the attract loop when the game is not free play, then START once it has begun
|
||||||
|
-- playing.
|
||||||
|
if (view.screen == 0) and not started and (view.frame >= 150) and (not view.freeplay) and (view.credits == 0) and not coined then
|
||||||
|
coined = true
|
||||||
|
pressed = SWITCH_COIN1
|
||||||
|
karisPress(pressed, true)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if (view.screen == 0) and not started and (view.frame >= 160) and (view.freeplay or (view.credits > 0)) then
|
||||||
|
started = true
|
||||||
|
pressed = SWITCH_START1
|
||||||
|
karisPress(pressed, true)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Kimmy's menus: the new-game menu (a level the framework never numbered, so nil) takes
|
||||||
|
-- the button for NEW GAME; the level select takes the button for the level shown.
|
||||||
|
if view.kimmy and started and ((view.screen == nil) or (view.screen == 117)) and (view.frame ~= menuAt) and (view.frames > menuAt + 30) then
|
||||||
|
menuAt = view.frames
|
||||||
|
pressed = SWITCH_BUTTON1
|
||||||
|
karisPress(pressed, true)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- The continue screen: taken once.
|
||||||
|
if (view.screen == 100) and (view.step == 1) and not continued then
|
||||||
|
continued = true
|
||||||
|
pressed = SWITCH_START1
|
||||||
|
karisPress(pressed, true)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if (view.screen ~= 103) or (view.window == nil) then
|
||||||
|
if plan then
|
||||||
|
followPlan(view.frame)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- A move is answered once per life: a game that rewinds to the move after a death
|
||||||
|
-- (the later map-mode copies) asks it again, and the drive answers it right that time.
|
||||||
|
local moveKey = tostring(view.level) .. "/" .. tostring(view.scene) .. "/" .. tostring(view.move)
|
||||||
|
local key = moveKey .. "/" .. tostring(view.lives)
|
||||||
|
local w = view.window
|
||||||
|
|
||||||
|
view.retry = (tried[moveKey] ~= nil)
|
||||||
|
|
||||||
|
if view.kimmy or view.rdg then
|
||||||
|
-- A Kimmy or map-mode move: a plan of presses for the window, made when it comes into
|
||||||
|
-- view.
|
||||||
|
if (view.step == 1) and (plan == nil) and not answered[key] and (view.frame >= w.from - 6) and (view.frame <= w.to) then
|
||||||
|
answered[key] = true
|
||||||
|
tried[moveKey] = true
|
||||||
|
plan = view.rdg and planForRdg(view) or planFor(view)
|
||||||
|
elseif view.rdg and (view.step == ((view.timegal and 12) or 11)) and view.choices then
|
||||||
|
answerChoice(view, key, w)
|
||||||
|
end
|
||||||
|
followPlan(view.frame)
|
||||||
|
elseif view.step == 1 then
|
||||||
|
local kind = w.kind
|
||||||
|
|
||||||
|
if (kind >= 1) and (kind <= 7) then
|
||||||
|
-- Waiting for a single input: answer three frames into the window.
|
||||||
|
if (view.frame >= w.from + 3) and (view.frame <= w.to) and not answered[key] then
|
||||||
|
answered[key] = true
|
||||||
|
tried[moveKey] = true
|
||||||
|
if not missedMove(view) then
|
||||||
|
local wrong = wrongMove(view)
|
||||||
|
local press = wrong and ((kind == 1) and SWITCH_DOWN or SWITCH_UP) or SWITCH_OF[kind]
|
||||||
|
|
||||||
|
pressed = press
|
||||||
|
karisPress(press, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif (plan == nil) and not answered[key] and (view.frame >= w.from - 6) and (view.frame <= w.to) and (HOLD_OF[kind] or MASH_OF[kind] or SEQ_OF[kind] or TOGETHER[kind] or (kind == KARIS_LETGO) or (kind == KARIS_MULTI)) then
|
||||||
|
-- A compound move: a plan of presses over the window, made as it comes into view.
|
||||||
|
answered[key] = true
|
||||||
|
tried[moveKey] = true
|
||||||
|
plan = planForKaris(view)
|
||||||
|
end
|
||||||
|
followPlan(view.frame)
|
||||||
|
elseif (view.step == 19) and view.choices then
|
||||||
|
answerChoice(view, key, w)
|
||||||
|
elseif plan then
|
||||||
|
-- The move is judged: what the plan still holds is let go, and the rest of it dropped.
|
||||||
|
for s in pairs(held) do
|
||||||
|
karisPress(s, false)
|
||||||
|
end
|
||||||
|
held, plan = {}, nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- The run is long enough when the game has been played through a continue and out.
|
||||||
|
function karisDone(view)
|
||||||
|
return continued and (view.screen == 0) and (seeks > 0)
|
||||||
|
end
|
||||||
106
testScripts/ports/karisOriginal.singe
Normal file
106
testScripts/ports/karisOriginal.singe
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
-- A KarisFramework game as it ships, driven by karisDrive.singe and traced. PORT_GAME names
|
||||||
|
-- the game's directory in the library and PORT_SCRIPT its main script; run with -v the video
|
||||||
|
-- its games.dat names.
|
||||||
|
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
|
||||||
|
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest/ported")
|
||||||
|
local game = os.getenv("PORT_GAME") or "TimeGal"
|
||||||
|
local script = library .. "/" .. game .. "/" .. (os.getenv("PORT_SCRIPT") or "Timegal.singe")
|
||||||
|
|
||||||
|
dofile(here .. "karisDrive.singe")
|
||||||
|
|
||||||
|
-- What its launcher entry and the engine would have given it, and a seed both runs share.
|
||||||
|
SINGE_LEGACY_SPRITE_ARGS = true
|
||||||
|
local realTime = os.time
|
||||||
|
os.time = function(...) return KARIS_SEED end
|
||||||
|
local realScriptPath = singeGetScriptPath
|
||||||
|
singeGetScriptPath = function() return script end
|
||||||
|
local realSkip = discSkipToFrame
|
||||||
|
discSkipToFrame = function(frame)
|
||||||
|
karisTraceSeek(frame)
|
||||||
|
return realSkip(frame)
|
||||||
|
end
|
||||||
|
dofile(script)
|
||||||
|
singeGetScriptPath = realScriptPath
|
||||||
|
|
||||||
|
local realUpdate = onOverlayUpdate
|
||||||
|
local frames = 0
|
||||||
|
|
||||||
|
|
||||||
|
function karisPress(switch, down)
|
||||||
|
if down then
|
||||||
|
onInputPressed(switch)
|
||||||
|
else
|
||||||
|
onInputReleased(switch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function karisMouse(x, y)
|
||||||
|
if onMouseMoved then
|
||||||
|
onMouseMoved(x, y, 0, 0, 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function view()
|
||||||
|
local m = move and move[currentMove or 0] or nil
|
||||||
|
local window = nil
|
||||||
|
local chosen = nil
|
||||||
|
|
||||||
|
-- A Kimmy game (acombo is its own) hands over the sequences and branches of the move too;
|
||||||
|
-- a map-mode game (segment is its own) says so, for its own numbering of the kinds.
|
||||||
|
local kimmy = (acombo ~= nil)
|
||||||
|
local timegal = (STAGEMAX ~= nil)
|
||||||
|
local rdg = ((segment ~= nil) or timegal) and (acombo == nil)
|
||||||
|
|
||||||
|
if timegal then
|
||||||
|
-- Time Gal's row: clip start, window start, window end, clip end, death clip, kind.
|
||||||
|
if (currentLevel == levelNormal) and m and (m[2] ~= nil) then
|
||||||
|
window = { from = m[2], to = m[3], kind = m[7], death = 0, alt = 0, alt2 = 0 }
|
||||||
|
end
|
||||||
|
elseif (currentLevel == levelNormal) and m and (m[1] ~= nil) then
|
||||||
|
window = { from = m[1], to = m[2], kind = m[3], death = m[4], alt = m[5], alt2 = m[6] }
|
||||||
|
end
|
||||||
|
-- A map-mode shot: a down action whose row carries a zone, and the game's pointer ratios.
|
||||||
|
local zone = (m and (m[3] == ACTDOWN) and (m[7] ~= nil)) and { m[7], m[8], m[9], m[10] } or nil
|
||||||
|
local mouse = ratiox and { ratiox, ratioy, ratioxOffset, ratioyOffset } or nil
|
||||||
|
-- The pair kinds by the game's own numbers (Super Don Quixote numbers them as Karis does).
|
||||||
|
local pairs = nil
|
||||||
|
|
||||||
|
if rdg and ACTUP then
|
||||||
|
pairs = { [ACTUP] = { 5, 1 }, [ACTDOWN] = { 5, 2 }, [ACTLEFT] = { 5, 3 }, [ACTRIGHT] = { 5, 4 }, [UPLEFT] = { 1, 3 }, [UPRIGHT] = { 1, 4 }, [DOWNLEFT] = { 2, 3 }, [DOWNRIGHT] = { 2, 4 } }
|
||||||
|
end
|
||||||
|
-- A choice: the framework's at branch10, the later map-mode copies' at branch02.
|
||||||
|
if ((lvlState == branch10) or (rdg and (lvlState == branch02))) and choice then
|
||||||
|
chosen = {}
|
||||||
|
for index, c in ipairs(choice) do
|
||||||
|
chosen[index] = { right = c[2] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if timegal and (lvlState == branch03) and opt then
|
||||||
|
chosen = {}
|
||||||
|
for index, c in ipairs(opt) do
|
||||||
|
chosen[index] = { right = c[2] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return { screen = currentLevel, step = lvlState, level = iLevel or iCurPos or iStageIndex, scene = iScene or iSegPointer, move = currentMove, frame = discGetFrame(), frames = frames, score = iScore, lives = iLives, window = window, choices = chosen, order = optorder, choice = iChoice,
|
||||||
|
kimmy = kimmy, rdg = rdg, timegal = timegal, multi = kimmy and multi and multi[currentMove] or nil, path = kimmy and path and path[currentMove] or nil, timed = kimmy and timed or nil,
|
||||||
|
freeplay = (dip_CoinsPerCredit == DOPT_FREEPLAY), credits = iCredits, zone = zone, mouse = mouse, pairs = pairs, mashKind = rdg and MASH or nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
if gameflow == flow_GameRunning then
|
||||||
|
karisDrive(view())
|
||||||
|
end
|
||||||
|
local r = realUpdate()
|
||||||
|
|
||||||
|
if (frames > 100) and karisDone(view()) or (frames > KARIS_FRAMES) then
|
||||||
|
debugPrint("TRACE end after " .. frames .. " frames")
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return r
|
||||||
|
end
|
||||||
101
testScripts/ports/karisPort.singe
Normal file
101
testScripts/ports/karisPort.singe
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
-- The Forge port of a KarisFramework game, driven and traced as the original is.
|
||||||
|
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
|
||||||
|
QTE_LEVEL_FINISH = 110
|
||||||
|
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest/ported")
|
||||||
|
local game = os.getenv("PORT_GAME") or "TimeGal"
|
||||||
|
local name = (os.getenv("PORT_SCRIPT") or "Timegal.singe"):gsub("%.singe$", "")
|
||||||
|
local stem = name:match("([^/\\]+)$")
|
||||||
|
|
||||||
|
dofile("Forge/AuthorCompile.singe")
|
||||||
|
dofile(here .. "karisDrive.singe")
|
||||||
|
|
||||||
|
local realSkip = discSkipToFrame
|
||||||
|
discSkipToFrame = function(frame)
|
||||||
|
karisTraceSeek(frame)
|
||||||
|
return realSkip(frame)
|
||||||
|
end
|
||||||
|
dofile(authorBuild(library .. "/" .. game .. "/" .. name .. ".forge", singeGetDataPath() .. stem .. ".port.singe"))
|
||||||
|
|
||||||
|
local realUpdate = onOverlayUpdate
|
||||||
|
local frames = 0
|
||||||
|
|
||||||
|
|
||||||
|
function karisPress(switch, down)
|
||||||
|
if down then
|
||||||
|
authorSwitchDown(switch)
|
||||||
|
else
|
||||||
|
authorSwitchUp(switch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function karisMouse(x, y)
|
||||||
|
onMouseMoved(x, y, 0, 0, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function view()
|
||||||
|
local q = authorEntity("game").q
|
||||||
|
local m = q.move[q.currentMove]
|
||||||
|
local window = nil
|
||||||
|
local chosen = nil
|
||||||
|
|
||||||
|
if q.timegal then
|
||||||
|
if (q.currentLevel == 103) and m and (m[2] ~= nil) then
|
||||||
|
window = { from = m[2], to = m[3], kind = m[7], death = 0, alt = 0, alt2 = 0 }
|
||||||
|
end
|
||||||
|
elseif (q.currentLevel == 103) and m and (m[1] ~= nil) then
|
||||||
|
window = { from = m[1], to = m[2], kind = m[3], death = m[4], alt = m[5], alt2 = m[6] }
|
||||||
|
end
|
||||||
|
local zone = (m and (m[3] == q.ACTDOWN) and (m[7] ~= nil)) and { m[7], m[8], m[9], m[10] } or nil
|
||||||
|
local mouse = q.ratiox and { q.ratiox, q.ratioy, q.ratioxOffset, q.ratioyOffset } or nil
|
||||||
|
local pairs = nil
|
||||||
|
|
||||||
|
if q.rdg and q.ACTUP then
|
||||||
|
pairs = { [q.ACTUP] = { 5, 1 }, [q.ACTDOWN] = { 5, 2 }, [q.ACTLEFT] = { 5, 3 }, [q.ACTRIGHT] = { 5, 4 }, [q.UPLEFT] = { 1, 3 }, [q.UPRIGHT] = { 1, 4 }, [q.DOWNLEFT] = { 2, 3 }, [q.DOWNRIGHT] = { 2, 4 } }
|
||||||
|
end
|
||||||
|
if ((q.lvlState == 19) or (q.rdg and (q.lvlState == 11))) and q.choice then
|
||||||
|
chosen = {}
|
||||||
|
for index, c in ipairs(q.choice) do
|
||||||
|
chosen[index] = { right = c[2] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if q.timegal and (q.lvlState == 12) and q.opt then
|
||||||
|
chosen = {}
|
||||||
|
for index, c in ipairs(q.opt) do
|
||||||
|
chosen[index] = { right = c[2] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kimmy's new-game menu is a level the framework never numbered (nil there); the port
|
||||||
|
-- numbers it, and reports it as the original does.
|
||||||
|
local screen = q.currentLevel
|
||||||
|
|
||||||
|
if q.kimmy and (screen == q.levelNG) then
|
||||||
|
screen = nil
|
||||||
|
end
|
||||||
|
-- The map-mode lineage never numbered its finish screen either.
|
||||||
|
if q.rdg and (screen == QTE_LEVEL_FINISH) then
|
||||||
|
screen = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return { screen = screen, step = q.lvlState, level = q.iLevel or q.iCurPos or q.iStageIndex, scene = q.iScene or q.iSegPointer, move = q.currentMove, frame = discGetFrame(), frames = frames, score = q.iScore, lives = q.iLives, window = window, choices = chosen, order = q.optorder, choice = q.iChoice,
|
||||||
|
kimmy = q.kimmy, rdg = q.rdg, timegal = q.timegal, multi = q.kimmy and q.multi[q.currentMove] or nil, path = q.kimmy and q.path[q.currentMove] or nil, timed = q.kimmy and q.timed or nil,
|
||||||
|
freeplay = (q.dip_CoinsPerCredit == q.DOPT_FREEPLAY), credits = q.iCredits, zone = zone, mouse = mouse, pairs = pairs, mashKind = q.rdg and q.MASH or nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
if authorEntity("game").q.gameflow == "running" then
|
||||||
|
karisDrive(view())
|
||||||
|
end
|
||||||
|
local r = realUpdate()
|
||||||
|
|
||||||
|
if (frames > 100) and karisDone(view()) or (frames > KARIS_FRAMES) then
|
||||||
|
debugPrint("TRACE end after " .. frames .. " frames")
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return r
|
||||||
|
end
|
||||||
49
testScripts/ports/launch.sh
Executable file
49
testScripts/ports/launch.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Launches every port in the ports folder from its games.dat, headless on a virtual screen and
|
||||||
|
# the deterministic clock, for a while each, and counts the runs that printed no error
|
||||||
|
# (FORGE.md section 14.8). Coverage, not a comparison: compare.sh is the comparison.
|
||||||
|
#
|
||||||
|
# testScripts/ports/launch.sh [<ports folder>]
|
||||||
|
#
|
||||||
|
# SINGE the engine binary (default: the newest build in .builddir)
|
||||||
|
# SINGE_RUN a directory to run in (default: a fresh one under /tmp)
|
||||||
|
# SECONDS_EACH how long each port runs (default: 25)
|
||||||
|
# JOBS ports run at once (default: 2; four 1080p games on Xvfb ran a
|
||||||
|
# 30 GB machine out of memory)
|
||||||
|
set -u
|
||||||
|
ports=${1:-$HOME/claude/singePorts}
|
||||||
|
repo=$(cd "$(dirname "$0")/../.." && pwd)
|
||||||
|
binary=${SINGE:-$(ls -t "$repo"/.builddir/linux-gcc/*/singe/Singe-v* "$repo"/.builddir/linux/x86_64/singe/Singe-v* 2>/dev/null | head -1)}
|
||||||
|
run=${SINGE_RUN:-$(mktemp -d /tmp/forgePorts.XXXXXX)}
|
||||||
|
each=${SECONDS_EACH:-25}
|
||||||
|
jobs=${JOBS:-2}
|
||||||
|
if [[ -z "$binary" || ! -x "$binary" ]]; then
|
||||||
|
echo "launch.sh: no engine binary; set SINGE" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
binary=$(realpath "$binary") # The runs change directory.
|
||||||
|
# SDL's offscreen driver has a 1024x768 desktop, smaller than the KarisFramework games ask for.
|
||||||
|
export SDL_VIDEODRIVER=x11 SDL_AUDIO_DRIVER=dummy
|
||||||
|
one() {
|
||||||
|
local script=$1 video=$2
|
||||||
|
local stem=$(basename "$script" .port.singe)
|
||||||
|
local work=$run/$stem
|
||||||
|
rm -rf "$work"
|
||||||
|
mkdir -p "$work"
|
||||||
|
( cd "$run" && timeout -s INT "$each" xvfb-run -a -s "-screen 0 1920x1200x24" "$binary" -w -p -k --deterministic -d "$work" -v "$ports/$video" "$ports/$script" ) > "$work/run.log" 2>&1
|
||||||
|
local rc=$?
|
||||||
|
# ffmpeg's hwaccel fallback notice and the X server going away under the timeout's INT are
|
||||||
|
# the harness, not the game.
|
||||||
|
local bad=$(grep -a "rror\|Author:\|raceback\|FAIL\|does not exist\|Unable to" "$work/run.log" | grep -vc "hwaccel\|XIO:")
|
||||||
|
if [[ "$bad" = 0 && ( $rc = 124 || $rc = 0 ) ]]; then
|
||||||
|
echo "$stem clean"
|
||||||
|
else
|
||||||
|
echo "$stem rc=$rc problems=$bad: $(grep -a "rror\|Author:\|raceback\|does not exist\|Unable to" "$work/run.log" | grep -v "hwaccel\|XIO:" | head -2 | tr '\n' ' ')"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
export -f one
|
||||||
|
export run each binary ports
|
||||||
|
for dat in "$ports"/*/games.dat; do
|
||||||
|
paste -d' ' <(grep -o 'SCRIPT *= *"[^"]*"' "$dat" | sed 's/.*"\(.*\)"/\1/') <(grep -o 'VIDEO *= *"[^"]*"' "$dat" | sed 's/.*"\(.*\)"/\1/')
|
||||||
|
done | xargs -P "$jobs" -L 1 bash -c 'one $0 $1' | tee "$run/results.txt"
|
||||||
|
echo "launch.sh: $(grep -c ' clean$' "$run/results.txt") of $(wc -l < "$run/results.txt") ports ran clean; logs under $run"
|
||||||
22
testScripts/ports/shots.sh
Executable file
22
testScripts/ports/shots.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Runs an original and its port through compare.sh with the drive's screenshots and service walk
|
||||||
|
# on, and gathers the pictures side by side under screenshots/ports/<game>/ in the repository:
|
||||||
|
# original-shot-<moment>.png beside port-shot-<moment>.png for the attract loop, the service
|
||||||
|
# screen, a level just after a shot, the continue, and the board (FORGE.md section 14.15).
|
||||||
|
#
|
||||||
|
# testScripts/ports/shots.sh <family> <game> [frames]
|
||||||
|
#
|
||||||
|
# The comparison's own verdict (the traces agreeing, service walk included) is printed as well.
|
||||||
|
set -u
|
||||||
|
family=${1:?usage: shots.sh <family> <game> [frames]}
|
||||||
|
game=${2:?usage: shots.sh <family> <game> [frames]}
|
||||||
|
frames=${3:-8000}
|
||||||
|
repo=$(cd "$(dirname "$0")/../.." && pwd)
|
||||||
|
name=${game##*/}
|
||||||
|
name=${name%.singe}
|
||||||
|
out=$repo/screenshots/ports/$name
|
||||||
|
rm -rf "$out"
|
||||||
|
SINGE_SHOTS=$out PORT_SHOTS=1 PORT_SERVICE=1 PORT_FRAMES=$frames "$repo/testScripts/ports/compare.sh" "$family" "$game"
|
||||||
|
status=$?
|
||||||
|
ls "$out" 2>/dev/null | sed "s|^|$out/|"
|
||||||
|
exit $status
|
||||||
68
testScripts/ports/sweep.sh
Executable file
68
testScripts/ports/sweep.sh
Executable file
|
|
@ -0,0 +1,68 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Compares every port with its original for a while each (FORGE.md section 14.1), two pairs at
|
||||||
|
# a time, and prints one line per title: the ActionMax five, the KarisFramework games and the
|
||||||
|
# map-mode games of the library, and the Hypseus titles (Karis 3.32b, Kimmy, map-mode, and Time
|
||||||
|
# Gal HD), each through compare.sh's family for it.
|
||||||
|
#
|
||||||
|
# testScripts/ports/sweep.sh [<frames>] (default 6000; 0 for the full length)
|
||||||
|
#
|
||||||
|
# SINGE, SINGE_RUN as compare.sh takes them; the library is ~/claude/singetest.
|
||||||
|
set -u
|
||||||
|
frames=${1:-6000}
|
||||||
|
repo=$(cd "$(dirname "$0")/../.." && pwd)
|
||||||
|
library=$HOME/claude/singetest
|
||||||
|
run=${SINGE_RUN:-$(mktemp -d /tmp/forgeSweep.XXXXXX)}
|
||||||
|
export SINGE_RUN=$run
|
||||||
|
if [[ "$frames" != "0" ]]; then
|
||||||
|
export PORT_FRAMES=$frames
|
||||||
|
fi
|
||||||
|
jobs=()
|
||||||
|
# One pair: family, game, and the library it lives in.
|
||||||
|
pair() {
|
||||||
|
local family=$1 game=$2 lib=$3
|
||||||
|
local out
|
||||||
|
# Each pair runs in a directory of its own: compare.sh refreshes Forge into its run
|
||||||
|
# directory at every start, which would pull it from under a pair already running there.
|
||||||
|
out=$(SINGE_RUN=$run/${game//\//_} SINGE_LIBRARY=$lib "$repo/testScripts/ports/compare.sh" "$family" "$game" 2>&1 | grep -a -v "hwaccel\|XIO" | grep -a "plays as\|differ\|no trace\|compare.sh" | tail -1)
|
||||||
|
echo "${out:-$family $game: no result}"
|
||||||
|
}
|
||||||
|
# Two at a time: three engines have run the machine out of memory.
|
||||||
|
throttle() {
|
||||||
|
while (( $(jobs -rp | wc -l) >= 2 )); do
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
}
|
||||||
|
for game in 38AmbushAlley BlueThunder Hydrosub2021 PopsGhostly SonicFury; do
|
||||||
|
throttle
|
||||||
|
pair actionMax "$game" "$library/ported" &
|
||||||
|
done
|
||||||
|
for desc in "$library"/ported/*/*.forge; do
|
||||||
|
folder=$(basename "$(dirname "$desc")")
|
||||||
|
[[ "$folder" == "ActionMax" || "$folder" == "hypseus" ]] && continue
|
||||||
|
throttle
|
||||||
|
pair karis "$folder/$(basename "${desc%.forge}").singe" "$library/ported" &
|
||||||
|
done
|
||||||
|
# The map-mode games keep their script in Script/ (in ported/ once they agree, in the library
|
||||||
|
# root until then).
|
||||||
|
for lib in "$library/ported" "$library"; do
|
||||||
|
for desc in "$lib"/*/Script/*.forge; do
|
||||||
|
[[ -f "$desc" ]] || continue
|
||||||
|
folder=$(basename "$(dirname "$(dirname "$desc")")")
|
||||||
|
throttle
|
||||||
|
pair karis "$folder/Script/$(basename "${desc%.forge}").singe" "$lib" &
|
||||||
|
done
|
||||||
|
done
|
||||||
|
for title in "$library"/ported/hypseus/*/; do
|
||||||
|
title=$(basename "$title")
|
||||||
|
desc=$(find -L "$library/ported/hypseus/$title/singe" -mindepth 2 -maxdepth 2 -name "*.forge" 2>/dev/null | head -1)
|
||||||
|
[[ -n "$desc" ]] || continue
|
||||||
|
family=hypseus
|
||||||
|
if grep -a -q 'kind = "rdg"\|kind = "timegal"' "$desc"; then
|
||||||
|
family=rdg
|
||||||
|
elif grep -a -q 'kind = "alg"' "$desc"; then
|
||||||
|
family=alg
|
||||||
|
fi
|
||||||
|
throttle
|
||||||
|
pair "$family" "$title" "$library/ported/hypseus" &
|
||||||
|
done
|
||||||
|
wait
|
||||||
200
testScripts/ports/timetravelerDrive.singe
Normal file
200
testScripts/ports/timetravelerDrive.singe
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
-- Drives Hologram Time Traveler (the original or its port) from what the game shows, the same
|
||||||
|
-- way for both. The driver file defines what it takes: a view() of the game's state and
|
||||||
|
-- ttPress(switch, down) to press or let go of a switch; this file decides. The trace lines
|
||||||
|
-- it prints are what compare.sh diffs.
|
||||||
|
--
|
||||||
|
-- The policy: a coin and START at the attract loop; in a level, the move the row asks for
|
||||||
|
-- three frames into its window (a turn first where the row wants a turn and a shot), a wrong
|
||||||
|
-- one on every fifth move and none on every seventh with misses on; a cube against the first
|
||||||
|
-- death with misses on; at the map, the trader once with misses on (a coin buys a cube) and
|
||||||
|
-- the timer's own choice otherwise; a pull at the hellgate's lever; a coin and START at the
|
||||||
|
-- continue; a letter at the board.
|
||||||
|
|
||||||
|
TT_FRAMES = tonumber(os.getenv("PORT_FRAMES")) or 60000
|
||||||
|
|
||||||
|
local perfect = (os.getenv("PORT_PERFECT") == "1")
|
||||||
|
local frames = 0
|
||||||
|
local lastLine = nil
|
||||||
|
local pending = nil -- { switch } pressed this frame, let go the next.
|
||||||
|
local answered = {} -- Windows answered, by level/segment/move/lives.
|
||||||
|
local tried = {} -- Moves met before, by level/segment/move.
|
||||||
|
local coined = false
|
||||||
|
local started = false
|
||||||
|
local cubeUsed = false
|
||||||
|
local traded = false
|
||||||
|
local pulled = false
|
||||||
|
local lettered = false
|
||||||
|
local continues = 0
|
||||||
|
local turnAt = nil -- The frame the turn went in, for the shot after it.
|
||||||
|
|
||||||
|
local SWITCH_OF = nil
|
||||||
|
|
||||||
|
|
||||||
|
function ttTraceSeek(frame)
|
||||||
|
debugPrint(string.format("TRACE seek %d", frame))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function tap(switch)
|
||||||
|
ttPress(switch, true)
|
||||||
|
pending = switch
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- view: { flow, screen, step, alt, level, seg, move, moves, frame, score, lives, cubes, credits,
|
||||||
|
-- window = { from, to }, correct (the row's move, or nil for the tutorial's choice),
|
||||||
|
-- turned (the turn is in), dying, wizard, mapWait, mapCursor, trader, hellgate,
|
||||||
|
-- continuing, board }
|
||||||
|
function ttDrive(view)
|
||||||
|
if pending then
|
||||||
|
ttPress(pending, false)
|
||||||
|
pending = nil
|
||||||
|
end
|
||||||
|
frames = frames + 1
|
||||||
|
if SWITCH_OF == nil then
|
||||||
|
SWITCH_OF = { [view.UP] = SWITCH_UP, [view.DOWN] = SWITCH_DOWN, [view.LEFT] = SWITCH_LEFT, [view.RIGHT] = SWITCH_RIGHT, [view.ACTION] = SWITCH_BUTTON1 }
|
||||||
|
end
|
||||||
|
-- (altState is left out: the original's LCD drawing steps it while the attract loop shows.)
|
||||||
|
local line = string.format("TRACE flow %s screen %s step %s level %s seg %s move %s score %s lives %s cubes %s credits %s",
|
||||||
|
tostring(view.flow), tostring(view.screen), tostring(view.step), tostring(view.level), tostring(view.seg),
|
||||||
|
tostring(view.move), tostring(view.score), tostring(view.lives), tostring(view.cubes), tostring(view.credits))
|
||||||
|
|
||||||
|
if line ~= lastLine then
|
||||||
|
debugPrint(line .. " disc " .. tostring(view.frame))
|
||||||
|
lastLine = line
|
||||||
|
end
|
||||||
|
if frames >= TT_FRAMES then
|
||||||
|
debugPrint("TRACE end after " .. frames .. " frames")
|
||||||
|
singeQuit()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- The attract loop: a coin, then START once the credit shows.
|
||||||
|
if view.intro then
|
||||||
|
if (frames > 200) and not coined then
|
||||||
|
coined = true
|
||||||
|
tap(SWITCH_COIN1)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if coined and (frames > 300) and (frames % 40 == 0) and (view.credits > 0) then
|
||||||
|
tap(SWITCH_START1)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- The continue: a coin, then START, taken once.
|
||||||
|
if view.continuing then
|
||||||
|
if (continues < 1) and (frames % 30 == 0) then
|
||||||
|
if view.credits == 0 then
|
||||||
|
tap(SWITCH_COIN1)
|
||||||
|
else
|
||||||
|
continues = continues + 1
|
||||||
|
tap(SWITCH_START1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- The map: the trader once (with misses on), else the timer's own choice.
|
||||||
|
if view.mapWait then
|
||||||
|
if (not perfect) and (not traded) and (view.mapCursor == view.TRADER) then
|
||||||
|
traded = true
|
||||||
|
tap(SWITCH_BUTTON1)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- The trader: a coin buys a cube.
|
||||||
|
if view.trader then
|
||||||
|
if (frames % 20 == 0) then
|
||||||
|
tap(SWITCH_COIN1)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- The hellgate: one pull at the lever with misses on (a roll can take every life); the
|
||||||
|
-- perfect run lets the clip pass.
|
||||||
|
if view.hellgate then
|
||||||
|
if (not perfect) and not pulled then
|
||||||
|
pulled = true
|
||||||
|
tap(SWITCH_BUTTON1)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
pulled = false
|
||||||
|
-- The board: one letter, and the rest to the clip's end.
|
||||||
|
if view.board then
|
||||||
|
if not lettered then
|
||||||
|
lettered = true
|
||||||
|
tap(SWITCH_BUTTON1)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- A death: a cube against the first, with misses on.
|
||||||
|
if view.dying then
|
||||||
|
if (not perfect) and (not cubeUsed) and (view.cubes > 0) then
|
||||||
|
cubeUsed = true
|
||||||
|
tap(SWITCH_BUTTON2)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- A level's window.
|
||||||
|
if view.window then
|
||||||
|
local moveKey = tostring(view.level) .. "/" .. tostring(view.seg) .. "/" .. tostring(view.move)
|
||||||
|
local key = moveKey .. "/" .. tostring(view.lives)
|
||||||
|
local retry = (tried[moveKey] ~= nil)
|
||||||
|
local at = view.frame
|
||||||
|
|
||||||
|
if (at >= view.window[1] + 3) and (at <= view.window[2]) and not answered[key] then
|
||||||
|
local correct = view.correct
|
||||||
|
|
||||||
|
if correct == nil then
|
||||||
|
-- The tutorial's choice: the sign post's right skips it, nothing plays it.
|
||||||
|
answered[key] = true
|
||||||
|
tried[moveKey] = true
|
||||||
|
if perfect then
|
||||||
|
tap(SWITCH_RIGHT)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if correct == view.NOMOVE then
|
||||||
|
-- A row that wants nothing.
|
||||||
|
answered[key] = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local missed = (not perfect) and (view.move % 7 == 0) and (view.move % 5 ~= 0) and not retry
|
||||||
|
local wrong = (not perfect) and (view.move % 5 == 0) and not retry
|
||||||
|
|
||||||
|
if missed then
|
||||||
|
answered[key] = true
|
||||||
|
tried[moveKey] = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if (correct == view.TURNRIGHTSHOOT) or (correct == view.TURNLEFTSHOOT) then
|
||||||
|
-- A turn, then the shot two frames on.
|
||||||
|
if not view.turned then
|
||||||
|
if turnAt == nil then
|
||||||
|
turnAt = at
|
||||||
|
tap(wrong and SWITCH_UP or ((correct == view.TURNRIGHTSHOOT) and SWITCH_RIGHT or SWITCH_LEFT))
|
||||||
|
if wrong then
|
||||||
|
answered[key] = true
|
||||||
|
tried[moveKey] = true
|
||||||
|
turnAt = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif at >= turnAt + 2 then
|
||||||
|
answered[key] = true
|
||||||
|
tried[moveKey] = true
|
||||||
|
turnAt = nil
|
||||||
|
tap(SWITCH_BUTTON1)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
answered[key] = true
|
||||||
|
tried[moveKey] = true
|
||||||
|
local switch = SWITCH_OF[correct]
|
||||||
|
|
||||||
|
if wrong then
|
||||||
|
switch = (correct == view.UP) and SWITCH_DOWN or SWITCH_UP
|
||||||
|
end
|
||||||
|
if switch then
|
||||||
|
tap(switch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
66
testScripts/ports/timetravelerOriginal.singe
Normal file
66
testScripts/ports/timetravelerOriginal.singe
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
-- Hologram Time Traveler as it ships, driven by timetravelerDrive.singe and traced. PORT_GAME
|
||||||
|
-- names the title's folder under the library and PORT_SCRIPT its script under it; run with -v
|
||||||
|
-- the title's video.
|
||||||
|
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
|
||||||
|
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest")
|
||||||
|
local game = os.getenv("PORT_GAME") or "HologramTimeTraveler"
|
||||||
|
local script = library .. "/" .. (os.getenv("PORT_SCRIPT") or (game .. "/Script/timetraveler.singe"))
|
||||||
|
|
||||||
|
dofile(here .. "timetravelerDrive.singe")
|
||||||
|
|
||||||
|
SINGE_LEGACY_SPRITE_ARGS = true
|
||||||
|
local realScriptPath = singeGetScriptPath
|
||||||
|
singeGetScriptPath = function() return script end
|
||||||
|
local realSkip = discSkipToFrame
|
||||||
|
discSkipToFrame = function(frame)
|
||||||
|
ttTraceSeek(frame)
|
||||||
|
return realSkip(frame)
|
||||||
|
end
|
||||||
|
local realSearch = discSearch
|
||||||
|
discSearch = function(frame)
|
||||||
|
ttTraceSeek(frame)
|
||||||
|
return realSearch(frame)
|
||||||
|
end
|
||||||
|
dofile(script)
|
||||||
|
singeGetScriptPath = realScriptPath
|
||||||
|
|
||||||
|
local realUpdate = onOverlayUpdate
|
||||||
|
|
||||||
|
|
||||||
|
function ttPress(switch, down)
|
||||||
|
if down then
|
||||||
|
onInputPressed(switch)
|
||||||
|
else
|
||||||
|
onInputReleased(switch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function view()
|
||||||
|
local window, correct = nil, nil
|
||||||
|
|
||||||
|
if (currentLevel == levelNormal) and (lvlState == lvlRunning) and move and move[currentMove] and (currentMove <= totalMoves) then
|
||||||
|
window = { move[currentMove][inputFrmStart], move[currentMove][inputFrmEnd] }
|
||||||
|
correct = move[currentMove][correctMove]
|
||||||
|
end
|
||||||
|
return { flow = gameflow, screen = currentLevel, step = lvlState, alt = altState, level = iCurPos, seg = segID, move = currentMove, moves = totalMoves,
|
||||||
|
frame = discGetFrame(), score = iScore, lives = iLives, cubes = iRevCubes, credits = iCredits,
|
||||||
|
window = window, correct = correct, turned = bTurn,
|
||||||
|
intro = (gameflow == flow_GameRunning) and (currentLevel == levelIntro),
|
||||||
|
dying = (currentLevel == levelNormal) and (lvlState == lvlPlayDeath),
|
||||||
|
mapWait = (currentLevel == levelMenuScreen) and (lvlState == lvlRunning), mapCursor = iCurPos,
|
||||||
|
trader = (currentLevel == levelTrader) and ((lvlState == lvlRunning) or (lvlState == branch01)),
|
||||||
|
hellgate = (currentLevel == levelHellgate) and (lvlState == branch01),
|
||||||
|
continuing = (currentLevel == levelContinue) and (lvlState == lvlRunning),
|
||||||
|
board = (currentLevel == levelHighScore) and (lvlState == lvlRunning),
|
||||||
|
UP = UP, DOWN = DOWN, LEFT = LEFT, RIGHT = RIGHT, ACTION = ACTION, NOMOVE = NOMOVE, TURNRIGHTSHOOT = TURNRIGHTSHOOT, TURNLEFTSHOOT = TURNLEFTSHOOT, TRADER = levelTrader }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
onOverlayUpdate = function(...)
|
||||||
|
local result = realUpdate(...)
|
||||||
|
|
||||||
|
ttDrive(view())
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
63
testScripts/ports/timetravelerPort.singe
Normal file
63
testScripts/ports/timetravelerPort.singe
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
-- The Forge port of Hologram Time Traveler, driven and traced as the original is.
|
||||||
|
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
|
||||||
|
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest")
|
||||||
|
local game = os.getenv("PORT_GAME") or "HologramTimeTraveler"
|
||||||
|
local name = (os.getenv("PORT_SCRIPT") or (game .. "/Script/timetraveler.singe")):gsub("%.singe$", "")
|
||||||
|
local stem = name:match("([^/\\]+)$")
|
||||||
|
|
||||||
|
dofile("Forge/AuthorCompile.singe")
|
||||||
|
dofile(here .. "timetravelerDrive.singe")
|
||||||
|
|
||||||
|
local realSkip = discSkipToFrame
|
||||||
|
discSkipToFrame = function(frame)
|
||||||
|
ttTraceSeek(frame)
|
||||||
|
return realSkip(frame)
|
||||||
|
end
|
||||||
|
local realSearch = discSearch
|
||||||
|
discSearch = function(frame)
|
||||||
|
ttTraceSeek(frame)
|
||||||
|
return realSearch(frame)
|
||||||
|
end
|
||||||
|
dofile(authorBuild(library .. "/" .. name .. ".forge", singeGetDataPath() .. stem .. ".port.singe"))
|
||||||
|
|
||||||
|
local realUpdate = onOverlayUpdate
|
||||||
|
|
||||||
|
|
||||||
|
function ttPress(switch, down)
|
||||||
|
if down then
|
||||||
|
authorSwitchDown(switch, nil)
|
||||||
|
else
|
||||||
|
authorSwitchUp(switch, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function view()
|
||||||
|
local q = authorEntity("game").q
|
||||||
|
local window, correct = nil, nil
|
||||||
|
|
||||||
|
if (q.currentLevel == q.levelNormal) and (q.lvlState == q.lvlRunning) and q.move and q.move[q.currentMove] and (q.currentMove <= q.totalMoves) then
|
||||||
|
window = { q.move[q.currentMove][q.inputFrmStart], q.move[q.currentMove][q.inputFrmEnd] }
|
||||||
|
correct = q.move[q.currentMove][q.correctMove]
|
||||||
|
end
|
||||||
|
return { flow = q.gameflow, screen = q.currentLevel, step = q.lvlState, alt = q.altState, level = q.iCurPos, seg = q.segID, move = q.currentMove, moves = q.totalMoves,
|
||||||
|
frame = discGetFrame(), score = q.iScore, lives = q.iLives, cubes = q.iRevCubes, credits = q.iCredits,
|
||||||
|
window = window, correct = correct, turned = q.bTurn,
|
||||||
|
intro = (q.gameflow == q.flow_GameRunning) and (q.currentLevel == q.levelIntro),
|
||||||
|
dying = (q.currentLevel == q.levelNormal) and (q.lvlState == q.lvlPlayDeath),
|
||||||
|
mapWait = (q.currentLevel == q.levelMenuScreen) and (q.lvlState == q.lvlRunning), mapCursor = q.iCurPos,
|
||||||
|
trader = (q.currentLevel == q.levelTrader) and ((q.lvlState == q.lvlRunning) or (q.lvlState == q.branch01)),
|
||||||
|
hellgate = (q.currentLevel == q.levelHellgate) and (q.lvlState == q.branch01),
|
||||||
|
continuing = (q.currentLevel == q.levelContinue) and (q.lvlState == q.lvlRunning),
|
||||||
|
board = (q.currentLevel == q.levelHighScore) and (q.lvlState == q.lvlRunning),
|
||||||
|
UP = q.UP, DOWN = q.DOWN, LEFT = q.LEFT, RIGHT = q.RIGHT, ACTION = q.ACTION, NOMOVE = q.NOMOVE, TURNRIGHTSHOOT = q.TURNRIGHTSHOOT, TURNLEFTSHOOT = q.TURNLEFTSHOOT, TRADER = q.levelTrader }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
onOverlayUpdate = function(...)
|
||||||
|
local result = realUpdate(...)
|
||||||
|
|
||||||
|
ttDrive(view())
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
120
testScripts/ports/videolandDrive.singe
Normal file
120
testScripts/ports/videolandDrive.singe
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
-- Drives Adventures in Videoland: Rollercoaster (the original or its port) from what the game
|
||||||
|
-- shows, the same way for both. The driver file defines what it takes: a view() of the game's
|
||||||
|
-- state and videolandKey(keysym, scancode) to press a key; this file decides. The trace lines
|
||||||
|
-- it prints are what compare.sh diffs.
|
||||||
|
--
|
||||||
|
-- The policy: a name at the name prompt, any key where the game waits for one, and the
|
||||||
|
-- walkthrough's lines typed one character a frame whenever the game waits for a line; with
|
||||||
|
-- PORT_PERFECT=1 the walkthrough is the solution alone, otherwise it opens with the unknown
|
||||||
|
-- words, the inventory play, and two of the deaths (the restaurant's meal and the coaster's
|
||||||
|
-- car) before solving the game from the start again.
|
||||||
|
|
||||||
|
VIDEOLAND_FRAMES = tonumber(os.getenv("PORT_FRAMES")) or 60000
|
||||||
|
|
||||||
|
local perfect = (os.getenv("PORT_PERFECT") == "1")
|
||||||
|
|
||||||
|
-- The solution: the coins, the bear from the gallery, the uniform from the closet, the toolkit,
|
||||||
|
-- the book past the guard, the bear to the dancer for the ticket, the radio from the booth, the
|
||||||
|
-- batteries out of the bear, the jammer made and powered, and the tower.
|
||||||
|
local SOLUTION = { "E", "TAKE COINS", "W", "W", "S", "SHOOT", "N", "N", "E", "TAKE UNIFORM", "WEAR UNIFORM", "W", "S", "W", "TAKE TOOLKIT",
|
||||||
|
"E", "E", "S", "S", "TAKE BOOK", "READ BOOK", "N", "N", "E", "S", "YES", "S", "TAKE TICKET", "W",
|
||||||
|
"E", "E", "PLAY", "RADIO", "OPEN BEAR", "TAKE BATTERIES", "MAKE JAMMER", "PUT BATTERIES", "IN JAMMER",
|
||||||
|
"W", "W", "W", "W", "S", "S", "JAM" }
|
||||||
|
|
||||||
|
-- The detours: words the game does not know, the inventory, a meal that ends the game and a
|
||||||
|
-- yes to play again, a walk to the top of the coaster and the car, and a yes again.
|
||||||
|
local DETOURS = { "DANCE", "HELP", "I", "LOOK", "TAKE ALL", "FIND BATTERIES", "KILL SABOTEUR", "N", "MAYBE", "YES", "HUH", "YES",
|
||||||
|
"E", "TAKE COINS", "DROP COINS", "TAKE COINS", "PUT COINS", "DOWN", "TAKE COINS", "GO NORTH", "W", "W", "N",
|
||||||
|
"EXAMINE BOX", "BREAK DOOR", "VISIT DANCER", "READ TICKET", "S", "W", "N", "N", "YES" }
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
local at = 1
|
||||||
|
local typing = nil -- The line being typed, and how much of it is in.
|
||||||
|
local typed = 0
|
||||||
|
local settle = 0 -- Frames to wait before the next line.
|
||||||
|
local frames = 0
|
||||||
|
local pressedFor = nil -- The wait for any key already answered, by what follows it.
|
||||||
|
local lastLine = nil
|
||||||
|
|
||||||
|
if not perfect then
|
||||||
|
for _, line in ipairs(DETOURS) do
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, line in ipairs(SOLUTION) do
|
||||||
|
lines[#lines + 1] = line
|
||||||
|
end
|
||||||
|
table.insert(lines, 1, "SCOTT")
|
||||||
|
|
||||||
|
-- The states that wait for a typed line (the game's numbers): the name, the restaurant, the
|
||||||
|
-- tent, the parser, the play-again, the prize, and the put-where.
|
||||||
|
local WAITING = { [2] = true, [10] = true, [11] = true, [15] = true, [17] = true, [20] = true, [23] = true }
|
||||||
|
|
||||||
|
|
||||||
|
function videolandTraceSeek(frame)
|
||||||
|
debugPrint(string.format("TRACE seek %d", frame))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- view: { state, next, room, turns, keyboard, keyhit, delay, still, endFrame, frame, prints,
|
||||||
|
-- last (the newest line of the screen), input }
|
||||||
|
function videolandDrive(view)
|
||||||
|
frames = frames + 1
|
||||||
|
local line = string.format("TRACE state %s next %s room %s turns %s kb %s hit %s delay %s still %s end %s prints %s last %s",
|
||||||
|
tostring(view.state), tostring(view.next), tostring(view.room), tostring(view.turns), tostring(view.keyboard), tostring(view.keyhit),
|
||||||
|
tostring(view.delay), tostring(view.still), tostring(view.endFrame), tostring(view.prints), tostring(view.last))
|
||||||
|
|
||||||
|
if line ~= lastLine then
|
||||||
|
debugPrint(line .. " disc " .. tostring(view.frame))
|
||||||
|
lastLine = line
|
||||||
|
end
|
||||||
|
if frames >= VIDEOLAND_FRAMES then
|
||||||
|
debugPrint("TRACE end after " .. frames .. " frames")
|
||||||
|
singeQuit()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Any key where the game waits for one, once per wait (two waits follow each other at the
|
||||||
|
-- story's end, told apart by what comes next). A full stop: the keyboard is still on for
|
||||||
|
-- the frame the wait begins in, and a letter or a space pressed then would go into the line.
|
||||||
|
if view.keyhit then
|
||||||
|
if pressedFor ~= view.next then
|
||||||
|
pressedFor = view.next
|
||||||
|
videolandKey(46, SCANCODE.PERIOD.value)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
pressedFor = nil
|
||||||
|
if typing then
|
||||||
|
-- The line in, a character a frame, then the return key.
|
||||||
|
typed = typed + 1
|
||||||
|
if typed <= string.len(typing) then
|
||||||
|
local c = typing:sub(typed, typed)
|
||||||
|
|
||||||
|
if c == " " then
|
||||||
|
videolandKey(32, SCANCODE.SPACE.value)
|
||||||
|
else
|
||||||
|
videolandKey(string.byte(string.lower(c)), SCANCODE[c].value)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
videolandKey(13, SCANCODE.RETURN.value)
|
||||||
|
typing = nil
|
||||||
|
settle = 3
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if settle > 0 then
|
||||||
|
settle = settle - 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if os.getenv("PORT_DEBUG") == "1" and (frames % 100 == 0) then
|
||||||
|
debugPrint(string.format("DRIVE probe kb %s input [%s] next %s at %s line %s typing %s settle %s", tostring(view.keyboard), tostring(view.input), tostring(view.next), tostring(at), tostring(lines[at]), tostring(typing), tostring(settle)))
|
||||||
|
end
|
||||||
|
-- (Not while a delay runs: the keyboard is on through the bomb's, and a line typed then
|
||||||
|
-- is taken before the play-again prompt and leaves the game in no state at all.)
|
||||||
|
if view.keyboard and (view.delay == 0) and (view.input == "") and WAITING[view.next] and lines[at] then
|
||||||
|
typing = lines[at]
|
||||||
|
typed = 0
|
||||||
|
at = at + 1
|
||||||
|
debugPrint("DRIVE line " .. typing)
|
||||||
|
end
|
||||||
|
end
|
||||||
58
testScripts/ports/videolandOriginal.singe
Normal file
58
testScripts/ports/videolandOriginal.singe
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
-- Adventures in Videoland: Rollercoaster as it ships, driven by videolandDrive.singe and
|
||||||
|
-- traced. PORT_GAME names the title's folder under the library and PORT_SCRIPT its script
|
||||||
|
-- under it; run with -v the title's framefile.
|
||||||
|
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
|
||||||
|
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest")
|
||||||
|
local game = os.getenv("PORT_GAME") or "Rollercoaster"
|
||||||
|
local script = library .. "/" .. (os.getenv("PORT_SCRIPT") or (game .. "/" .. game .. ".singe"))
|
||||||
|
|
||||||
|
dofile(here .. "videolandDrive.singe")
|
||||||
|
|
||||||
|
SINGE_LEGACY_SPRITE_ARGS = true
|
||||||
|
-- The game seeds its random draws from os.time at load: pinned, as the port's is.
|
||||||
|
os.time = function(...) return 12345 end
|
||||||
|
local realScriptPath = singeGetScriptPath
|
||||||
|
singeGetScriptPath = function() return script end
|
||||||
|
local realSkip = discSkipToFrame
|
||||||
|
discSkipToFrame = function(frame)
|
||||||
|
videolandTraceSeek(frame)
|
||||||
|
return realSkip(frame)
|
||||||
|
end
|
||||||
|
local realSearch = discSearch
|
||||||
|
discSearch = function(frame)
|
||||||
|
videolandTraceSeek(frame)
|
||||||
|
return realSearch(frame)
|
||||||
|
end
|
||||||
|
dofile(script)
|
||||||
|
singeGetScriptPath = realScriptPath
|
||||||
|
|
||||||
|
-- Every line the game prints, counted (the wrapped lines too).
|
||||||
|
local prints = 0
|
||||||
|
local realPrint = screenPrint
|
||||||
|
screenPrint = function(what)
|
||||||
|
prints = prints + 1
|
||||||
|
return realPrint(what)
|
||||||
|
end
|
||||||
|
|
||||||
|
local realUpdate = onOverlayUpdate
|
||||||
|
|
||||||
|
|
||||||
|
-- A key, as the engine hands one to a script in the whole keyboard mode.
|
||||||
|
function videolandKey(keysym, scancode)
|
||||||
|
onKeyPressed(keysym, scancode)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function view()
|
||||||
|
return { state = STATE, next = NEXT_STATE, room = L, turns = T, keyboard = (KEYBOARD_ON == ON), keyhit = (KEYHIT_ON == ON), delay = DELAY,
|
||||||
|
still = (SHOW_JPG ~= -1), endFrame = (END_FRAME ~= -1) and END_FRAME or nil, frame = discGetFrame(), prints = prints, last = SCREEN[SCREEN_LINES], input = INPUT }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
onOverlayUpdate = function(...)
|
||||||
|
local result = realUpdate(...)
|
||||||
|
|
||||||
|
videolandDrive(view())
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
57
testScripts/ports/videolandPort.singe
Normal file
57
testScripts/ports/videolandPort.singe
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
-- The Forge port of Adventures in Videoland: Rollercoaster, driven and traced as the original
|
||||||
|
-- is.
|
||||||
|
local here = ((debug.getinfo(1, "S").source:gsub("^@", "")):match("^(.*[/\\])") or "")
|
||||||
|
local library = os.getenv("SINGE_LIBRARY") or (os.getenv("HOME") .. "/claude/singetest")
|
||||||
|
local game = os.getenv("PORT_GAME") or "Rollercoaster"
|
||||||
|
local name = (os.getenv("PORT_SCRIPT") or (game .. "/" .. game .. ".singe")):gsub("%.singe$", "")
|
||||||
|
local stem = name:match("([^/\\]+)$")
|
||||||
|
|
||||||
|
dofile("Forge/AuthorCompile.singe")
|
||||||
|
dofile(here .. "videolandDrive.singe")
|
||||||
|
|
||||||
|
-- The game seeds its random draws from os.time at load; the original's driver pins it, and so
|
||||||
|
-- does this, to the same number.
|
||||||
|
os.time = function(...) return 12345 end
|
||||||
|
|
||||||
|
local realSkip = discSkipToFrame
|
||||||
|
discSkipToFrame = function(frame)
|
||||||
|
videolandTraceSeek(frame)
|
||||||
|
return realSkip(frame)
|
||||||
|
end
|
||||||
|
local realSearch = discSearch
|
||||||
|
discSearch = function(frame)
|
||||||
|
videolandTraceSeek(frame)
|
||||||
|
return realSearch(frame)
|
||||||
|
end
|
||||||
|
dofile(authorBuild(library .. "/" .. name .. ".forge", singeGetDataPath() .. stem .. ".port.singe"))
|
||||||
|
|
||||||
|
local realUpdate = onOverlayUpdate
|
||||||
|
|
||||||
|
|
||||||
|
-- A key, as the engine hands one to the runtime in the whole keyboard mode: its keysym on the
|
||||||
|
-- switch channel, then the key itself. The return and backspace keys' keysyms are switch
|
||||||
|
-- numbers too, and the runtime drops those while the key is down, which a driven run cannot
|
||||||
|
-- show it; they go by the key alone, as they arrive in play.
|
||||||
|
function videolandKey(keysym, scancode)
|
||||||
|
if (keysym ~= 13) and (keysym ~= 8) then
|
||||||
|
authorSwitchDown(keysym, nil)
|
||||||
|
end
|
||||||
|
authorKeyDown(keysym, scancode)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function view()
|
||||||
|
local q = authorEntity("game").q
|
||||||
|
|
||||||
|
return { state = q.state, next = q.next, room = q.L, turns = q.T, keyboard = q.keyboard, keyhit = q.keyhit, delay = q.delay,
|
||||||
|
still = (q.still ~= nil), endFrame = q.endFrame, frame = discGetFrame(), prints = q.prints, last = q.screen[q.lines], input = q.input }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
onOverlayUpdate = function(...)
|
||||||
|
local result = realUpdate(...)
|
||||||
|
|
||||||
|
videolandDrive(view())
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
@ -15,10 +15,10 @@ fontQuality(FONT_QUALITY_BLENDED)
|
||||||
overlaySetResolution(720, 480)
|
overlaySetResolution(720, 480)
|
||||||
|
|
||||||
for _, name in ipairs({ "platformer", "qte", "shmup", "gunvideo" }) do
|
for _, name in ipairs({ "platformer", "qte", "shmup", "gunvideo" }) do
|
||||||
local game = authorLoad("testScripts/author/" .. name .. ".game")
|
local game = authorLoad("testScripts/author/" .. name .. ".forge")
|
||||||
local problems = authorCheck(game)
|
local problems = authorCheck(game)
|
||||||
|
|
||||||
built[name] = authorBuild("testScripts/author/" .. name .. ".game", out .. name .. ".singe")
|
built[name] = authorBuild("testScripts/author/" .. name .. ".forge", out .. name .. ".singe")
|
||||||
debugPrint("AUTHOR built " .. built[name] .. " with " .. #problems .. " problems")
|
debugPrint("AUTHOR built " .. built[name] .. " with " .. #problems .. " problems")
|
||||||
for _, problem in ipairs(problems) do
|
for _, problem in ipairs(problems) do
|
||||||
debugPrint("AUTHOR FAIL " .. name .. ": " .. problem)
|
debugPrint("AUTHOR FAIL " .. name .. ": " .. problem)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||||
fontSelect(font)
|
fontSelect(font)
|
||||||
fontQuality(FONT_QUALITY_BLENDED)
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
|
|
||||||
dofile(authorBuild("testScripts/author/qte.game", singeGetDataPath() .. "qte.singe"))
|
dofile(authorBuild("testScripts/author/qte.forge", singeGetDataPath() .. "qte.singe"))
|
||||||
|
|
||||||
local gameUpdate = onOverlayUpdate
|
local gameUpdate = onOverlayUpdate
|
||||||
local frames = 0
|
local frames = 0
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ dofile("Forge/Forge.singe")
|
||||||
|
|
||||||
local font = fontLoad("Singe/FreeSansBold.ttf", 16)
|
local font = fontLoad("Singe/FreeSansBold.ttf", 16)
|
||||||
local out = singeGetDataPath()
|
local out = singeGetDataPath()
|
||||||
local work = out .. "edited.game"
|
local work = out .. "edited.forge"
|
||||||
|
|
||||||
fontSelect(font)
|
fontSelect(font)
|
||||||
fontQuality(FONT_QUALITY_BLENDED)
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
|
|
@ -20,7 +20,7 @@ overlaySetResolution(720, 480)
|
||||||
|
|
||||||
-- Work on a copy; an editor that writes back to the file it was handed is right, but a test that
|
-- Work on a copy; an editor that writes back to the file it was handed is right, but a test that
|
||||||
-- edits the repository's own sample is not.
|
-- edits the repository's own sample is not.
|
||||||
local source = assert(io.open("testScripts/author/platformer.game", "r"))
|
local source = assert(io.open("testScripts/author/platformer.forge", "r"))
|
||||||
local copy = assert(io.open(work, "w"))
|
local copy = assert(io.open(work, "w"))
|
||||||
|
|
||||||
copy:write(source:read("a"))
|
copy:write(source:read("a"))
|
||||||
|
|
@ -94,8 +94,8 @@ local function step()
|
||||||
-- The invariant the whole editor rests on: a description that has been through load and
|
-- The invariant the whole editor rests on: a description that has been through load and
|
||||||
-- save has to compile to the same game as the one that has not. Without it, opening a
|
-- save has to compile to the same game as the one that has not. Without it, opening a
|
||||||
-- game in the editor and saving it unchanged could quietly alter it.
|
-- game in the editor and saving it unchanged could quietly alter it.
|
||||||
local original = authorLoad("testScripts/author/platformer.game")
|
local original = authorLoad("testScripts/author/platformer.forge")
|
||||||
local trip = out .. "trip.game"
|
local trip = out .. "trip.forge"
|
||||||
|
|
||||||
authorSave(original, trip)
|
authorSave(original, trip)
|
||||||
local again = authorLoad(trip)
|
local again = authorLoad(trip)
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@ dofile("Forge/Forge.singe")
|
||||||
|
|
||||||
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
||||||
local out = singeGetDataPath()
|
local out = singeGetDataPath()
|
||||||
local work = out .. "rules.game"
|
local work = out .. "rules.forge"
|
||||||
|
|
||||||
fontSelect(font)
|
fontSelect(font)
|
||||||
fontQuality(FONT_QUALITY_BLENDED)
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
overlaySetResolution(720, 480)
|
overlaySetResolution(720, 480)
|
||||||
|
|
||||||
local source = assert(io.open("testScripts/author/platformer.game", "r"))
|
local source = assert(io.open("testScripts/author/platformer.forge", "r"))
|
||||||
local copy = assert(io.open(work, "w"))
|
local copy = assert(io.open(work, "w"))
|
||||||
|
|
||||||
copy:write(source:read("a"))
|
copy:write(source:read("a"))
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ dofile("Forge/Forge.singe")
|
||||||
|
|
||||||
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
||||||
local out = singeGetDataPath()
|
local out = singeGetDataPath()
|
||||||
local work = out .. "release.game"
|
local work = out .. "release.forge"
|
||||||
local dest = out .. "Released"
|
local dest = out .. "Released"
|
||||||
|
|
||||||
fontSelect(font)
|
fontSelect(font)
|
||||||
fontQuality(FONT_QUALITY_BLENDED)
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
overlaySetResolution(720, 480)
|
overlaySetResolution(720, 480)
|
||||||
|
|
||||||
local source = assert(io.open("testScripts/author/platformer.game", "r"))
|
local source = assert(io.open("testScripts/author/platformer.forge", "r"))
|
||||||
local copy = assert(io.open(work, "w"))
|
local copy = assert(io.open(work, "w"))
|
||||||
|
|
||||||
copy:write(source:read("a"))
|
copy:write(source:read("a"))
|
||||||
|
|
@ -40,7 +40,7 @@ local function step()
|
||||||
|
|
||||||
elseif stage == 2 then
|
elseif stage == 2 then
|
||||||
-- Everything a standalone game needs has to be there.
|
-- Everything a standalone game needs has to be there.
|
||||||
for _, name in ipairs({ "Platformer.singe", "Author.singe", "games.dat", "Platformer.game" }) do
|
for _, name in ipairs({ "Platformer.singe", "Author.singe", "games.dat", "Platformer.forge" }) do
|
||||||
local f = io.open(dest .. "/" .. name, "r")
|
local f = io.open(dest .. "/" .. name, "r")
|
||||||
|
|
||||||
if f == nil then
|
if f == nil then
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ fontSelect(font)
|
||||||
fontQuality(FONT_QUALITY_BLENDED)
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
overlaySetResolution(720, 480)
|
overlaySetResolution(720, 480)
|
||||||
|
|
||||||
dofile(authorBuild("testScripts/author/shmup.game", singeGetDataPath() .. "shmup.singe"))
|
dofile(authorBuild("testScripts/author/shmup.forge", singeGetDataPath() .. "shmup.singe"))
|
||||||
|
|
||||||
local gameUpdate = onOverlayUpdate
|
local gameUpdate = onOverlayUpdate
|
||||||
local frames = 0
|
local frames = 0
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||||
fontSelect(font)
|
fontSelect(font)
|
||||||
fontQuality(FONT_QUALITY_BLENDED)
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
|
|
||||||
dofile(authorBuild("testScripts/author/gunvideo.game", singeGetDataPath() .. "gunvideo.singe"))
|
dofile(authorBuild("testScripts/author/gunvideo.forge", singeGetDataPath() .. "gunvideo.singe"))
|
||||||
|
|
||||||
local gameUpdate = onOverlayUpdate
|
local gameUpdate = onOverlayUpdate
|
||||||
local frames = 0
|
local frames = 0
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ dofile("Forge/Forge.singe")
|
||||||
|
|
||||||
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
local font = fontLoad("Singe/FreeSansBold.ttf", 15)
|
||||||
local out = singeGetDataPath()
|
local out = singeGetDataPath()
|
||||||
local work = out .. "timeline.game"
|
local work = out .. "timeline.forge"
|
||||||
|
|
||||||
fontSelect(font)
|
fontSelect(font)
|
||||||
fontQuality(FONT_QUALITY_BLENDED)
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
overlaySetResolution(720, 480)
|
overlaySetResolution(720, 480)
|
||||||
|
|
||||||
local source = assert(io.open("testScripts/author/gunvideo.game", "r"))
|
local source = assert(io.open("testScripts/author/gunvideo.forge", "r"))
|
||||||
local copy = assert(io.open(work, "w"))
|
local copy = assert(io.open(work, "w"))
|
||||||
|
|
||||||
copy:write(source:read("a"))
|
copy:write(source:read("a"))
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||||
fontSelect(font)
|
fontSelect(font)
|
||||||
fontQuality(FONT_QUALITY_BLENDED)
|
fontQuality(FONT_QUALITY_BLENDED)
|
||||||
|
|
||||||
dofile(authorBuild("testScripts/author/railshooter.game", singeGetDataPath() .. "railshooter.singe"))
|
dofile(authorBuild("testScripts/author/railshooter.forge", singeGetDataPath() .. "railshooter.singe"))
|
||||||
|
|
||||||
local gameUpdate = onOverlayUpdate
|
local gameUpdate = onOverlayUpdate
|
||||||
local frames = 0
|
local frames = 0
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue