Compare commits

..

7 Commits

Author SHA1 Message Date
bryanthaboi f3165ceb71 Merge pull request #361 from bryanthaboi/dev
tiled stuff
2026-07-28 20:58:16 -04:00
bryanthaboi d6e36d457f Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev 2026-07-28 20:32:30 -04:00
bryanthaboi 52e3a0d23f update for tiled stuff 2026-07-28 20:32:29 -04:00
bryanthaboi f1950348d4 Merge pull request #344 from bryanthaboi/dev
bazinga linux
2026-07-28 15:39:59 -04:00
bryanthaboi 5603477787 Merge pull request #343 from GFlorio/appimage-fixes
Fix ROM picker + Auto Updater in the Linux Appimage release
2026-07-28 15:36:54 -04:00
Gabriel Florio 6a9e77cde0 Fix app crashing on self-restart in the AppImage release 2026-07-28 16:02:20 -03:00
Gabriel Florio 4943d3e5f4 Unset LD_LIBRARY_PATH when calling system tools from AppImage 2026-07-28 14:42:45 -03:00
12 changed files with 1596 additions and 10 deletions
+4
View File
@@ -35,3 +35,7 @@ mobile/ios/build/
# Legacy manual convenience-copy location (superseded by /dist/android/)
mobile/dist/
# Tiled map-editing workspace (tools/tiled_export.py). Derived from the imported
# ROM cache exactly like data/generated/, so it is never committable.
/build/tiled/
+5
View File
@@ -150,6 +150,11 @@ reference — lives on the
Shipped example mods, one per kind of author, live in `[mods/](mods/)`.
Maps can be edited in our own build of [Tiled](https://www.mapeditor.org),
[bryanthaboi/tiled_gen1recomp](https://github.com/bryanthaboi/tiled_gen1recomp/releases),
and exported back out as a mod; see
[docs/tiled-map-editing.md](docs/tiled-map-editing.md).
## Bugs and Ideas
Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math
+17
View File
@@ -18,6 +18,23 @@ Regenerate the reference straight into a wiki checkout:
luajit tools/gen_registry_docs.lua ../gen1recomp.wiki
```
## Editing maps in Tiled
Maps are data, not assets, so they can be authored in a real map editor and
exported as a mod. `tools/tiled_export.py` builds a
[Tiled](https://www.mapeditor.org) workspace out of the imported ROM cache:
```sh
python3 tools/tiled_export.py # -> build/tiled/ (gitignored)
```
Open `build/tiled/gen1.tiled-project`, edit any of the 222 maps (or
`kanto.world` for the stitched overworld), and export with the
`gen1-mod-export` extension — one map file, or a whole loadable mod folder.
An edited vanilla map becomes a `mod.content.maps:patch` carrying only the
fields that moved; a new map becomes a `:register`. See
`docs/new-features.md` and the extension's own README.
## Rendering pipelines
Most registries hand the engine *content*. `render_pipelines` hands it
+6
View File
@@ -338,3 +338,9 @@ second, relabelling itself to `Confirm?` in between.
A validation pill in the tab rail mirrors what the running game would
quarantine on load; clicking it jumps to the tab holding the first problem.
## Tiled map editing (mod authoring)
`tools/tiled_export.py` turns the imported ROM cache into a Tiled workspace,
so maps can be edited in a real map editor and exported back out as a mod.
It has its own document: docs/tiled-map-editing.md.
+65
View File
@@ -0,0 +1,65 @@
# Tiled map editing (mod authoring)
`tools/tiled_export.py` turns the imported ROM cache into a
[Tiled](https://www.mapeditor.org) workspace, so maps can be edited in a
real map editor and exported back out as a mod. The original had no map
editor at all; the port's own map data is plain Lua, which is what makes
this a data path rather than an asset path.
Editing is done in our own Tiled build,
[bryanthaboi/tiled_gen1recomp](https://github.com/bryanthaboi/tiled_gen1recomp/releases),
which ships the `gen1-mod-export` extension the workspace relies on. Grab it
from that repo's releases; upstream Tiled opens the workspace but cannot
export a mod out of it.
```sh
python3 tools/tiled_export.py # -> build/tiled/ (gitignored)
```
Then open `build/tiled/gen1.tiled-project` in that build of Tiled.
- **The overworld is one surface.** All 222 maps become `maps/*.tmj`, and
`kanto.world` places the 36 connected overworld maps at their real
connection offsets. That world is pre-loaded (seeded into the workspace's
Tiled session), so opening any one overworld map draws its neighbors around
it and you scroll and edit straight across the seams. Everything else is a
double-click away in Tiled's project panel.
- **Extending Kanto wires both ends.** A connection lives on both maps, so
hooking a new map onto a base map also emits the return connection as a
patch on that base map, keeping its other directions intact. The return
offset is derived, not guessed: all 78 vanilla reciprocal pairs satisfy
`back.offset == -offset`.
- **A Tiled tile is a gen1 block.** Each of the 24 tilesets becomes a Tiled
tileset whose tiles are its 32x32 blocks, composited from the 8x8 sheet,
so a tile layer *is* the map's `blocks` array. Warps, signs and objects
sit on the 16px cell grid in object layers, which is the grid the engine
addresses them on.
- **Collision is visible.** View > Show Tile Collision Shapes draws the real
walkability: a rectangle covers each cell whose feet tile is not in the
tileset's `walkable` list, which is the rule `src/world/Map.lua` applies.
- **Maps are shown in their real colors.** Each map is atlased in the SGB
palette it renders with, so Cerulean is blue and Lavender is purple in the
editor exactly as in game. Vanilla resolves that through a cascade with
interiors inheriting the last outdoor map, so the workspace mirrors the
cascade and walks the warp graph to colour interiors. Changing a map's
`palette` exports `palette = "..."` on the record, which beats the cascade,
and the editor offers the real palette names as a dropdown.
- **New blocks and new tilesets.** `blocksets/*.tmj` show a tileset's blocks
as raw 8x8 tiles, four by four, so new blocks can be composed there;
per-tile flags on `tilesets/tiles_*.tsj` become `walkable`, `waterTiles`,
`doorTiles` and the rest.
- **Export is a diff, not a fork of the data.** The `gen1-mod-export`
extension (shipped in `tiled_gen1recomp`) writes either one map file or a whole
loadable mod folder. An edited vanilla map diffs against the imported data
and emits `mod.content.maps:patch` carrying *only* the fields that moved, so
a mod covers the parts it changes and leaves the rest to the base game; a
new map gets `:register` at an index of 1000 or above. An unchanged map
exports nothing at all. Exports pass `tools/modkit.py validate` and `lint`.
- **Or the whole record, on request.** Ticking `exactExport` on a map switches
it to `mod.content.maps:override`, pinning the map to exactly what the
editor shows. It is off by default because an override wins outright over
any other mod patching that map, where a patch composes.
No ROM-derived art travels into an exported mod: a tileset still drawing on
the player's own imported sheet references that path rather than shipping the
pixels, and only a sheet the author supplied is copied in.
+1 -3
View File
@@ -400,9 +400,7 @@ end
-- LÖVE process ensures scripts, registries, and assets are all rebuilt from
-- the newly selected mod state.
function Game:restartWithMods()
if love.event and love.event.quit then
love.event.quit("restart")
end
require("src.core.HostShell").restart()
end
function Game:keyreleased(key)
+51
View File
@@ -0,0 +1,51 @@
-- Helpers for calling host tools (curl, zenity/kdialog, ...).
local HostShell = {}
-- Our AppRun exports LD_LIBRARY_PATH="$APPDIR/lib:..." so every subprocess we
-- spawn tries to link against the libraries we're shipping instead of the
-- system ones. We want to unset the var so that any system tools can find
-- their proper libraries. Only needed when running in an AppImage.
function HostShell.envPrefix()
if os.getenv("APPIMAGE") then
return "env -u LD_LIBRARY_PATH "
end
return ""
end
-- Wraps io.popen with the AppImage env fix applied and lua errors swallowed
function HostShell.popen(command, mode)
local ok, pipe = pcall(io.popen, HostShell.envPrefix() .. command, mode or "r")
if not ok or not pipe then return nil end
return pipe
end
-- Restart the whole app. The obvious love.event.quit("restart") re-runs LÖVE's
-- boot in-process, which calls love.filesystem.init a second time -- and inside
-- an AppImage physfs is already initialized, so that second init throws
-- ("Failed to initialize filesystem: already initialized") and the relaunch
-- crashes. So on an AppImage we relaunch the executable; the fresh process's
-- Boot step mounts any downloaded update exactly as a manual relaunch would.
-- On every other platform the in-process restart works, so keep it.
function HostShell.restart()
if not (love and love.event and love.event.quit) then return end
local appimage = os.getenv("APPIMAGE")
if not appimage then
love.event.quit("restart")
return
end
-- We have to restart the process with this cursed execv call to prevent the
-- PID from changing, which might cause SteamOS and other Linux launchers to
-- think the app has crashed.
local ffi = require("ffi")
pcall(ffi.cdef, [[
int execv(const char *path, char *const argv[]);
int unsetenv(const char *name);
]])
ffi.C.unsetenv("LD_LIBRARY_PATH")
local argv = ffi.new("const char *[2]", appimage, nil)
ffi.C.execv(appimage, ffi.cast("char *const *", argv))
end
return HostShell
+3 -2
View File
@@ -1,5 +1,6 @@
local GameVersion = require("src.core.GameVersion")
local Strings = require("src.core.Strings")
local HostShell = require("src.core.HostShell")
local RomImporter = {}
RomImporter.__index = RomImporter
@@ -285,7 +286,7 @@ end
local function commandOutput(command)
releasePointerGrab()
local pipe = io.popen(command, "r")
local pipe = HostShell.popen(command)
if not pipe then return nil end
local result = pipe:read("*a")
pipe:close()
@@ -1859,7 +1860,7 @@ function RomImporter:mousepressed(x, y, button)
if action == "download" and self.Check then
pcall(self.Check.download)
elseif action == "restart" then
love.event.quit("restart")
HostShell.restart()
elseif action == "openurl" and self.Check then
love.system.openURL(self.Check.releaseUrl())
end
+6
View File
@@ -493,6 +493,12 @@ R.maps = {
width = f.int(1), height = f.int(1),
blocks = f.list(f.int(0, 255)),
borderBlock = f.opt(f.int(0, 255)),
-- A named SGB palette, which wins over the field.palettes cascade
-- (OverworldController.lua:506 reads map.def.palette first). Deliberately
-- a plain string rather than f.id("palettes"): the ROM-free fixture base
-- carries no palettes at all, so an id reference would fail validation for
-- a perfectly good mod wherever there is no imported dataset.
palette = f.opt(f.str),
warps = f.opt(f.list(f.rec{ x = f.int(0), y = f.int(0),
destMap = f.str, destWarp = f.int(0) })),
objects = f.opt(f.list(f.any)),
+6 -5
View File
@@ -35,6 +35,7 @@ local Json = loadModule("src/link/Json.lua")
local Check = loadModule("src/update/Check.lua")
local Version = loadModule("src/core/Version.lua")
local Semver = loadModule("src/update/Semver.lua")
local HostShell = loadModule("src/core/HostShell.lua")
-- Boot's top-level require("src.update.Semver") cannot resolve in this thread
-- (no src.* searcher), which would leave Boot nil and the minShell gate
-- permanently permissive. Seed the loaded table first so it resolves.
@@ -76,8 +77,8 @@ local function curlCapture(url)
.. "-H " .. shq("User-Agent: gen1recomp-updater") .. " "
.. "-H " .. shq("Accept: application/vnd.github+json") .. " "
.. shq(url)
local ok, pipe = pcall(io.popen, cmd)
if not ok or not pipe then return nil end
local pipe = HostShell.popen(cmd)
if not pipe then return nil end
local out = pipe:read("*a")
pipe:close()
if not out or out == "" then return nil end
@@ -85,8 +86,8 @@ local function curlCapture(url)
end
local function haveCurl()
local ok, pipe = pcall(io.popen, "curl --version")
if not ok or not pipe then return false end
local pipe = HostShell.popen("curl --version")
if not pipe then return false end
local out = pipe:read("*a")
pipe:close()
return out ~= nil and out:find("curl", 1, true) ~= nil
@@ -239,7 +240,7 @@ local function launchDownload(url, partAbs, doneAbs)
os.execute('start "" /b ' .. shq(saveDir .. "/" .. batRel))
else
-- ( ... ) & backgrounds the whole group so os.execute returns at once
os.execute("( curl -fsSL --connect-timeout 15 --max-time 900 -o "
os.execute("( " .. HostShell.envPrefix() .. "curl -fsSL --connect-timeout 15 --max-time 900 -o "
.. shq(partAbs) .. " " .. shq(url)
.. " ; touch " .. shq(doneAbs) .. " ) >/dev/null 2>&1 &")
end
+102
View File
@@ -0,0 +1,102 @@
-- Dump one generated data table as JSON.
--
-- luajit tools/lua_to_json.lua data/generated/maps.lua
--
-- The generated tables are plain data (the extractor serializes them with no
-- functions or cycles), so a straight recursive encode is enough. This exists
-- so Python tooling -- tools/tiled_export.py -- can read the same records the
-- engine reads without carrying a Lua parser of its own, which is the usual
-- way those two drift apart.
local path = ...
if not path then
io.stderr:write("usage: luajit tools/lua_to_json.lua <data/generated/x.lua>\n")
os.exit(2)
end
local chunk, err = loadfile(path)
if not chunk then
io.stderr:write("cannot load " .. path .. ": " .. tostring(err) .. "\n")
os.exit(1)
end
local root = chunk()
local ESCAPES = {
['"'] = '\\"', ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t",
}
local function encodeString(s)
return '"' .. s:gsub('[%c"\\]', function(c)
return ESCAPES[c] or string.format("\\u%04x", c:byte())
end) .. '"'
end
local function encodeNumber(n)
if n ~= n or n == math.huge or n == -math.huge then
error("non-finite number in " .. path)
end
-- integers must not pick up a ".0": tile and block ids are compared as
-- integers on the Python side and in the exported Lua
if n % 1 == 0 then return string.format("%d", n) end
return string.format("%.17g", n)
end
-- A table is an array when its keys are exactly 1..#t. Empty tables encode as
-- [] and the reader coerces; nothing in the generated data distinguishes an
-- empty list from an empty map.
local function isArray(t)
local count = 0
for k in pairs(t) do
if type(k) ~= "number" or k % 1 ~= 0 or k < 1 then return false end
count = count + 1
end
return count == #t
end
local out = {}
local function emit(s) out[#out + 1] = s end
local function encode(value)
local kind = type(value)
if value == nil then
emit("null")
elseif kind == "boolean" then
emit(value and "true" or "false")
elseif kind == "number" then
emit(encodeNumber(value))
elseif kind == "string" then
emit(encodeString(value))
elseif kind == "table" then
if isArray(value) then
emit("[")
for i = 1, #value do
if i > 1 then emit(",") end
encode(value[i])
end
emit("]")
else
-- sort keys so a regenerated dump is byte-stable and diffable
local keys = {}
for k in pairs(value) do keys[#keys + 1] = k end
table.sort(keys, function(a, b)
if type(a) == type(b) then return a < b end
return type(a) == "number"
end)
emit("{")
for i, k in ipairs(keys) do
if i > 1 then emit(",") end
emit(encodeString(tostring(k)))
emit(":")
encode(value[k])
end
emit("}")
end
else
error("cannot encode a " .. kind .. " in " .. path)
end
end
encode(root)
io.write(table.concat(out))
io.write("\n")
File diff suppressed because it is too large Load Diff