1177 lines
43 KiB
Lua
1177 lines
43 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$", "")
|
|
|
|
-- A game that draws at random as it loads (a scene swap, a level's variant) draws the same every
|
|
-- time, so a description made twice is made the same.
|
|
local CONVERT_SEED = 12345
|
|
|
|
math.randomseed(CONVERT_SEED)
|
|
|
|
-- ===== 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 the game's config files in Cfg/, the first to name one winning.
|
|
local function configFiles(names)
|
|
local found = {}
|
|
|
|
for _, name in ipairs(names) do
|
|
local handle = io.open(dir .. "/Cfg/" .. name, "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
|
|
|
|
-- 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()
|
|
return configFiles({ "game.cfg", "default.cfg" })
|
|
end
|
|
|
|
-- What the service screen's "Default" restores: Cfg/default.cfg alone.
|
|
local function defaults()
|
|
return configFiles({ "default.cfg" })
|
|
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
|
|
|
|
-- A game's swapScene is a function of the difficulty, the level, and the scene: Tron plays a
|
|
-- different scene of five levels on the two easier settings. Nothing else goes into it, so every
|
|
-- answer can be read here and written as a table -- difficulty, then level, then the scene asked
|
|
-- for, giving the scene played -- and a game whose only hook is this one needs no Lua at play.
|
|
-- Only the answers that differ are kept, so a game that swaps nothing writes nothing.
|
|
local function swaps()
|
|
if type(env.swapScene) ~= "function" then
|
|
return nil
|
|
end
|
|
local out = {}
|
|
local found = false
|
|
|
|
for difficulty = 0, 3 do
|
|
local byLevel = {}
|
|
|
|
for level = 1, (env.finalstage or 0) do
|
|
local scenes = (env.Level[level] or {})[4] or 0
|
|
local byScene = {}
|
|
|
|
for scene = 1, scenes do
|
|
env.dip_Difficulty = difficulty
|
|
env.iLevel = level
|
|
env.iScene = scene
|
|
env.bSwap = false
|
|
local ok = pcall(env.swapScene)
|
|
|
|
if ok and (type(env.iScene) == "number") and (env.iScene ~= scene) then
|
|
byScene[scene] = env.iScene
|
|
found = true
|
|
end
|
|
end
|
|
if next(byScene) then
|
|
byLevel[level] = byScene
|
|
end
|
|
end
|
|
if next(byLevel) then
|
|
out[difficulty] = byLevel
|
|
end
|
|
end
|
|
env.dip_Difficulty = nil
|
|
env.iLevel = nil
|
|
env.iScene = nil
|
|
env.bSwap = nil
|
|
|
|
return found and out or nil
|
|
end
|
|
|
|
-- ===== Writing ================================================================================
|
|
|
|
local out = {}
|
|
|
|
-- The original author's credits, from the game's own games.dat and script header (forgeCredits.lua
|
|
-- beside this file), so a port names whoever made the game.
|
|
dofile((arg[0]:match("^(.*[/\\])") or "") .. "forgeCredits.lua")
|
|
|
|
|
|
local function put(line)
|
|
out[#out + 1] = line
|
|
end
|
|
|
|
|
|
local function putCredits()
|
|
local line = creditsFor(dir, main)
|
|
|
|
if line then
|
|
put(line)
|
|
end
|
|
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 level row as the description keeps it: the one place its fields are written down, the
|
|
-- runtime building the loops' positional rows from it. play and playBy are its scenes, for a
|
|
-- level the game can reach.
|
|
local function levelOf(level, play, playBy)
|
|
return { title = level[1], intro = level[2], introEnd = level[3], scenes = level[4], mirror = level[5], deathMirror = level[6], replay = level[7], selectStill = level[8], play = play, playBy = playBy }
|
|
end
|
|
|
|
-- Every row the game declares, those past its last stage and a secret one it cannot reach
|
|
-- included, since the game's own Lua may read them. A map-mode game's levels are functions of
|
|
-- its main.singe, run at play; nothing to read here.
|
|
if not rdg then
|
|
for index, level in pairs(env.Level) do
|
|
if (type(index) == "number") and (index ~= env.levelSecret) then
|
|
local play, playBy = nil, nil
|
|
|
|
if index <= env.finalstage then
|
|
play, playBy = scenesOf(index)
|
|
end
|
|
levels[index] = levelOf(level, play, playBy)
|
|
end
|
|
end
|
|
if env.Level[env.levelSecret] then
|
|
local play, playBy = nil, nil
|
|
|
|
if env.AllowSecret then
|
|
play, playBy = scenesOf(env.levelSecret)
|
|
end
|
|
levels.secret = levelOf(env.Level[env.levelSecret], play, playBy)
|
|
end
|
|
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.
|
|
-- The game's boards from Cfg/hscore.cfg, as the framework reads them: ten lines each, a blank
|
|
-- line between, for the normal game, the life-bar game, and the survivor game; then, after the
|
|
-- trophy counts, each level's best percent ("name,percent!difficulty").
|
|
local function boards()
|
|
local found = { normal = {}, lifeBar = {}, survivor = {}, percent = {} }
|
|
local order = { "normal", "lifeBar", "survivor" }
|
|
local handle = io.open(dir .. "/Cfg/hscore.cfg", "r")
|
|
local which = 1
|
|
|
|
if handle then
|
|
for line in handle:lines() do
|
|
local who, score = line:match("^%s*([^,]+),(%d+)%s*$")
|
|
local board = found[order[which]]
|
|
|
|
local best, percent, difficulty = line:match("^%s*([^,]+),(%d+)!(%a)%s*$")
|
|
|
|
if board and who then
|
|
board[#board + 1] = { who, tonumber(score) }
|
|
if #board == 10 then
|
|
which = which + 1
|
|
end
|
|
elseif best then
|
|
found.percent[#found.percent + 1] = { best, tonumber(percent), difficulty }
|
|
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 table literal of Author.singe's, by its local's name: the runtime's own names and constants,
|
|
-- read from the runtime so they are written once.
|
|
local authorSource = nil
|
|
|
|
local function authorLiteral(name)
|
|
if authorSource == nil then
|
|
local path = (arg[0]:match("^(.*[/\\])") or "") .. "../forge/Author.singe"
|
|
local handle = io.open(path, "rb") or error("cannot read " .. path)
|
|
|
|
authorSource = handle:read("a")
|
|
handle:close()
|
|
end
|
|
local body = authorSource:match("\nlocal " .. name .. " = (%b{})") or error("no " .. name .. " in Author.singe")
|
|
|
|
return load("return " .. body, name, "t", {})()
|
|
end
|
|
|
|
-- The clips and stills under Forge's names, out of the settings: the names are Author.singe's
|
|
-- own QTE_CLIPS table, read from there so they are written once; a still is a frame global
|
|
-- named by what follows "frame"; the attract steps are offsetIntro01, 02, and on.
|
|
local clips, stills = {}, {}
|
|
|
|
do
|
|
local names = authorLiteral("QTE_CLIPS")
|
|
|
|
if not rdg then
|
|
for _, clip in ipairs(names) do
|
|
if settings[clip[2]] ~= nil then
|
|
clips[clip[1]] = { settings[clip[2]], settings[clip[3]] }
|
|
settings[clip[2]] = nil
|
|
settings[clip[3]] = nil
|
|
end
|
|
end
|
|
local attract = {}
|
|
|
|
while settings[string.format("offsetIntro%02d", #attract + 1)] ~= nil do
|
|
local step = #attract + 1
|
|
|
|
attract[step] = { settings[string.format("offsetIntro%02d", step)], settings[string.format("offsetIntro%02dend", step)] }
|
|
settings[string.format("offsetIntro%02d", step)] = nil
|
|
settings[string.format("offsetIntro%02dend", step)] = nil
|
|
end
|
|
if #attract > 0 then
|
|
clips.attract = attract
|
|
end
|
|
for key, value in pairs(settings) do
|
|
local name = key:match("^frame(%u%w*)$")
|
|
|
|
if name and (type(value) == "number") then
|
|
stills[name:sub(1, 1):lower() .. name:sub(2)] = value
|
|
settings[key] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- The other named fields, by Author.singe's QTE_FIELDS: each value leaves the settings for its
|
|
-- group (scoring, lifeBar, windowDelay, order, deaths, secret, video).
|
|
local groups = {}
|
|
|
|
if not rdg then
|
|
for _, field in ipairs(authorLiteral("QTE_FIELDS")) do
|
|
local group, name = field[1]:match("^(%w+)%.(%w+)$")
|
|
|
|
if settings[field[2]] ~= nil then
|
|
groups[group] = groups[group] or {}
|
|
groups[group][name] = settings[field[2]]
|
|
settings[field[2]] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- A setting equal to the constant the runtime lays down before the settings (Author.singe's QTE
|
|
-- table, and HELD over it for a held game) is a copy of it, and is left out.
|
|
do
|
|
local constants = {}
|
|
|
|
for _, tableName in ipairs(kimmy and { "QTE", "HELD" } or { "QTE" }) do
|
|
for key, value in pairs(authorLiteral(tableName)) do
|
|
if (key ~= "FLAG_OF") and (key ~= "BRANCH") then
|
|
constants[key] = value
|
|
end
|
|
end
|
|
end
|
|
for key, value in pairs(settings) do
|
|
if (type(value) ~= "table") and (constants[key] == value) then
|
|
settings[key] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- The segmented lineage's runtime reads no settings (its game's own Lua declares them all), so
|
|
-- its descriptions carry none (see the qte table below).
|
|
-- One source for each: the levels are in levels, and a dip is in dips (the runtime lays the dips
|
|
-- over the settings, so a copy here would only look like it mattered).
|
|
if not rdg then
|
|
settings.Level = nil
|
|
end
|
|
for key in pairs(dips or {}) do
|
|
settings[key] = nil
|
|
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
|
|
|
|
-- The add-ons, last of all: they carry the hooks the loop calls, and a hook that is a function
|
|
-- of the state can be read here rather than run at play (see swaps). They are loaded after the
|
|
-- settings are taken, because their own load-time globals (a sound handle, a map cursor) belong
|
|
-- to them and not to the game's declarations -- a snapshot that carried them would describe a
|
|
-- state the game never starts in. A file that will not load under the stubs is no loss; the
|
|
-- game still names it and the loop still runs it.
|
|
-- The functions they define or replace are the hooks the loop calls into the game's own Lua, which
|
|
-- the ledger names (FORGE_LEDGER, below).
|
|
local hooks = {}
|
|
|
|
do
|
|
local chunk = loadfile(dir .. "/Script/addons.singe", "t", env)
|
|
|
|
if chunk then
|
|
local before = {}
|
|
|
|
for key, value in pairs(env) do
|
|
before[key] = value
|
|
end
|
|
pcall(chunk)
|
|
-- A hook whose body is only blank lines and comments is a stub, and does nothing.
|
|
local source = {}
|
|
local handle = io.open(dir .. "/Script/addons.singe", "rb")
|
|
|
|
for line in (handle and handle:read("a") or ""):gmatch("([^\n]*)\n?") do
|
|
source[#source + 1] = line
|
|
end
|
|
if handle then
|
|
handle:close()
|
|
end
|
|
for key, value in pairs(env) do
|
|
if (type(value) == "function") and (before[key] ~= value) then
|
|
local info = debug.getinfo(value, "S")
|
|
local body = {}
|
|
|
|
for at = info.linedefined + 1, info.lastlinedefined - 1 do
|
|
body[#body + 1] = (source[at] or ""):gsub("%-%-.*$", "")
|
|
end
|
|
-- The one-line form, "function name() end", has its body on its own line.
|
|
if info.linedefined == info.lastlinedefined then
|
|
body[1] = (source[info.linedefined] or ""):gsub("%-%-.*$", ""):gsub("^.-%b()", ""):gsub("end%s*$", "")
|
|
end
|
|
hooks[#hooks + 1] = { name = key, empty = not table.concat(body, " "):find("%S") }
|
|
end
|
|
end
|
|
table.sort(hooks, function(a, b) return a.name < b.name end)
|
|
end
|
|
end
|
|
|
|
-- Where the games the qte behaviour plays differ, the description says which way, by name
|
|
-- (FORGE.md 15.4): the runtime never asks which framework or version a game came from. Only
|
|
-- the qte loop reads these; the other loops' games carry none.
|
|
local function optionsFor()
|
|
if alg or timetraveler then
|
|
return nil
|
|
end
|
|
local options = {}
|
|
|
|
-- How the qte behaviour judges and runs this game; the default, windows, is left unsaid.
|
|
options.style = (timegal and "checkpoints") or (sdq and "trophies") or (rdg and "segments") or (kimmy and "held") or nil
|
|
|
|
if frameworkVersion == "3.32b" then
|
|
options.gameOverAfterContinue = "seek"
|
|
options.secretInputs = { "p1BUTTON4", "p1UP" }
|
|
end
|
|
if mazescater then
|
|
options.hintSeconds = 4
|
|
options.alternateMashRate = 6
|
|
options.mashAnySwitch = true
|
|
options.doubleKind = 34
|
|
end
|
|
|
|
return (next(options) ~= nil) and options or nil
|
|
end
|
|
|
|
local shipped = defaults()
|
|
|
|
local qte = {
|
|
script = main,
|
|
addons = (not rdg) and "Script/addons.singe" or nil,
|
|
options = optionsFor(),
|
|
hsDR = kimmy and dieAndRetry() or nil,
|
|
saves = (not rdg) and saves() or nil,
|
|
settings = (not rdg) and settings or nil,
|
|
clips = next(clips) and clips or nil,
|
|
stills = next(stills) and stills or nil,
|
|
scoring = groups.scoring,
|
|
lifeBar = groups.lifeBar,
|
|
windowDelay = groups.windowDelay,
|
|
order = groups.order,
|
|
deaths = groups.deaths,
|
|
secret = groups.secret,
|
|
video = groups.video,
|
|
dips = dips,
|
|
defaults = (not rdg) and next(shipped) and shipped or nil,
|
|
swaps = (not rdg) and swaps() or nil,
|
|
highScores = rdg and RDG_BOARD or nil,
|
|
boards = (not rdg) and boards() or nil,
|
|
tweaks = rdg and RDG_TWEAKS[name] or nil,
|
|
trophyOrder = 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
|
|
}
|
|
|
|
-- ===== The ledger ===============================================================================
|
|
--
|
|
-- FORGE_LEDGER=<file> writes, instead of the description, what the title uses and how this
|
|
-- converter maps each thing (FORGE.md section 17), one line each: a status, a capability, and a
|
|
-- detail. "forge" is a field of Forge's own model; "framework" is something Forge plays under
|
|
-- the framework's names or numbers (a dip, a leftover global, a move kind, a style); "lua" is
|
|
-- something the game's own Lua still does. util/forgeLedger.py gathers these over the library.
|
|
|
|
-- The judge (FORGE.md 15.4) each framework move kind is, by the kind's name in Author.singe's QTE
|
|
-- table, or HELD's for a held game.
|
|
local KIND_JUDGES = {
|
|
UP = "press.direction", DOWN = "press.direction", LEFT = "press.direction", RIGHT = "press.direction",
|
|
BUTTON1 = "press.button", BUTTON2 = "press.button", BUTTON3 = "press.button", BUTTON4 = "press.button",
|
|
UPLEFT = "press.diagonal", UPRIGHT = "press.diagonal", DOWNLEFT = "press.diagonal", DOWNRIGHT = "press.diagonal",
|
|
ACTUP = "chord.buttonDirection", ACTDOWN = "chord.buttonDirection", ACTLEFT = "chord.buttonDirection", ACTRIGHT = "chord.buttonDirection",
|
|
COMBO = "chord", MASH = "mash.one", MASHMIN = "mash.one", MASHMAX = "mash.one",
|
|
MASH2 = "mash.two", MASH2MIN = "mash.two", MASH2MAX = "mash.two", RUN = "mash.run", RUNMIN = "mash.run", RUNMAX = "mash.run",
|
|
HOLDUP = "hold.direction", HOLDDOWN = "hold.direction", HOLDLEFT = "hold.direction", HOLDRIGHT = "hold.direction",
|
|
HOLDBUT = "hold.button", HOLD = "hold", LETGO = "letGo", DOUBLE = "double", MULTI = "count",
|
|
LOOP = "circle", LOOPLEFT = "circle", LOOPRIGHT = "circle", UD = "sequence", DU = "sequence", LR = "sequence", RL = "sequence", B1B2 = "sequence",
|
|
CHOOSE = "choose", PATH = "path", YESNO = "yesNo", TIMED = "timed", MTIMED = "timed.multi",
|
|
ANYTHING = "anything", NOTHING = "nothing", SKIP = "skip",
|
|
WAY = "jump", WAYOUT = "jump", TOLEVEL = "jump", TOSCENE = "jump"
|
|
}
|
|
|
|
|
|
local function ledger(path)
|
|
local lines = {}
|
|
local seen = {}
|
|
|
|
local function use(status, capability, detail)
|
|
local key = status .. "\t" .. capability .. "\t" .. (detail or "")
|
|
|
|
if not seen[key] then
|
|
seen[key] = true
|
|
lines[#lines + 1] = key
|
|
end
|
|
end
|
|
|
|
local family = (videoland and "videoland") or (timetraveler and "timetraveler") or (alg and "alg") or ("qte." .. ((qte.options and qte.options.style) or "windows"))
|
|
|
|
use("forge", "family." .. family, frameworkVersion)
|
|
-- The moves: every row's kind, as the judge it is.
|
|
local names = {}
|
|
|
|
for _, tableName in ipairs(kimmy and { "QTE", "HELD" } or { "QTE" }) do
|
|
for key, value in pairs(authorLiteral(tableName)) do
|
|
if KIND_JUDGES[key] and (type(value) == "number") then
|
|
names[value] = key
|
|
end
|
|
end
|
|
end
|
|
local function moves(value)
|
|
for key, item in pairs(value) do
|
|
if (key == "moves") and (type(item) == "table") then
|
|
for _, row in ipairs(item) do
|
|
if (type(row) == "table") and (type(row[3]) == "number") then
|
|
local kind = names[row[3]]
|
|
|
|
if kind then
|
|
use("framework", "judge." .. KIND_JUDGES[kind], kind)
|
|
else
|
|
-- Not a kind Forge names: the framework's own constants of that value,
|
|
-- as candidates for what it is.
|
|
local candidates = {}
|
|
|
|
for key, value in pairs(env) do
|
|
if (value == row[3]) and (type(key) == "string") and key:match("^%u[%u%d]+$") then
|
|
candidates[#candidates + 1] = key
|
|
end
|
|
end
|
|
table.sort(candidates)
|
|
use("framework", "judge.unnamed", "kind " .. row[3] .. ((#candidates > 0) and (" (" .. table.concat(candidates, " or ") .. ")") or ""))
|
|
end
|
|
end
|
|
end
|
|
elseif type(item) == "table" then
|
|
moves(item)
|
|
end
|
|
end
|
|
end
|
|
moves(levels)
|
|
if qte.swaps then
|
|
use("forge", "scene.swaps")
|
|
end
|
|
-- The screens and the named groups.
|
|
for clip in pairs(clips) do
|
|
use("forge", "screen." .. clip)
|
|
end
|
|
for still in pairs(stills) do
|
|
use("forge", "still." .. still)
|
|
end
|
|
for group, fields in pairs(groups) do
|
|
for field in pairs(fields) do
|
|
use("forge", group .. "." .. field)
|
|
end
|
|
end
|
|
-- Persistence.
|
|
if qte.boards or qte.highScores then
|
|
use("forge", "persist.boards")
|
|
end
|
|
if qte.saves then
|
|
use("forge", "persist.saves")
|
|
end
|
|
if qte.hsDR then
|
|
use("framework", "persist.dieAndRetryRecords")
|
|
end
|
|
-- What is still the framework's: its settings, the globals nothing names, its style, and the
|
|
-- per-title tweaks and tables of the map-mode lineage.
|
|
for dip in pairs(dips or {}) do
|
|
use("framework", "setting." .. dip)
|
|
end
|
|
for key in pairs(qte.settings or {}) do
|
|
use("framework", "global." .. key)
|
|
end
|
|
for option in pairs(qte.options or {}) do
|
|
use((option == "style") and "framework" or "forge", "option." .. option, (option == "style") and qte.options.style or nil)
|
|
end
|
|
for tweak in pairs(qte.tweaks or {}) do
|
|
use("framework", "tweak." .. tweak)
|
|
end
|
|
if qte.trophyOrder then
|
|
use("framework", "order.trophies")
|
|
end
|
|
if qte.percents then
|
|
use("framework", "scoring.percents")
|
|
end
|
|
if qte.flows then
|
|
use("framework", "flows.handWritten", main:gsub("%.singe$", "") .. ".levels.lua")
|
|
end
|
|
-- What the game's own Lua still does.
|
|
if alg or videoland or timetraveler then
|
|
use("lua", "vocabulary", "Vocabulary.singe")
|
|
elseif rdg then
|
|
use("lua", "levelFunctions", main)
|
|
end
|
|
-- swapScene is run here, over every level, scene, and difficulty, into scene.swaps (see swaps).
|
|
for _, hook in ipairs(hooks) do
|
|
if hook.empty then
|
|
use("framework", "hook." .. hook.name, "empty")
|
|
elseif hook.name == "swapScene" then
|
|
use("forge", "scene.swaps", "read from swapScene")
|
|
else
|
|
use("lua", "hook." .. hook.name, "Script/addons.singe")
|
|
end
|
|
end
|
|
if qte.script and not (alg or videoland or timetraveler) then
|
|
use("lua", "script", qte.script)
|
|
end
|
|
table.sort(lines)
|
|
local handle = assert(io.open(path, "w"))
|
|
|
|
handle:write(table.concat(lines, "\n") .. "\n")
|
|
handle:close()
|
|
end
|
|
|
|
|
|
if os.getenv("FORGE_LEDGER") then
|
|
ledger(os.getenv("FORGE_LEDGER"))
|
|
return
|
|
end
|
|
|
|
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 original script by the port converter:")
|
|
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) .. ",")
|
|
putCredits()
|
|
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 "qte"
|
|
|
|
put("-- " .. (rawget(env, "Title") or name) .. ", ported to Forge from its original script by the port converter:")
|
|
put("-- the game's tables as data for the " .. loop .. " behaviour, which plays the game's loop.")
|
|
put("return {")
|
|
put("\ttitle = " .. lua(name) .. ",")
|
|
putCredits()
|
|
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 = {")
|
|
-- The qte behaviour's games draw their HUD with the qteHud look (FORGE.md 14.15), from the game's
|
|
-- own pictures and font; the other loops' games draw none of it yet.
|
|
put("\t\tgame = { look = { kind = \"" .. ((loop == "qte") and "qteHud" or "none") .. "\" }, behaviours = { { kind = \"" .. loop .. "\" } } }")
|
|
-- The HUD's second font (the options' Font row other than the game's own) is the framework's
|
|
-- Media/Font/default.ttf, which the port cannot reach: it travels as the title's Fonts/hudDefault.ttf.
|
|
if loop == "qte" then
|
|
local font, found = frameworkFile:gsub("/Script/globals%.singe$", "/Media/Font/default.ttf")
|
|
local from = (found == 1) and io.open(font, "rb") or nil
|
|
local into = env.MYDIR .. "/Fonts/hudDefault.ttf"
|
|
|
|
if from and not io.open(into, "rb") then
|
|
local bytes = from:read("a")
|
|
local handle = io.open(into, "wb") or error("cannot write " .. into)
|
|
|
|
handle:write(bytes)
|
|
handle:close()
|
|
end
|
|
if from then
|
|
from:close()
|
|
end
|
|
end
|
|
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")
|