forge/util/forgeCredits.lua
2026-10-06 16:57:52 -05:00

191 lines
5.4 KiB
Lua

-- The original author's credits for a Forge port of an existing game: the fields of the game's own
-- games.dat entry (developer, publisher, year, platform, genre, creator, source, description), the
-- "KEY: value" lines of its main script's header comment as notes, and the licence files beside
-- the script, which a release carries. Nothing is invented: a field the original does not state
-- is left out.
--
-- lua5.4 util/forgeCredits.lua <description.forge> <game directory> <main script>
--
-- writes or replaces the description's credits block, just after its title, and touches nothing
-- else in it. util/forgePortKaris.lua loads this file for creditsFor, so a converted description
-- carries its credits from the start.
local CREDIT_KEYS = {
{ field = "developer", key = "DEVELOPER" },
{ field = "publisher", key = "PUBLISHER" },
{ field = "year", key = "YEAR" },
{ field = "platform", key = "PLATFORM" },
{ field = "genre", key = "GENRE" },
{ field = "creator", key = "CREATOR" },
{ field = "source", key = "SOURCE" },
{ field = "description", key = "DESCRIPTION" }
}
local LICENCE_PATTERNS = { "^copying", "^licen[cs]e", "^gpl" }
local GAMES_DAT_DEPTH = 2 -- How many folders above the script games.dat may be.
local function readFile(path)
local handle = io.open(path, "rb")
local text = nil
if handle then
text = handle:read("a")
handle:close()
end
return text
end
-- The games.dat entry for the script: beside it or a folder or two above, the entry whose SCRIPT
-- names this script, or the only entry there is.
local function gamesEntry(dir, main)
local folder = dir
for _ = 0, GAMES_DAT_DEPTH do
local text = readFile(folder .. "/games.dat")
if text then
local sandbox = {}
local chunk = load(text, "games.dat", "t", sandbox)
if chunk and pcall(chunk) and (type(sandbox.GAMES) == "table") then
for _, entry in ipairs(sandbox.GAMES) do
local script = tostring(entry.SCRIPT or "")
if script:lower():sub(-#main) == main:lower() then
return entry
end
end
if #sandbox.GAMES == 1 then
return sandbox.GAMES[1]
end
end
end
folder = folder:match("^(.*)[/\\][^/\\]+$") or folder
end
return nil
end
-- The header comment's "KEY: value" lines, as the author wrote them: program name, author, who
-- converted it and for what.
local function headerNotes(dir, main)
local text = readFile(dir .. "/" .. main) or ""
local block = text:match("%-%-%[=*%[(.-)%]=*%]")
local notes = {}
for line in (block or ""):gmatch("[^\r\n]+") do
local key, value = line:match("^%s*([A-Z][A-Z0-9 /%-]*[A-Z0-9])%s*:%s*(.-)%s*$")
if key and (value ~= "") then
notes[#notes + 1] = key:sub(1, 1) .. key:sub(2):lower() .. ": " .. value
end
end
return notes
end
-- The licence files beside the script, by name.
local function licences(dir)
local found = {}
local handle = io.popen('ls -1 "' .. dir .. '" 2>/dev/null')
if handle then
for name in handle:lines() do
for _, pattern in ipairs(LICENCE_PATTERNS) do
if name:lower():match(pattern) then
found[#found + 1] = name
break
end
end
end
handle:close()
end
table.sort(found)
return found
end
-- The credits block as description source, one line, or nil when there is nothing to credit.
function creditsFor(dir, main)
local entry = gamesEntry(dir, main) or {}
local parts = {}
local notes = headerNotes(dir, main)
local files = licences(dir)
for _, credit in ipairs(CREDIT_KEYS) do
local value = entry[credit.key]
if type(value) == "number" then
parts[#parts + 1] = string.format("%s = %d", credit.field, value)
elseif (type(value) == "string") and (value ~= "") then
parts[#parts + 1] = string.format("%s = %q", credit.field, value)
end
end
if #notes > 0 then
local quoted = {}
for _, note in ipairs(notes) do
quoted[#quoted + 1] = string.format("%q", note)
end
parts[#parts + 1] = "notes = { " .. table.concat(quoted, ", ") .. " }"
end
if #files > 0 then
local quoted = {}
for _, name in ipairs(files) do
quoted[#quoted + 1] = string.format("%q", name)
end
parts[#parts + 1] = "licenses = { " .. table.concat(quoted, ", ") .. " }"
end
if #parts == 0 then
return nil
end
return "\tcredits = { " .. table.concat(parts, ", ") .. " },"
end
-- The description with its credits line put just after the title, replacing one already there.
local function withCredits(text, line)
local out = {}
local after = false
for row in (text .. "\n"):gmatch("(.-)\n") do
if not row:match("^\tcredits = {") then
out[#out + 1] = row
if (not after) and line and row:match("^\ttitle%s*=") then
out[#out + 1] = line
after = true
end
end
end
while (#out > 0) and (out[#out] == "") do
out[#out] = nil
end
return table.concat(out, "\n") .. "\n", after
end
if arg and arg[0] and arg[0]:match("forgeCredits%.lua$") then
local description = arg[1] or error("a description")
local dir = arg[2] or error("the game's directory")
local main = arg[3] or error("its main script")
local text = readFile(description) or error("cannot read " .. description)
local line = creditsFor(dir, main)
local result, put = withCredits(text, line)
if line and not put then
error("no title line in " .. description)
end
local handle = io.open(description, "wb")
handle:write(result)
handle:close()
print(description .. ": " .. (line and line:gsub("^\t", "") or "nothing to credit"))
end