699 lines
27 KiB
Lua
699 lines
27 KiB
Lua
-- Writes a Forge description for a KarisFramework game from the game's own script: the script is
|
|
-- Lua, so it is loaded with the engine stubbed out, its settings and tables read, and setupMoves
|
|
-- called for every level and scene. Nothing is transcribed by hand.
|
|
--
|
|
-- lua5.4 util/forgePortKaris.lua <game directory> <main script> [<name>]
|
|
-- lua5.4 util/forgePortKaris.lua ~/claude/singetest/ported/TimeGal Timegal.singe
|
|
-- lua5.4 util/forgePortKaris.lua ~/claude/singetest/ported/hypseus/Astroboy/singe/Astroboy Astroboy.singe
|
|
-- lua5.4 util/forgePortKaris.lua ~/claude/singetest/hypseus/starblazers/singe/starblazers starblazers.singe
|
|
--
|
|
-- The description goes beside the script as <name>.forge (the script's stem unless given). It is
|
|
-- data for the qte behaviour in Author.singe, which plays the framework's game loop.
|
|
--
|
|
-- The framework is found where the game keeps it: KarisFramework/Script beside the script (the
|
|
-- library's games), Structure or Script beside it, or Framework or FrameworkKimmy beside the
|
|
-- game's folder (the Hypseus zip-ROM layout, singe/<Title>/ and singe/Framework/). Its
|
|
-- version is written into the description, since 3.32b differs from 3.31c in a few things a
|
|
-- player can see (and MazescaterFramework beside the game, LINEA's Karis 3.31c with compound
|
|
-- moves, is marked as such there); a Kimmy Script Engine game gets the kimmy behaviour, and its saves and its
|
|
-- die-and-retry records are snapshotted too, since its new-game menu reads them. A game of
|
|
-- RDG's map-mode lineage (no framework folder: its globals.singe is beside its script and its
|
|
-- levels are functions of its main.singe) gets the rdg behaviour, which runs those functions
|
|
-- itself; its moves are not snapshotted, since the game draws some of them at random at play.
|
|
|
|
local dir = arg[1] or error("a game directory")
|
|
local main = arg[2] or error("the main script")
|
|
local name = arg[3] or main:gsub("%.singe$", "")
|
|
|
|
-- ===== Loading the game's script ==============================================================
|
|
|
|
local env = setmetatable({}, { __index = _G })
|
|
|
|
local function nothing() return 0 end
|
|
|
|
for _, key in ipairs({ "soundLoad", "spriteLoad", "fontLoad", "fontSelect", "fontQuality", "colorForeground", "colorBackground", "discGetWidth", "discGetHeight", "overlaySetResolution", "discSetFPS", "debugPrint", "discSkipToFrame", "discPause", "discPlay", "singeSetGameName", "keyboardSetMode", "overlayClear", "singeGetDataPath", "spriteGetWidth", "spriteGetHeight", "setOverlaySize", "setOverlayResolution", "scoreBezelEnable", "scoreBezelGetState", "discAudioSuffix", "singeSetQuitKeyEnabled", "keyboardCatchQuit", "ratioGetX", "ratioGetY", "vldpGetScale", "overlayGetWidth", "overlayGetHeight", "fontGetHeight", "fontGetWidth", "spriteLoadFrames", "mouseGetPosition", "keyboardCatchQuit", "singeSetQuitKeyEnabled" }) do
|
|
env[key] = nothing
|
|
end
|
|
-- Where the framework's globals are, and its version from their header.
|
|
local frameworkFile
|
|
local frameworkVersion
|
|
local kimmy = false
|
|
local rdg = false
|
|
local timegal = false
|
|
local sdq = false
|
|
local mazescater = false
|
|
-- An American Laser Games title (Mad Dog McCree and its kin): no framework at all, its
|
|
-- hitbox files beside the script; the alg behaviour plays it over the level flows written
|
|
-- by hand in <stem>.levels.lua beside the script.
|
|
local alg = (function()
|
|
local handle = io.open(dir .. "/" .. main, "rb")
|
|
local text = handle and handle:read("a") or ""
|
|
|
|
if handle then
|
|
handle:close()
|
|
end
|
|
-- The family's mark: a shot judged against a hitmap of the frame (shooterHit), its
|
|
-- hitbox files loaded by name, or the typing edition's own globals.
|
|
return (text:find("function shooterHit", 1, true) ~= nil) or (text:find('dofile(MYDIR .. "hitbox-', 1, true) ~= nil) or (text:find("te-globals.singe", 1, true) ~= nil)
|
|
end)()
|
|
-- Adventures in Videoland (Rollercoaster): a text adventure over the disc, its rooms, objects,
|
|
-- and furniture tables in the script; the videoland behaviour plays its loop over them.
|
|
local videoland = (function()
|
|
local handle = io.open(dir .. "/" .. main, "rb")
|
|
local text = handle and handle:read("a") or ""
|
|
|
|
if handle then
|
|
handle:close()
|
|
end
|
|
return (text:find("STATE_GET_NAME", 1, true) ~= nil) and (text:find("ROOMS", 1, true) ~= nil)
|
|
end)()
|
|
-- Hologram Time Traveler (RDG2010's Singe edition): its own loop over segment tables shuffled
|
|
-- at start, read like the American Laser Games titles (the script is its data) and played by
|
|
-- the timetraveler behaviour in the vocabulary beside it.
|
|
local timetraveler = (function()
|
|
local handle = io.open(dir .. "/" .. main, "rb")
|
|
local text = handle and handle:read("a") or ""
|
|
|
|
if handle then
|
|
handle:close()
|
|
end
|
|
return text:find("TIME TRAVELER (SINGE EDITION)", 1, true) ~= nil
|
|
end)()
|
|
if videoland or timetraveler then
|
|
alg = true
|
|
end
|
|
|
|
-- (hq-globals.singe, Time Gal HD's, comes before a plain globals.singe beside it: that title
|
|
-- keeps its Singe 1 ancestor's files too.)
|
|
for _, candidate in ipairs({ dir .. "/MazescaterFramework/Script/globals.singe", dir .. "/KarisFramework/Script/globals.singe", dir .. "/Structure/globals.singe", dir .. "/Script/globals.singe", dir .. "/../Framework/globals.singe", dir .. "/../FrameworkKimmy/globals.singe", dir .. "/hq-globals.singe", dir .. "/globals.singe" }) do
|
|
local handle = io.open(candidate, "r")
|
|
|
|
if handle then
|
|
local text = handle:read("a") or ""
|
|
|
|
-- The MazescaterFramework (LINEA): Karis 3.31c with compound moves, its version
|
|
-- marked as the framework's own so the loop tells the two apart.
|
|
mazescater = (candidate == dir .. "/MazescaterFramework/Script/globals.singe")
|
|
frameworkFile = candidate
|
|
frameworkVersion = (mazescater and "Mazescater " or "") .. (text:match("VERSION:%s*([%w%.]+)") or "")
|
|
kimmy = text:find("KIMMY SCRIPT ENGINE", 1, true) ~= nil
|
|
timegal = (candidate == dir .. "/hq-globals.singe")
|
|
rdg = (candidate == dir .. "/globals.singe") or timegal
|
|
-- Super Don Quixote: map-mode data under a Karis 3.31c judging loop, whose globals
|
|
-- name the Karis kinds (HOLDUP and the rest).
|
|
sdq = rdg and (not timegal) and (text:find("HOLDUP", 1, true) ~= nil)
|
|
handle:close()
|
|
break
|
|
end
|
|
end
|
|
if alg then
|
|
-- The game's own script is its globals: everything it declares at load is its data.
|
|
frameworkFile = dir .. "/" .. main
|
|
frameworkVersion = nil
|
|
rdg = true
|
|
end
|
|
assert(frameworkFile, "no framework globals beside " .. dir)
|
|
env.singeGetScriptPath = function() return dir .. "/" .. main end
|
|
env.random = { new = function() return 0 end }
|
|
-- The map-mode scripts seed from os.clock at load; plain Lua wants a whole number for that.
|
|
env.os = setmetatable({ clock = function() return 1 end }, { __index = os })
|
|
-- A game that asks the filesystem whether its saved config exists (cfgReadPath, through lfs)
|
|
-- hears that it does not, and reads the shipped copy.
|
|
env.require = function() return { attributes = function() return nil end } end
|
|
-- A game that reads its config at load (Mad Dog McCree's readConfig runs as the script does)
|
|
-- reads the real file when the path names one, and nothing otherwise.
|
|
local ioCurrent = nil
|
|
env.io = { open = function() return nil end,
|
|
input = function(path)
|
|
-- The path as given, else its name under the game's Cfg/ folder (the script's
|
|
-- MYDIR is a stub here).
|
|
local base = (type(path) == "string") and path:match("([^/\\]+)$") or nil
|
|
|
|
ioCurrent = nil
|
|
for _, candidate in ipairs({ path, base and (dir .. "/../Cfg/" .. base), base and (dir .. "/Cfg/" .. base), base and (dir .. "/" .. base) }) do
|
|
if (type(candidate) == "string") and (ioCurrent == nil) then
|
|
ioCurrent = io.open(candidate, "r")
|
|
end
|
|
end
|
|
return ioCurrent
|
|
end,
|
|
output = function() return nil end,
|
|
read = function(what)
|
|
if ioCurrent then
|
|
return ioCurrent:read(what or "l")
|
|
end
|
|
return nil
|
|
end,
|
|
write = function() end,
|
|
close = function(handle)
|
|
if handle and (handle == ioCurrent) then
|
|
ioCurrent:close()
|
|
ioCurrent = nil
|
|
end
|
|
end,
|
|
lines = function() return function() return nil end end }
|
|
env.mouseHowMany = function() return 0 end
|
|
env.discSearch = function() end
|
|
env.discSetFPS = function() end
|
|
env.singeWantsCrosshairs = function() return false end
|
|
-- A setupMoves that reads the game's state (Space Ace picks a scene by which levels are
|
|
-- beaten) sees nothing beaten here; the qte behaviour runs the script itself as it plays.
|
|
env.stage = setmetatable({}, { __index = function() return { false, false, 0 } end })
|
|
env.scene = setmetatable({}, { __index = function() return setmetatable({}, { __index = function() return { 0, false } end }) end })
|
|
env.Tiers = {}
|
|
env.Level = {}
|
|
env.Death = {}
|
|
env.move = {}
|
|
env.choice = {}
|
|
env.path = {}
|
|
env.timed = {}
|
|
env.dofile = function(path)
|
|
if alg and (path ~= frameworkFile) then
|
|
-- The title's own files (its hitbox tables, its board, its service) run here too,
|
|
-- under the stubs: the script calls what they define at load (readConfig).
|
|
-- (Beside the script, or in a Script/ folder beside it, as the HD copies keep theirs.)
|
|
local base = path:match("([^/\\]+)$")
|
|
local own = loadfile(dir .. "/" .. base, "t", env) or loadfile(dir .. "/Script/" .. base, "t", env)
|
|
|
|
if own then
|
|
own()
|
|
end
|
|
return
|
|
end
|
|
if path:match("globals%.singe$") then
|
|
-- The framework's globals name the moves; what it loads beyond that is stubbed.
|
|
local chunk = assert(loadfile(frameworkFile, "t", env))
|
|
local inner = env.dofile
|
|
|
|
env.dofile = function() end
|
|
chunk()
|
|
env.dofile = inner
|
|
end
|
|
end
|
|
|
|
if alg then
|
|
-- A hand-written game touches whatever engine calls it likes at load (videoGetVolume, ...):
|
|
-- any name the stubs do not know answers as one that does nothing.
|
|
for _, name in ipairs({ "pairs", "ipairs", "next", "type", "tonumber", "tostring", "select", "pcall", "error", "assert", "setmetatable", "getmetatable", "rawget", "rawset", "rawequal", "print", "unpack" }) do
|
|
if rawget(env, name) == nil then
|
|
env[name] = _G[name] or (name == "unpack" and table.unpack) or nil
|
|
end
|
|
end
|
|
setmetatable(env, { __index = function(_, key)
|
|
local real = _G[key]
|
|
|
|
if real ~= nil then
|
|
return real
|
|
end
|
|
return nothing
|
|
end })
|
|
-- Its framework.singe defines the switch names and the defaults the script reads.
|
|
local frame = loadfile(dir .. "/framework.singe", "t", env)
|
|
|
|
if frame then
|
|
frame()
|
|
end
|
|
end
|
|
-- The game's own directory (the Script/ folder's parent when it has one), for the files the
|
|
-- script joins onto MYDIR at load.
|
|
env.MYDIR = (io.open(dir .. "/../Cfg/", "r") or io.open(dir .. "/../Script/", "r")) and (dir .. "/..") or dir
|
|
assert(loadfile(dir .. "/" .. main, "t", env))()
|
|
|
|
-- ===== The settings ===========================================================================
|
|
|
|
-- The dips, by name, from what the game was last saved with or shipped with: the framework
|
|
-- reads Cfg/game.cfg and falls back to Cfg/default.cfg.
|
|
local function settings()
|
|
local found = {}
|
|
|
|
for _, file in ipairs({ dir .. "/Cfg/game.cfg", dir .. "/Cfg/default.cfg" }) do
|
|
local handle = io.open(file, "r")
|
|
|
|
if handle then
|
|
for line in handle:lines() do
|
|
local key, value = line:match("^%s*([%w_]+)%s*=%s*(-?%d+)")
|
|
|
|
if key and (found[key] == nil) then
|
|
found[key] = tonumber(value)
|
|
end
|
|
end
|
|
handle:close()
|
|
end
|
|
end
|
|
|
|
return found
|
|
end
|
|
|
|
-- ===== The moves ==============================================================================
|
|
|
|
local function copy(t)
|
|
local out = {}
|
|
|
|
for key, value in pairs(t or {}) do
|
|
-- A function is not data (a stub of the converter's, a game's own helper).
|
|
if type(value) == "table" then
|
|
out[key] = copy(value)
|
|
elseif type(value) ~= "function" then
|
|
out[key] = value
|
|
end
|
|
end
|
|
|
|
return out
|
|
end
|
|
|
|
-- A game's moves may differ by difficulty (Space Ace's do: setupMoves reads dip_Difficulty):
|
|
-- each scene is read at every difficulty, and kept once when they agree.
|
|
local function scenesAt(level, difficulty)
|
|
local scenes = {}
|
|
|
|
env.dip_Difficulty = difficulty
|
|
for scene = 1, env.Level[level][4] do
|
|
env.move = {}
|
|
env.choice = {}
|
|
env.path = {}
|
|
env.timed = {}
|
|
env.sceneStart = nil
|
|
env.sceneEnd = nil
|
|
env.totalMoves = 0
|
|
env.setupMoves(level, scene)
|
|
local moves = {}
|
|
|
|
for index = 1, env.totalMoves or 0 do
|
|
moves[index] = copy(env.move[index])
|
|
end
|
|
scenes[scene] = { start = env.sceneStart, finish = env.sceneEnd, moves = moves, choices = copy(env.choice), paths = copy(env.path), timed = copy(env.timed) }
|
|
end
|
|
|
|
return scenes
|
|
end
|
|
|
|
|
|
local function same(a, b)
|
|
if type(a) ~= type(b) then
|
|
return false
|
|
end
|
|
if type(a) ~= "table" then
|
|
return a == b
|
|
end
|
|
for key, value in pairs(a) do
|
|
if not same(value, b[key]) then
|
|
return false
|
|
end
|
|
end
|
|
for key in pairs(b) do
|
|
if a[key] == nil then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
-- The scenes of a level: play, or playBy difficulty when the difficulties differ.
|
|
local function scenesOf(level)
|
|
local by = {}
|
|
local differ = false
|
|
|
|
for difficulty = 0, 3 do
|
|
by[difficulty] = scenesAt(level, difficulty)
|
|
if not same(by[difficulty], by[0]) then
|
|
differ = true
|
|
end
|
|
end
|
|
env.dip_Difficulty = nil
|
|
if differ then
|
|
return nil, by
|
|
end
|
|
|
|
return by[0], nil
|
|
end
|
|
|
|
-- ===== Writing ================================================================================
|
|
|
|
local out = {}
|
|
|
|
local function put(line)
|
|
out[#out + 1] = line
|
|
end
|
|
|
|
local function lua(value, indent)
|
|
indent = indent or ""
|
|
if type(value) == "table" then
|
|
local keys = {}
|
|
local isList = (#value > 0)
|
|
local parts = {}
|
|
|
|
for key in pairs(value) do
|
|
keys[#keys + 1] = key
|
|
end
|
|
table.sort(keys, function(a, b)
|
|
if type(a) == type(b) then
|
|
return a < b
|
|
end
|
|
return type(a) == "number"
|
|
end)
|
|
for _, key in ipairs(keys) do
|
|
local item = lua(value[key], indent .. "\t")
|
|
|
|
if isList and (type(key) == "number") and (key >= 1) and (key <= #value) and (math.floor(key) == key) then
|
|
parts[#parts + 1] = item
|
|
elseif type(key) == "string" and key:match("^[%a_][%w_]*$") then
|
|
parts[#parts + 1] = key .. " = " .. item
|
|
else
|
|
parts[#parts + 1] = "[" .. lua(key) .. "] = " .. item
|
|
end
|
|
end
|
|
local text = "{ " .. table.concat(parts, ", ") .. " }"
|
|
|
|
if #text > 110 then
|
|
text = "{\n" .. indent .. "\t" .. table.concat(parts, ",\n" .. indent .. "\t") .. "\n" .. indent .. "}"
|
|
end
|
|
return text
|
|
elseif type(value) == "string" then
|
|
return string.format("%q", value)
|
|
end
|
|
|
|
return tostring(value)
|
|
end
|
|
|
|
local levels = {}
|
|
|
|
-- A map-mode game's levels are functions of its main.singe, run at play; nothing to read here.
|
|
for index = 1, (rdg and 0 or env.finalstage) do
|
|
local level = env.Level[index]
|
|
|
|
local play, playBy = scenesOf(index)
|
|
|
|
levels[index] = { title = level[1], intro = level[2], introEnd = level[3], scenes = level[4], mirror = level[5], deathMirror = level[6], replay = level[7], play = play, playBy = playBy }
|
|
end
|
|
if (not rdg) and env.AllowSecret and env.Level[env.levelSecret] then
|
|
local level = env.Level[env.levelSecret]
|
|
local play, playBy = scenesOf(env.levelSecret)
|
|
|
|
levels.secret = { title = level[1], intro = level[2], introEnd = level[3], scenes = level[4], mirror = level[5], deathMirror = level[6], replay = level[7], play = play, playBy = playBy }
|
|
end
|
|
|
|
local tiers = {}
|
|
|
|
for index = 0, (env.Tiers and env.Tiers[0] and env.Tiers[0][1]) or 0 do
|
|
tiers[index + 1] = copy(env.Tiers[index])
|
|
end
|
|
|
|
local dips = settings()
|
|
|
|
-- A map-mode game keeps its dips and its board in one file, the one its service.singe reads
|
|
-- (game.cfg or the game's own name beside the script; the later copies read theirs from Cfg/
|
|
-- beside the Script/ folder, through cfgReadPath): the dips as name = value lines (the names
|
|
-- are the game's own), a blank, then ten lines of NAME,score.
|
|
local function mapModeConfigFile()
|
|
local handle = (timegal and io.open(dir .. "/hq-service.singe", "rb")) or io.open(dir .. "/service.singe", "rb") or io.open(dir .. "/Script/service.singe", "rb") or io.open(dir .. "/te-service.singe", "rb")
|
|
local name = "game.cfg"
|
|
|
|
if handle then
|
|
local text = handle:read("a")
|
|
|
|
handle:close()
|
|
-- The config the game reads first: through cfgReadPath (the later copies, whose
|
|
-- MYDIR reads name a default.cfg fallback too), else joined onto MYDIR.
|
|
name = text:match('io%.input%(cfgReadPath%("([^"]+)"%)') or text:match('io%.input%(MYDIR%s*%.%.%s*"([^"]+)"') or text:match('io%.input,%s*MYDIR%s*%.%.%s*"([^"]+)"') or text:match('io%.input%("[^"]-([^"/]+%.cfg)"%)') or name
|
|
name = name:gsub("^/", "")
|
|
end
|
|
for _, candidate in ipairs({ dir .. "/" .. name, dir .. "/../Cfg/" .. name, dir .. "/../" .. name }) do
|
|
local found = io.open(candidate, "r")
|
|
|
|
if found then
|
|
found:close()
|
|
return candidate
|
|
end
|
|
end
|
|
|
|
return dir .. "/" .. name
|
|
end
|
|
|
|
|
|
local function mapModeConfig()
|
|
local found = {}
|
|
local board = {}
|
|
local extra = {} -- Super Don Quixote's best percent per level, after its board.
|
|
local handle = io.open(mapModeConfigFile(), "r")
|
|
|
|
if handle then
|
|
for line in handle:lines() do
|
|
local key, value = line:match("^%s*([%w_]+)%s*=%s*(-?%d+)")
|
|
local who, score = line:match("^%s*([^,=]+),(%d+)%s*$")
|
|
|
|
if key then
|
|
found[key] = tonumber(value)
|
|
elseif who and (#board < 10) then
|
|
board[#board + 1] = { who, tonumber(score) }
|
|
elseif who then
|
|
extra[#extra + 1] = { who, tonumber(score) }
|
|
end
|
|
end
|
|
handle:close()
|
|
end
|
|
|
|
return found, board, extra
|
|
end
|
|
|
|
-- What a map-mode title does differently from the lineage, by the script's stem: the sound of
|
|
-- a right move when it is not sndright, and how long the intro's command screen holds.
|
|
local RDG_TWEAKS = {
|
|
freedomfighter = { rightSound = "sndshot", introHold = 15 }
|
|
}
|
|
|
|
if rdg then
|
|
local rdgDips, rdgBoard, rdgExtra = mapModeConfig()
|
|
|
|
dips = rdgDips
|
|
RDG_BOARD = rdgBoard
|
|
RDG_EXTRA = rdgExtra
|
|
end
|
|
|
|
-- Super Don Quixote's level order is two tables hardcoded in its doLevel: the level after a
|
|
-- beaten one, and the level after a scene that did not beat its level (the game alternates
|
|
-- two levels' scenes); read from the script's text as the game's data.
|
|
local function sdqOrder()
|
|
local handle = io.open(dir .. "/main.singe", "rb")
|
|
local beaten = {}
|
|
local unfinished = {}
|
|
|
|
if handle == nil then
|
|
return nil
|
|
end
|
|
local text = handle:read("a")
|
|
|
|
handle:close()
|
|
local body = text:match("function doLevel%(%).-\nend") or ""
|
|
local split = body:find("if CompleteLevel then", 1, true) or #body
|
|
local before, after = body:sub(1, split), body:sub(split)
|
|
|
|
for from, to in before:gmatch("if%s*%(thisLevel == level(%d+)%)%s*then%s*iCurPos%s*=%s*level(%d+)") do
|
|
beaten[tonumber(from)] = tonumber(to)
|
|
end
|
|
for from, to in after:gmatch("if%s*%(thisLevel == level(%d+)%)%s*then%s*iCurPos%s*=%s*level(%d+)") do
|
|
unfinished[tonumber(from)] = tonumber(to)
|
|
end
|
|
|
|
return { beaten = beaten, unfinished = unfinished }
|
|
end
|
|
|
|
-- The high score board the game ships with (Cfg/hscore.cfg: ten lines of NAME,score): a score
|
|
-- that reaches it goes to the board's screens at the end.
|
|
local function highScores()
|
|
local found = {}
|
|
local handle = io.open(dir .. "/Cfg/hscore.cfg", "r")
|
|
|
|
if handle then
|
|
for line in handle:lines() do
|
|
local who, score = line:match("^%s*([^,]+),(%d+)")
|
|
|
|
if who and (#found < 10) then
|
|
found[#found + 1] = { who, tonumber(score) }
|
|
end
|
|
end
|
|
handle:close()
|
|
end
|
|
|
|
return found
|
|
end
|
|
|
|
-- Kimmy's "die and retry" records, one per difficulty: the four lines after the three boards
|
|
-- and the two trophy blocks of Cfg/hscore.cfg, read as the framework reads them (a line
|
|
-- without a comma-and-name counts 100).
|
|
local function dieAndRetry()
|
|
local records = { 100, 100, 100, 100 }
|
|
local handle = io.open(dir .. "/Cfg/hscore.cfg", "r")
|
|
|
|
if handle == nil then
|
|
return records
|
|
end
|
|
local lines = {}
|
|
|
|
for line in handle:lines() do
|
|
lines[#lines + 1] = line
|
|
end
|
|
handle:close()
|
|
-- 10 + blank, 10 + blank, 10 + blank, 4 + blank, 4 + blank: the records begin at line 44.
|
|
for k = 1, 4 do
|
|
local line = lines[43 + k]
|
|
local score = line and (line:find("!", 1, true) == nil) and line:match("^[^,]*,(.*)$")
|
|
|
|
records[k] = tonumber(score) or 100
|
|
end
|
|
|
|
return records
|
|
end
|
|
|
|
-- Kimmy's save slots, Cfg/s1.cfg to s6.cfg: a line of eleven fields in the framework's
|
|
-- punctuation, then a line per level (its order, started, beaten, deaths).
|
|
local function saves()
|
|
local slots = {}
|
|
|
|
for slot = 1, 6 do
|
|
local handle = io.open(dir .. "/Cfg/s" .. slot .. ".cfg", "r")
|
|
|
|
if handle then
|
|
local first = handle:read("l") or ""
|
|
local line = {}
|
|
local rest = first
|
|
|
|
for _, mark in ipairs({ ",", "!", "?", ";", ":", "A", "B", "C", "D", "E", "F" }) do
|
|
local at = rest:find(mark, 1, true)
|
|
|
|
if at == nil then
|
|
break
|
|
end
|
|
line[#line + 1] = rest:sub(1, at - 1)
|
|
rest = rest:sub(at + 1)
|
|
end
|
|
local levels = {}
|
|
|
|
for entry in handle:lines() do
|
|
local order, started, beaten, deaths = entry:match("^(%d+)A(%a+)B(%a+)C(%d+)D")
|
|
|
|
if order then
|
|
levels[#levels + 1] = { tonumber(order), started == "true", beaten == "true", tonumber(deaths) }
|
|
end
|
|
end
|
|
handle:close()
|
|
slots[slot] = { line = line, levels = levels }
|
|
end
|
|
end
|
|
|
|
return slots
|
|
end
|
|
|
|
-- Everything the game's script declared, under its own names: the scores, the offsets, the
|
|
-- flags, the Level and Death tables, the play order and the tiers. The qte behaviour takes
|
|
-- these as its state, and runs the script itself when it can, for a setupMoves that reads the
|
|
-- state as it plays.
|
|
local settings = {}
|
|
|
|
for key, value in pairs(env) do
|
|
local kind = type(value)
|
|
|
|
if (kind == "number") or (kind == "string") or (kind == "boolean") or (kind == "table") then
|
|
if not (key == "move" or key == "choice" or key == "path" or key == "timed" or key == "io" or key == "os" or key == "string" or key == "math" or key == "table" or key == "sceneStart" or key == "sceneEnd" or key == "totalMoves" or key == "dip_Difficulty" or key == "MYDIR" or key == "BASEDIR" or key == "SINGE_LEGACY_SPRITE_ARGS" or key == "random" or key == "os") then
|
|
settings[key] = (kind == "table") and copy(value) or value
|
|
end
|
|
end
|
|
end
|
|
-- (A game that sets its rate by number rather than by name has no MovieFPS; under the
|
|
-- hand-written games' catch-all stubs the name answers as a function, which is not data.)
|
|
settings.MovieFPS = (type(env.MovieFPS) == "number") and env.MovieFPS or nil
|
|
|
|
-- The level flows of an American Laser Games title, written by hand beside its script.
|
|
local function algLevels()
|
|
local chunk = loadfile(dir .. "/" .. main:gsub("%.singe$", "") .. ".levels.lua", "t", {})
|
|
|
|
return chunk and chunk() or nil
|
|
end
|
|
|
|
local qte = {
|
|
script = main,
|
|
addons = (not rdg) and "Script/addons.singe" or nil,
|
|
framework = frameworkVersion,
|
|
hsDR = kimmy and dieAndRetry() or nil,
|
|
saves = kimmy and saves() or nil,
|
|
settings = settings,
|
|
dips = dips,
|
|
highScores = rdg and RDG_BOARD or highScores(),
|
|
tweaks = rdg and RDG_TWEAKS[name] or nil,
|
|
sdq = sdq and sdqOrder() or nil,
|
|
percents = sdq and RDG_EXTRA or nil,
|
|
flows = alg and algLevels() or nil,
|
|
levels = (not rdg) and levels or nil
|
|
}
|
|
|
|
if videoland then
|
|
-- The objects are set up by the game's own reset; the rooms, furniture, and directions
|
|
-- stand as the script declares them.
|
|
env.resetGameData()
|
|
put("-- " .. name .. ", ported to Forge from its Adventures in Videoland script by util/forgePortKaris.lua:")
|
|
put("-- the game's rooms, objects, and furniture as data for the videoland behaviour, which plays its loop")
|
|
put("-- under the console look; both live in Vocabulary.singe beside this file.")
|
|
put("return {")
|
|
put("\ttitle = " .. lua(name) .. ",")
|
|
put("\tvocabulary = \"Vocabulary.singe\",")
|
|
put("\tplayers = 1,")
|
|
put("\tlayers = { { kind = \"disc\" }, { kind = \"overlay\" } },")
|
|
put("\tvars = { room = 1, turns = 0 },")
|
|
put("")
|
|
put("\ttypes = {")
|
|
put("\t\tgame = { look = { kind = \"console\" }, behaviours = { { kind = \"videoland\" } } }")
|
|
put("\t},")
|
|
put("")
|
|
put("\trooms = {")
|
|
put("\t\t{ name = \"play\", entities = { { type = \"game\", id = \"game\", x = 0, y = 0 } } }")
|
|
put("\t},")
|
|
put("")
|
|
put("\trules = {},")
|
|
put("")
|
|
put("\tvideoland = " .. lua({ script = main, rooms = env.ROOMS, objects = env.OBJECTS, furniture = env.FURNITURE, directions = env.DIRECTIONS, turns = 150 }, "\t"))
|
|
put("}")
|
|
local path = dir .. "/" .. name .. ".forge"
|
|
local file = assert(io.open(path, "w"))
|
|
|
|
file:write(table.concat(out, "\n") .. "\n")
|
|
file:close()
|
|
print("wrote " .. path .. ": " .. #env.ROOMS .. " rooms, " .. #env.OBJECTS .. " objects")
|
|
return
|
|
end
|
|
|
|
local loop = (timetraveler and "timetraveler") or (alg and "alg") or (timegal and "timegal" or (sdq and "sdq" or (rdg and "rdg" or (kimmy and "kimmy" or "qte"))))
|
|
|
|
put("-- " .. (rawget(env, "Title") or name) .. ", ported to Forge from its " .. (timetraveler and "Time Traveler (Singe Edition)" or alg and "American Laser Games" or (timegal and "Time Gal (Singe Edition)" or (rdg and "map-mode" or (kimmy and "Kimmy Script Engine" or (mazescater and "MazescaterFramework" or "KarisFramework"))))) .. " script by util/forgePortKaris.lua:")
|
|
put("-- the framework's tables as data for the " .. loop .. " behaviour, which plays the framework's game loop.")
|
|
put("return {")
|
|
put("\ttitle = " .. lua(name) .. ",")
|
|
if alg then
|
|
-- The title's mechanics and screens are a vocabulary beside its script.
|
|
put("\tvocabulary = \"Vocabulary.singe\",")
|
|
end
|
|
put("\tplayers = 1,")
|
|
put("\tlayers = { { kind = \"disc\" }, { kind = \"overlay\" } },")
|
|
put("\tvars = { score = 0, lives = 0, credits = 0, level = 0, prompt = \"\"" .. (kimmy and ", deaths = 0, lifeBar = 0, tilt = 0" or "") .. " },")
|
|
put("")
|
|
put("\ttypes = {")
|
|
-- A hand-written title's vocabulary draws its HUD as its own files do (the hud look); a
|
|
-- framework's is drawn by the runtime's look for that loop.
|
|
put("\t\tgame = { look = { kind = \"" .. (alg and "hud" or (loop .. "Hud")) .. "\" }, behaviours = { { kind = \"" .. loop .. "\" } } }")
|
|
put("\t},")
|
|
put("")
|
|
put("\trooms = {")
|
|
put("\t\t{ name = \"play\", entities = { { type = \"game\", id = \"game\", x = 0, y = 0 } } }")
|
|
put("\t},")
|
|
put("")
|
|
put("\trules = {},")
|
|
put("")
|
|
put("\tqte = " .. lua(qte, "\t"))
|
|
put("}")
|
|
|
|
local path = dir .. "/" .. name .. ".forge"
|
|
local file = assert(io.open(path, "w"))
|
|
|
|
file:write(table.concat(out, "\n") .. "\n")
|
|
file:close()
|
|
print("wrote " .. path .. ": " .. #levels .. " levels, " .. #env.Death .. " deaths")
|