mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 12:15:31 +02:00
big ass modding update
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user