No description
  • Rust 95%
  • Lua 4%
  • Shell 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
cyberex 4f38a7b64d
All checks were successful
CI / check (push) Successful in 48s
atomic tasks.json saves + stricter catalog validation; clones get distinct dest paths
- 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
2026-11-04 07:02:55 +03:00
.cargo Statically link the MSVC CRT: the exe runs without vcruntime140.dll 2026-10-23 16:12:07 +03:00
.forgejo/workflows Forgejo CI: ci.yml runs clippy -D warnings + cargo test on master/PR, release.yml builds dist/ via make-dist.sh on v* tags (windows stays msvc crt-static through cargo-xwin, xwin SDK cached); fixed 5 pre-existing clippy warnings so the gate passes 2026-10-24 14:32:24 +03:00
scripts/psd_convert tasks.json goes flat, editing moves into the menu: E/N/C/Del edit tasks in a full-screen form, the F2 editor screen is removed 2026-11-01 13:50:00 +03:00
src atomic tasks.json saves + stricter catalog validation; clones get distinct dest paths 2026-11-04 07:02:55 +03:00
.gitignore Initial commit: Rust TUI port of the PSD convert pipeline 2026-10-23 15:40:40 +03:00
AGENTS.md Initial commit: Rust TUI port of the PSD convert pipeline 2026-10-23 15:40:40 +03:00
build-windows.sh Initial commit: Rust TUI port of the PSD convert pipeline 2026-10-23 15:40:40 +03:00
Cargo.lock atomic tasks.json saves + stricter catalog validation; clones get distinct dest paths 2026-11-04 07:02:55 +03:00
Cargo.toml atomic tasks.json saves + stricter catalog validation; clones get distinct dest paths 2026-11-04 07:02:55 +03:00
make-dist.sh tasks.json and README.md move under scripts/psd_convert/: config discovery now checks <dir>/tasks.json and <dir>/scripts/psd_convert/tasks.json in the launch dir and up the exe chain; bin_dir/profile retarget to the config's own folder (bin, profiles/); dist/ ships the res root with just the executables 2026-10-24 14:23:12 +03:00
README.md atomic tasks.json saves + stricter catalog validation; clones get distinct dest paths 2026-11-04 07:02:55 +03:00

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_root is resolved relative to the directory you launch the app from; out_root is 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_convert still 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 into Assets/GameData by hand. out_root is wiped on every launch.
  • tasks.json is looked up in ./tasks.json, ./scripts/psd_convert/tasks.json, then the same pair next to the binary (walking up its parent directories); --config PATH overrides.
  • IMAGINUM_BIN / PACKIT_BIN env 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: webp both sets, etc1/etc2 @1x only)
    • webp_quality (75; valid range 0–100), webp_sharp_yuv (0)
    • scale_filter — bicubic | bilinear; mode — layer | file
    • layouts (0) — generate .layout files next to the textures; the catalog ships it on only for the *_icons groups
  • exclude_filter takes 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 (grouping per-psd ↔ one-scene, res_type uisprite → sprite → scenesprite, scale_filter bicubic ↔ bilinear, mode layer ↔ file). The boolean fields (the six output-format switches webp_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_map is a comma-separated stem=dest list. An empty value means null for 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 (field pass2 removes 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/grouping combination 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) — press s first.
  • Save writes the file pretty-printed (2-space indent, explicit nulls, fields in the same order as shipped); dest_map entries 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.