big ass modding update

This commit is contained in:
bryanthaboi
2026-07-19 16:18:18 -04:00
parent b5a673b252
commit 47923d95b3
258 changed files with 31048 additions and 2310 deletions
@@ -0,0 +1,12 @@
# Changelog
Format: [keep a changelog](https://keepachangelog.com/en/1.1.0/).
Version headings match `manifest.json`'s `version`.
## 1.0.0
### Added
- `transforms.lua` recolor of the player overworld sheets.
- `EXAMPLE_SHINY` palette record and a `PALLET` town palette override.
- `trueColor` opt-out patches for `SPRITE_RED` and `SPRITE_RED_BIKE`.
@@ -0,0 +1,78 @@
# Shiny Palette Example
Recolors the player's overworld sheets to teal and repaints Pallet Town —
and ships no ROM-derived pixels to do it.
**Persona: the Artist.** This is the canonical answer to "how do I ship a
recolor legally": you ship the *transform*, not the image.
## Try it
```sh
python3 tools/modkit.py validate mods/examples/example_shiny_palette --base imported
python3 tools/modkit.py lint mods/examples/example_shiny_palette
luajit mods/examples/example_shiny_palette/tests/example_shiny_palette_test.lua
```
Enable it (`example_shiny_palette = true` under `mods` in `options.lua`, or
the F10 manager) and start the game. On first load the transform runs once
and writes `save/mod-derived/example_shiny_palette/sprites/*.png`. Delete
that directory to force a re-run.
## What it demonstrates
| Seam | Where |
|---|---|
| `assets_transforms` | `manifest.json` + `transforms.lua` — the recipe that derives art |
| `content.palettes:register` | `main.lua` — the v2 named-record palette shape |
| `content.palettes:override` | `main.lua` — the vanilla four-triple shape |
| `content.sprites:patch` | `main.lua``trueColor` opt-out, nothing else touched |
| `events:on("assets.transformed")` | `main.lua` — the empty-state warning |
## The legal pattern
`transforms.lua` runs inside a restricted context with exactly two
filesystem roots: read `assets/generated/**` (the player's own imported
cache) and write `save/mod-derived/example_shiny_palette/**`. There is no
`require`, no `love`, no `io`, no `os`. The only way data leaves the
sandbox is the `ctx` table.
Because the derived file keeps the *same relative name* as the cache file
it came from, the asset resolver finds it automatically:
```
assets/generated/sprites/red.png <- the player's import
save/mod-derived/example_shiny_palette/sprites/red.png <- this mod's recolor
```
Every consumer of the first path transparently gets the second. No
`sprites:override`, no path string in `main.lua` — and `modkit lint` can
prove the repo carries no cache-derived bytes, because it carries no bytes
at all.
The one thing that *does* need a registry entry is the 4-shade contract.
The renderer normally re-shades an overworld sheet into the current
palette's four grays, which would throw the teal away. `trueColor = true`
opts out:
```lua
mod.content.sprites:patch("SPRITE_RED", { trueColor = true })
```
`patch`, not `override`: `image`, `frames` and `walker` stay whatever the
merged view already holds, so the derived sheet keeps supplying the pixels.
## Empty state
No ROM imported yet? `ctx.exists(rel)` is false, the transform writes
nothing, the mod still loads, and `main.lua` logs a remediation line naming
the directory to delete once you have imported. It never errors.
## Original assets
`assets/accent_sparkle.png` is a 16x16 four-shade sparkle drawn for this
example. It is the only image in the directory and it is original work.
## Credits
- pret/pokered — the overworld sheet layout the transform recolors.
Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

@@ -0,0 +1,37 @@
-- Gallery #2 (Artist): a recolor that ships no pixels. transforms.lua
-- derives the sheets from the player's own cache; this file only declares
-- the palette records and the one flag the recolor needs.
return function(mod)
-- v2 record shape: a named table of four colors, lightest first
mod.content.palettes:register("EXAMPLE_SHINY", {
colors = {
{ r = 248, g = 248, b = 248 },
{ r = 120, g = 224, b = 216 },
{ r = 32, g = 128, b = 152 },
{ r = 8, g = 32, b = 64 },
},
})
-- vanilla raw shape: four {r,g,b} triples. Overriding a town palette is
-- the smallest visible artist change there is -- no assets involved.
mod.content.palettes:override("PALLET", {
{ 248, 248, 248 }, { 152, 232, 224 }, { 64, 152, 168 }, { 8, 32, 64 },
})
-- trueColor opts SPRITE_RED out of the 4-shade re-shade so the teal the
-- transform baked in survives to the screen. patch, not override: the
-- image path and frame count stay whatever the merged view already has,
-- which is how the derived sheet keeps supplying the pixels.
mod.content.sprites:patch("SPRITE_RED", { trueColor = true })
mod.content.sprites:patch("SPRITE_RED_BIKE", { trueColor = true })
mod.events:on("assets.transformed", function(ev)
if ev.modId ~= mod.id then return end
if ev.count == 0 then
mod.log:warn("no sheets derived -- import your ROM first, then "
.. "delete save/mod-derived/%s to re-run the transform", mod.id)
else
mod.log:info("derived %d recolored sheets", ev.count)
end
end)
end
@@ -0,0 +1,16 @@
{
"id": "example_shiny_palette",
"name": "Shiny Palette Example",
"version": "1.0.0",
"api": 2,
"entry": "main.lua",
"profile": "content",
"category": "GRAPHICS",
"game_version": ">=1.0.0 <2.0.0",
"priority": 100,
"assets_transforms": "transforms.lua",
"dependencies": [],
"optional_dependencies": [],
"conflicts": [],
"description": "Artist gallery entry: a recolored player sheet derived from the player's own cache, plus two palette records."
}
@@ -0,0 +1,26 @@
-- Sharing metadata (25-community-and-ecosystem.md 3.2). Read by tooling
-- and the manager detail pane; never by the loader's merge.
return {
summary = "A teal player recolor derived from your own cache, plus two palette records.",
author = "Pokemon Gen 1 Recompilation Project",
contact = "https://github.com/bryanthaboi/pokemon-gen1-recomp-project",
tags = { "cosmetic", "graphics", "beginner" },
screenshots = {
{ transform = "shots/pallet_town.lua", caption = "Pallet Town under the recolored palette" },
},
differences = {
changed = {
"SPRITE_RED and SPRITE_RED_BIKE opt into trueColor",
"the PALLET town palette is recolored",
},
added = { "EXAMPLE_SHINY palette record" },
known = {
"the derived sheets only appear after a ROM import; without a cache "
.. "the transform writes nothing and the vanilla sheets keep rendering",
},
},
credits = {
{ who = "pret/pokered", for_ = "the overworld sheet layout the transform recolors" },
},
compat = { engine = ">=1.0.0 <2.0.0", modApi = 2 },
}
@@ -0,0 +1,108 @@
-- Standalone: luajit mods/examples/example_shiny_palette/tests/example_shiny_palette_test.lua
-- Asserts the palette records merge and the transform degrades cleanly
-- when there is no imported cache to read.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local Data = require("src.core.Data")
Data:load()
-- The transform reads assets/generated/** and writes save/mod-derived/**.
-- Hiding the cache is what puts this run on the no-cache path, which is
-- the branch a mod owes the player: write nothing, load anyway. The pixel
-- path needs a real LOVE run (love.image is not in the headless stub).
local MOD = "mods/examples/example_shiny_palette"
local function noCacheFs()
local inner = T.fs.new(".")
local overlay = {}
local hidden = "assets/generated/"
local mount = "mods/example_shiny_palette"
local function map(path)
if path == mount then return MOD end
if path and path:sub(1, #mount + 1) == mount .. "/" then
return MOD .. path:sub(#mount + 1)
end
return path
end
local fs = { root = inner.root }
function fs.read(path)
if path:sub(1, #hidden) == hidden then return nil end
return overlay[path] or inner.read(map(path))
end
function fs.write(path, body) overlay[path] = body return true end
function fs.createDirectory() return true end
function fs.load(path) return inner.load(map(path)) end
function fs.getInfo(path)
if path == "mods" then return { type = "directory" } end
if path:sub(1, #hidden) == hidden then return nil end
if overlay[path] then return { type = "file" } end
return inner.getInfo(map(path))
end
function fs.getDirectoryItems(path)
if path == "mods" then return { "example_shiny_palette" } end
return inner.getDirectoryItems(map(path))
end
return fs
end
local run = T.sdk.loadMod(MOD, { data = Data, fs = noCacheFs() })
T.eq(#run.errors, 0,
"loads clean with no cache to transform (" .. tostring(run.errors[1]) .. ")")
T.eq(run.mod and run.mod.manifest.assets_transforms, "transforms.lua",
"the manifest declares its transform")
-- ------- palettes
local shiny = Data.palettes.palettes.EXAMPLE_SHINY
T.check(shiny ~= nil, "the v2 named palette record merged")
T.eq(#shiny.colors, 4, "it carries exactly four colors")
T.eq(shiny.colors[1].r, 248, "the lightest shade is first")
local pallet = Data.palettes.palettes.PALLET
T.eq(#pallet, 4, "the town palette override kept the raw four-triple shape")
T.eq(pallet[2][2], 232, "the override took")
-- ------- the trueColor opt-out, applied by patch
for _, id in ipairs({ "SPRITE_RED", "SPRITE_RED_BIKE" }) do
local sprite = Data.sprites[id]
T.eq(sprite.trueColor, true, id .. " opted into trueColor")
T.check(sprite.image ~= nil and sprite.image ~= "",
id .. " kept its sheet path (patch named only the flag)")
T.check(sprite.frames ~= nil, id .. " kept its frame count")
end
-- ------- the recipe itself compiles and is a function(ctx)
local chunk = assert(loadfile(MOD .. "/transforms.lua"))
local transform = chunk()
T.check(type(transform) == "function", "transforms.lua returns a function(ctx)")
-- driven with an empty cache it must write nothing and not raise
local wrote = 0
local ok, err = pcall(transform, {
exists = function() return false end,
readImage = function() error("must not read without exists()", 0) end,
writeImage = function() wrote = wrote + 1 end,
recolor = function(img) return img end,
})
T.check(ok, "the recipe survives an empty cache (" .. tostring(err) .. ")")
T.eq(wrote, 0, "and writes nothing rather than failing the mod")
-- with a cache present it derives one file per declared sheet
wrote = 0
local read = {}
T.check(pcall(transform, {
exists = function() return true end,
readImage = function(rel) read[#read + 1] = rel return { rel } end,
writeImage = function() wrote = wrote + 1 end,
recolor = function(img) return img end,
}), "the recipe runs over a populated cache")
T.eq(wrote, #read, "every sheet it read, it wrote back")
T.check(wrote >= 1, "at least one sheet is derived")
run.release()
T.finish("example_shiny_palette")
@@ -0,0 +1,32 @@
-- Asset transform: the whole point of this example. It runs once at
-- install inside the restricted context -- read the player's own imported
-- cache, write under save/mod-derived/<id>/ -- so the repo ships the
-- recipe and never a ROM-derived pixel.
--
-- The derived path mirrors the cache path, so Assets.resolve picks it up
-- for every consumer of assets/generated/sprites/red.png with no registry
-- entry at all. A recolor is exactly this: read, recolor, write back
-- under the same relative name.
local SHEETS = {
"sprites/red.png",
"sprites/red_bike.png",
}
-- lightest shade first; the recolor buckets every ink pixel into one of
-- these four by luminance, matching the importer's own 4-gray split
local TEAL = {
{ 248, 248, 248 },
{ 120, 224, 216 },
{ 32, 128, 152 },
{ 8, 32, 64 },
}
return function(ctx)
for _, rel in ipairs(SHEETS) do
-- a player who has not imported yet simply gets no derived art; the
-- vanilla sheet keeps rendering and the mod stays loaded
if ctx.exists(rel) then
ctx.writeImage(ctx.recolor(ctx.readImage(rel), TEAL), rel)
end
end
end