- Rust 95%
- Lua 4%
- Shell 1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
CI / check (push) Successful in 48s
- tasks.json is written through a temp file in the same directory (write_all + fsync + rename, MoveFileExW on Windows): a crash or a full disk mid-save can no longer leave a truncated config - include_filter/exclude_filter are compiled at catalog load - a malformed glob fails fast naming the field - webp_quality must stay within 0-100 (packit's range) - pass2 requires per-psd grouping - resolved output paths are checked for overlaps among the PSD files each task currently finds: two tasks writing into the same GameData folder fail the load instead of silently overwriting each other's results - cloning a task suffixes dest and every dest_map entry with the clone's name, so a fresh clone never writes into the original's outputs - version -> 2026.11.2 |
||
| .cargo | ||
| .forgejo/workflows | ||
| scripts/psd_convert | ||
| src | ||
| .gitignore | ||
| AGENTS.md | ||
| build-windows.sh | ||
| Cargo.lock | ||
| Cargo.toml | ||
| make-dist.sh | ||
| README.md | ||
psd_convert (Rust)
Converts the game's PSD sources into ready-to-use game resources: each
.psd file from the res/ tree is rendered into webp sprites by
imaginum and packed into a GameData resource bundle by packit.
An interactive DOS-style console menu selects what to convert; the
command-line mode runs tasks directly (scriptable).
Layout
Deployed into the game res/ tree (the make-dist.sh output copies 1:1
into an existing res/ folder):
res/
├── psd_convert # the app, Linux (target/release)
├── psd_convert.exe # the app, Windows
└── scripts/psd_convert/
├── tasks.json # task catalog + folder settings (edit this)
├── README.md # this file
├── bin/ # imaginum, packit (+ .exe for Windows)
└── profiles/ # marlo_engine.lua (imaginum layer conventions)
Launch the app from the game root (rf/) or from res/ itself - both work:
tasks.json is found in scripts/psd_convert/ beside the executable,
res_root resolves from the launch directory, out_root from the
executable's folder.
Paths (tasks.json)
{
"res_root": "res", // psd source tree, from the launch dir
"out_root": "_output", // output folder, beside the executable
"bin_dir": "bin", // tools, relative to tasks.json
"profile": "profiles/marlo_engine.lua",
"tasks": [ ... ]
}
res_rootis resolved relative to the directory you launch the app from;out_rootis resolved relative to the executable's folder (absolute paths work for both). If the psd tree is not in the launch dir, the app walks up from the executable's location (so running<project>/target/release/psd_convertstill finds<project>/res). If neither location has the tree, the menu shows a warning line and empty counts. Results land in<out_root>/GameData/<dest>mirroring the real GameData 1:1; copy them intoAssets/GameDataby hand.out_rootis wiped on every launch.tasks.jsonis looked up in./tasks.json,./scripts/psd_convert/tasks.json, then the same pair next to the binary (walking up its parent directories);--config PATHoverrides.IMAGINUM_BIN/PACKIT_BINenv vars override the tool paths.
Task fields: name, folder (default = full path), grouping
(0 = per-psd, every psd its own resource; 1 = one-scene, the whole
folder as one resource), include_filter, exclude_filter, dest (path
in GameData, {psd} = psd stem), dest_map (per-psd dest overrides).
The task catalog
tasks is a flat list of tasks, kept sorted alphabetically by name. A task
name must be unique and must not contain /; folder defaults to the name and holds the psd
files relative to res_root. grouping: 0 = per-psd (every psd its own
resource), 1 = one-scene (the whole folder as one resource). psd files
whose name starts with ___ always sort last (old versions; the first psd
of a one-scene task names the output folder).
{ "name": "loading_screens", "folder": "ui",
"grouping": 0, "include_filter": "*.psd", "exclude_filter": "spinner*.psd,tutorial.psd",
"dest": "ui/{psd}",
"dest_map": { "loading_screen_halloween": "holidays/halloween/ui/{psd}" },
"res_type": "uisprite",
"webp_1x": 1, "webp_2x": 1,
"etc1_1x": 1, "etc1_2x": 0, "etc2_1x": 1, "etc2_2x": 0,
"webp_quality": 75, "webp_sharp_yuv": 0,
"scale_filter": "bicubic", "mode": "layer",
"layouts": 0,
"pass2": null }
- All fields serialize explicitly; booleans are numbers (
1= true,0= false). - The pipeline knobs map to imaginum/packit options; everything the format
does not expose always runs on the tool default:
res_type—uisprite|sprite|scenesprite- the output formats are six 0/1 switches, one per encoder and source
set:
webp_1x/webp_2x,etc1_1x/etc1_2x,etc2_1x/etc2_2x. Both sets on packs that format from the 1x and the 2x psd sets (packit's bare format name), a single set pins@1x/@2x. At least one switch must be on; when none of the enabled formats uses the 2x set, the 2x extraction pass is skipped entirely (default:webpboth sets,etc1/etc2@1xonly) webp_quality(75; valid range0–100),webp_sharp_yuv(0)scale_filter—bicubic|bilinear;mode—layer|filelayouts(0) — generate.layoutfiles next to the textures; the catalog ships it on only for the*_iconsgroups
exclude_filtertakes comma-separated glob patterns (files handled by dedicated tasks of the same folder). Include and exclude patterns are checked when the catalog loads.- The catalog checks resolved output directories for overlaps among the PSD files currently found by each task. Add new source PSDs and reload the catalog to validate their destinations too.
- A non-null
pass2(the same knobs) switches the task to the 2-pass decors pipeline: pass 1 renders loose sprites from the task settings, pass 2 renders an etc1/etc2 atlas from the pass2 settings and both land in one folder.
Usage
Run from the game project root (where res/ lives):
psd_convert # interactive menu
psd_convert --list # list tasks with their psd counts
psd_convert --all # run everything
psd_convert <task>... # by full name or unique basename (beats, decors…)
psd_convert <task> <psd|mask>... # narrow one per-psd task
Menu keys: ↑↓/j/k move, PgUp/PgDn page, Home/End first/last of the group,
←→ collapse/expand (→ into a task shows its psd files; whole-folder tasks
open a view-only list without checkboxes), psd files modified within the
last 3 days glow light green, space mark a task or a single psd,
a mark/clear all, enter run,
e edit the task under the cursor, n new task, c clone it,
del delete, g global paths, s save, q/esc quit. Selected psd rows and
their group glow white; unselected psd
files modified within the last 3 days show light green. Mouse wheel scrolls
3 rows. Enter tears the menu down, streams
the conversion into the terminal, then one keypress brings the menu back
with the run summary in the message slot. Nothing is persisted between
launches: every start shows an empty selection for you to pick anew.
Editing tasks.json in place
The menu itself is the editor: there is no separate mode to learn. e
opens a full-screen form for the task under the cursor with every field
visible at once, n creates a
task from the template (and opens its form on the name line), c clones
the task under the cursor with a numeric suffix (decors → decors1 →
decors2…), del deletes it (confirmed; the last task is kept), g edits
the global paths (res_root, out_root, bin_dir, profile), s writes
tasks.json. esc returns to the menu from the form.
- Form: one field per line, all of them visible on the full-screen form
(taller lists scroll on small terminals); ↑↓/PgUp/PgDn pick a field,
enter — edit the value in the input
line (←→/Home/End move the block caret, backspace erases before it, del
erases at it, typing inserts; enter accepts, esc cancels), space cycles
the choice fields (
groupingper-psd ↔ one-scene,res_typeuisprite → sprite → scenesprite,scale_filterbicubic ↔ bilinear,modelayer ↔ file). The boolean fields (the six output-format switcheswebp_1x…etc2_2x,webp_sharp_yuv,layouts,pass2) show the same ○/● circles as the psd selection — space flips them. All 20 fields are editable;dest_mapis a comma-separatedstem=destlist. An empty value meansnullfor the optional fields. pass2: space switches the second pass on (it starts as a copy of the task's own knobs) and off, enter opens the pass2 sub-form (fieldpass2removes it, the rest edits its own copy of the knobs), esc returns to the task form.- Every applied edit is validated with the exact load-time rule set, so a
bad
dest/groupingcombination or an unknown format is caught at once: the menu list retargets in place while the config still builds, and the form header shows [INVALID] when it does not. The [MODIFIED] flag means the working copy actually differs from the saved file — toggling a switch back (or creating and deleting a task) leaves the catalog unmodified. A save is refused while the config is invalid, and enter refuses to run while edits are unsaved (the run always uses the saved catalog) — presssfirst. - Save writes the file pretty-printed (2-space indent, explicit nulls,
fields in the same order as shipped);
dest_mapentries are written sorted. Unknown JSON keys are dropped by the round-trip. Saving also reloads the running menu catalog — new tasks appear without a restart, and existing selections survive for the task names the edit kept (tasks without any psd files stay hidden in the menu, as before). q/esc with unsaved edits asks for a confirmation before quitting.
Building
cargo build --release # Linux: target/release/psd_convert
./build-windows.sh # Windows: via cargo-xwin (see below)
Windows cross-build from Linux uses cargo-xwin (MSVC target, no MSVC installation needed):
cargo install cargo-xwin # once; clang-cl must be on PATH
./build-windows.sh
# -> target/x86_64-pc-windows-msvc/release/psd_convert.exe
The first run downloads the Windows SDK/CRT through xwin (cached in
~/.cache/xwin afterwards). The app itself is pure Rust - no C toolchain
flags needed; the bin/ folder already carries both Linux and Windows
builds of the imaginum/packit tools.
Release folder
./make-dist.sh
Recreates dist/ with everything needed to run: the app for both platforms
(psd_convert, psd_convert.exe) at the top level, and scripts/psd_convert/
holding the task catalog (tasks.json), this readme, the tools (bin/) and
the profile (profiles/). Copy the folder contents next to the game res/
tree and launch the app from there - all config paths in tasks.json are
relative to its own folder.