Compare commits

..

6 Commits

Author SHA1 Message Date
bryanthaboi 0dd889b35b Merge pull request #1505 from mleo2003/fix/launcher-split-layout
rg34xxsp launcher: also resolve GAMEDIR on split-tree firmwares (muOS)
2026-08-19 16:57:41 -04:00
bryanthaboi 032f894f7f Merge pull request #1495 from mleo2003/fix/fresh-skeleton-playthrough-id
SaveData: a fresh skeleton must not overwrite an existing playthrough binding
2026-08-19 16:57:30 -04:00
bryanthaboi 9922e235c6 Merge pull request #1576 from dburton95/dev
Fixes the greyscale cutoff for boulder.png
2026-08-19 16:56:17 -04:00
Dorian Burton 667267d9bb Fixes the greyscale cutoff for boulder.png
A fixed cutoff (e.g. "<=200 is ink") only makes sense for sprites with a light background to split against; a mostly-opaque 16x16 icon has almost no pixel above that cutoff, so every such icon collapsed onto the same "all ink" hash and was flagged as a near-duplicate of anything else that also collapsed -- which was most of them, boulder.png included. Thresholding against the image's own mean keeps the split meaningful (and roughly balanced) no matter how light or dark the source is.
2026-08-19 14:56:08 -04:00
mleo2003 6588901e9a rg34xxsp launcher: also resolve GAMEDIR on split-tree firmwares
The generated launcher resolves the game folder as "$SHDIR/gen1recomp" -- the
sibling of the script. That is right on Anbernic stock, and the comment above
it explains why PortMaster's \$directory was not used there (casing and mount
points differ).

Firmwares that keep launcher scripts and port data in SEPARATE trees -- muOS
puts scripts in roms/ports and data in ports, as does PortMaster on several
devices -- have no game beside the script, so the launcher exits without
starting anything.

Try the sibling first, unchanged, and fall back to the split layouts only when
the sibling holds no game.

Probe for bin/love.aarch64, not for the directory: on a split layout the script
has usually already created "$SHDIR/gen1recomp/conf" and log.txt on an earlier
failed run (its own mkdir and tee), so a directory test matches a decoy of the
script's own making. Verified on a muOS RG35XXSP, where exactly that decoy
exists and holds only conf/ and log.txt.

Stock is unaffected: its sibling holds the real binary and wins the first test,
including when a populated path exists elsewhere. With no game anywhere the
value is unchanged, so the failure mode stays what it was.
2026-08-17 21:36:49 -07:00
mleo2003 142d1358dd SaveData: a fresh skeleton must not overwrite an existing playthrough binding
ensurePlaythroughId() treats a fresh New Game skeleton as having no id, mints
one, and persists it into opts.playthroughIds[version][scope] -- even when that
slot already names a playthrough.

newGame() marks the skeleton on the boot frame, before any save is loaded, and
mods initialise inside that window: Storage:selected needs TitleState, which
does not exist yet, so Storage:context -> _scope -> ensurePlaythroughId is the
only path open to them. A mod touching mod.storage at init therefore replaces
the real save's id with a throwaway, stranding that save's mod storage, and it
repeats on every launch.

Observed on an RG35XXSP (engine 0.2.1, PotatoVoxel 1.7.11): a new playthrough
id in options.lua after every launch, 32 orphaned mod_storage directories, and
the mod's ~400MB prebuilt mesh cache abandoned under the id options.lua used to
name -- so every map rebuilt from scratch.

Keep both existing behaviours: a fresh skeleton still gets its own id, so two
unsaved New Games sharing a slot stay distinct, and it is still persisted when
the slot has no binding yet -- the contract tests/modkit/cases/
title_playthrough_context.lua pins, where a tool persists before the first
normal SAVE and the title must resolve it after a restart.

Only the overwrite of an EXISTING binding is dropped.

./scripts/test.sh: ALL TIERS PASSED (44/44 title_playthrough_context,
18/18 playthrough_identity).
2026-08-17 16:19:46 -07:00
8 changed files with 56 additions and 155 deletions
+20
View File
@@ -193,6 +193,26 @@ get_controls
[ -f "${controlfolder}/mod_${CFW_NAME}.txt" ] && source "${controlfolder}/mod_${CFW_NAME}.txt"
GAMEDIR="$SHDIR/gen1recomp"
# Anbernic stock keeps the launcher and the game folder side by side, so the
# SHDIR-relative path above is correct there and is tried first.
#
# Other firmwares (muOS, and PortMaster's layout on several devices) keep
# launcher scripts and port data in SEPARATE trees -- scripts under roms/ports,
# data under ports -- so the sibling folder holds no game.
#
# Probe for the BINARY, not the directory: on a split layout this script has
# usually already created "$SHDIR/gen1recomp/conf" and log.txt on an earlier
# failed run (see mkdir/tee below), so an existence test matches a decoy of our
# own making. Stock is unaffected -- its sibling holds the real binary and wins
# on the first test.
if [ ! -f "$GAMEDIR/bin/love.aarch64" ]; then
for candidate in "/$directory/ports/gen1recomp" \
"/mnt/sdcard/ports/gen1recomp" \
"/mnt/mmc/ports/gen1recomp" \
"/roms/ports/gen1recomp"; do
if [ -f "$candidate/bin/love.aarch64" ]; then GAMEDIR="$candidate"; break; fi
done
fi
CONFDIR="$GAMEDIR/conf"
mkdir -p "$CONFDIR"
+2 -3
View File
@@ -541,9 +541,8 @@ gains a field instead of the name gaining a prefix.
`battle.crit`, `battle.accuracy`, `battle.turn_order`,
`battle.enemy_action`, `battle.run`, `battle.exp_award`, `exp.gain`,
`catch.rate`, `trainer.party`, `battle.overlay`, `battle.low_health_alarm`,
`battle.catch_exp`, `battle.bottom_ui_visible`,
`battle.status_hud_visible` and `battle.move_grid_navigation`. One payload
difference: Gen 1's vanilla
`battle.catch_exp`, `battle.bottom_ui_visible` and
`battle.status_hud_visible`. One payload difference: Gen 1's vanilla
`battle.low_health_alarm` link reads `ctx.battle.data`, and Gold's battle
screen has no `.data` field, so the Gen 2 site **adds** `ctx.data` beside the
Gen 1 keys. A mod that calls `nextFn` is unaffected; one that reaches through
-84
View File
@@ -12,90 +12,6 @@
"tintColor": "3b5ca8",
"category": "games",
"versions": [
{
"version": "0.2.9",
"date": "2026-08-19",
"size": 13656293,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.9/gen1recomp++-0.2.9-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
},
{
"version": "0.2.8",
"date": "2026-08-19",
"size": 13653911,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.8/gen1recomp++-0.2.8-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1502 Gold doesn't show trainer balls\n- #1533 Retroarch Skin Problem 2 (#1503)\n\n## Contributors\n\n- @1Jamie\n- @AverageConsumer\n- @bryanthaboi\n- @castdrian\n- @thibautbus"
},
{
"version": "0.2.7",
"date": "2026-08-18",
"size": 13597177,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.7/gen1recomp++-0.2.7-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1005 (Android) Screen record mutes the game\n- #1291 Audio Crash\n- #1310 Incoming call crashes G1R\n- #1471 [Gold] #1117 still not fixed\n- #1528 Surfing Minigame doesn't play as intended\n- #1537 Shellder and Corsola missing from Rod encounter tables\n\n## Contributors\n\n- @1Jamie\n- @bryanthaboi\n- @castdrian"
},
{
"version": "0.2.6",
"date": "2026-08-18",
"size": 13589036,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.6/gen1recomp++-0.2.6-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1393 [Launcher -> Mods] Only the pages that you manually clicked to are used for sorting\n- #1418 (Pokémon Gold) Framerate and void fill options missing\n- #1430 [Gold] Shop ui off because of a border\n- #1519 Poison damage after battle inconsistent\n\n## Contributors\n\n- @bryanthaboi"
},
{
"version": "0.2.5",
"date": "2026-08-18",
"size": 13586158,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.5/gen1recomp++-0.2.5-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1390 switch and gold\n- #1503 Retroarch Skin Problem\n- #1508 Please check #1412 & #1414 again, we had a misunderstanding\n\n## Contributors\n\n- @bryanthaboi"
},
{
"version": "0.2.4",
"date": "2026-08-18",
"size": 13582747,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.4/gen1recomp++-0.2.4-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1496 Investigate Security according to https://hdbreaker.github.io/blog/pokemon-gen1recomp-hate-cheat/\n\n## Contributors\n\n- @bryanthaboi"
},
{
"version": "0.2.3",
"date": "2026-08-18",
"size": 13579254,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.3/gen1recomp++-0.2.3-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1497 skin studio needs a import file picker babyyyyyy\n\n## Contributors\n\n- @anxiousintrovert\n- @AverageConsumer\n- @bryanthaboi\n- @thibautbus"
},
{
"version": "0.2.2",
"date": "2026-08-18",
"size": 13575387,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.2/gen1recomp++-0.2.2-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @AverageConsumer\n- @bryanthaboi"
},
{
"version": "0.2.1",
"date": "2026-08-17",
"size": 13575320,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.1/gen1recomp++-0.2.1-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
},
{
"version": "0.2.0",
"date": "2026-08-17",
"size": 13575299,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.2.0/gen1recomp++-0.2.0-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1396 Nurse dialogue & options\n- #1398 Alignment of options for changing Pokemon\n- #1400 Flying Bug\n- #1401 [Gold] battlergfx $d9/$da load the wrong row count (jumptable crossed vs macro names)\n- #1406 Magikarp salesman dialogue issues\n- #1407 Not able to nickname Magikarp\n- #1411 No indication for stone evolutions\n- #1413 Using a stone closes menu\n- #1415 Super Nerd dialogue issues\n- #1416 (Pokémon Gold) Pokédex doesn't register other trainers' pokémon as seen\n- #1417 (Pokémon Gold) Pokémon you get in trade aren't being registered as caught\n- #1419 (Pokémon Gold) Deposited pokémon don't get healed\n- #1421 (Pokémon Gold) Bad status and catch state appears on the HUD before they should\n- #1422 (Pokémon Gold) Impossible to have the pokédex register Ditto as caught after it transforms\n- #1423 (Pokémon Gold) No save prompt before changing boxes in the PC\n- #1424 (Pokémon Gold) Quantity for owned TMs not being displayed\n- #1425 (Pokémon Gold) Items quantity in your bag should be alligned to the right\n- #1427 (Pokémon Gold) Can't switch items' position in your bag\n- #1428 (Pokémon Gold) Game doesn't show how many pokémon other trainers have\n- #1429 Pikachu not sliding in before its cry. Stuck on standard pokeball release animation.\n- #1431 Shiny sparkle does not play on your sent out shiny pokemon\n- #1432 Experimental marked mods don't install Android\n- #1433 (Pokémon Gold) Missing prompt for depositing pokémon\n- #1435 When npcs stop you to talk or when you walk up to npcs to talk to them sometimes the player has the wrong sprite\n- #1437 Issues with player sprite on map\n- #1440 hold a direction during cutscene and face the wrong way\n- #1441 Magnet Train missing animation\n- #1442 Radio dial is missing in PokeGear radio\n- #1443 Skipping production logo also skips battle scene\n- #1444 Pokemon lack type immunity to status moves\n- #1447 Soft-lock on Cinnabar Island\n- #1449 Visual error on Route 28\n- #1456 Activating all mods doesn't work properly\n- #1461 #1265 didnt got fixed.\n- #1464 Experiance shared in battle\n- #1465 Changing Touch Layout crashes launcher\n- #1466 #1403 Still Happens\n- #1467 A clearer definition of the use of AI for this reconstruction\n- #1468 [Gold] BICYCLE is broken and some pokegear bug\n- #1469 [Gold] status effects aren't shown in the party overlay or the summary screen of the pokemon\n- #1470 Mod updater doesn't work properly when AppImage is running through Steam or Game Mode (Steam Deck)\n\n## Contributors\n\n- @bryanthaboi"
},
{
"version": "0.1.99",
"date": "2026-08-17",
"size": 11391467,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.99/gen1recomp++-0.1.99-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #597 Pulling mod index fails on Android\n- #1403 Save editor not allowing moves to go past ZAP_CANNON\n\n## Contributors\n\n- @1Jamie\n- @AverageConsumer\n- @bryanthaboi\n- @emre155\n- @sanjinpepic\n- @ShaneMcGovernIE\n- @syybott\n- @thibautbus"
},
{
"version": "0.1.98",
"date": "2026-08-16",
"size": 11380117,
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.98/gen1recomp++-0.1.98-ios.ipa",
"localizedDescription": "Download the correct version for your computer below.\n\n## Issues closed\n\n- #1181 Poison seems to trigger twice during the poisoned pokemon's turn\n- #1211 S.S. Anne Visual bug when it's sailing away\n- #1212 the move payday does not grant money in gen 2\n- #1214 Title Screen with OG Red is the wrong color\n- #1224 Windowed and borderless toggle in Gold\n- #1228 No option to nickname starter\n- #1229 Encounter rate grace period not working\n- #1230 Couple of sound effects missing\n- #1231 Using Tackle partially distorts battle sprites\n- #1232 Wild pokemon's sprite disappears early when using a pokeball\n- #1249 Cant use stat items IE HP UP PP UP PROTIEN\n- #1251 You don't have a COIN CASE\n- #1265 Major: Regression from #984 (probably?)\n- #1267 [GOLD] POKEDEX didn't show pokemon appear area\n- #1269 [Gold] shadow ball should be invert the screen\n- #1271 [Gold] substitute image broken/not shown\n- #1272 [Gold] swift still checks accuracy and/or evasion\n- #1273 S.S. Anne Issues\n- #1276 Nurse back to not bowing (and turning)\n- #1279 Rival still not looking at player when initiating first fight\n- #1282 PKMN league PC option missing\n- #1293 Dig animation is bugged in-battle\n- #1296 Opponent's moves failing\n- #1298 Gen1 sound tracks have a fade in period, if you enter a route and immediately exit it while this transition is going on it will land on the wrong music\n- #1301 Pixels aren't square\n- #1303 Animation speed of walking NPCs too slow\n- #1305 Wrong Pikachu cry when getting defeated\n- #1307 Rival theme broken after initial fight in Yellow\n- #1318 Thunder Wave works on Ground-types\n- #1328 Message for turning on the PC missing\n- #1329 Name Select Background\n- #1330 Message before looking at map missing\n- #1331 Messages in Oak's lab missing\n- #1333 E-mail in Oak's lab missing\n- #1334 Missing message after picking starter\n- #1335 No Money Box\n- #1338 Rival's sister missing dialogue and roaming\n- #1340 Color palett doesn't affect attack animations\n- #1341 Pokedex entries look wrong\n- #1343 No dashes in empty attack slots during fights\n- #1344 Town Map not showing player sprite\n- #1345 Wrong health color on OG palett\n- #1346 Health still black when viewing stats\n- #1360 No Surfing Music\n- #1362 Poison damage does not flash the screen\n- #1368 Fishing Rods behaving irregularly\n- #1385 Team Rocket Hideouts missing music\n- #1388 Safeguard targets opponent, not user\n- #1389 Gastly unobtainable\n- #1391 NPC not escorting player to museum\n\n## Contributors\n\n- @bryanthaboi"
},
{
"version": "0.1.97",
"date": "2026-08-16",
+14 -1
View File
@@ -1279,14 +1279,27 @@ function SaveData.ensurePlaythroughId(save, injectedFs)
local isFresh = save == freshPlaythrough
if isFresh then freshPlaythrough = nil end
local byVersion = opts.playthroughIds and opts.playthroughIds[version]
id = not isFresh and byVersion and byVersion[scope] or nil
local existing = byVersion and byVersion[scope]
id = not isFresh and existing or nil
if type(id) ~= "string" or id == "" then
id = SaveData.newPlaythroughId()
-- A fresh skeleton still gets its own id (two unsaved New Games sharing a
-- slot must stay distinct), and it is still persisted when the slot has no
-- binding yet -- that is the contract a tool relies on to resolve
-- `selected` at the title after a restart, before any normal SAVE.
--
-- What it must NOT do is OVERWRITE a binding that already exists. newGame()
-- marks a skeleton on the boot frame, before any save is loaded, and mods
-- initialise inside that window -- so a mod touching storage at init
-- replaced the real save's id with a throwaway, stranding that save's mod
-- storage and repeating on every launch.
if not (isFresh and type(existing) == "string" and existing ~= "") then
opts.playthroughIds = opts.playthroughIds or {}
opts.playthroughIds[version] = opts.playthroughIds[version] or {}
opts.playthroughIds[version][scope] = id
SaveData.saveOptions(opts, injectedFs)
end
end
save.meta.playthroughId = id
return id
end
+1 -22
View File
@@ -44,12 +44,6 @@ local BattleState = {}
BattleState.__index = BattleState
BattleState.isOpaque = true
function BattleState:moveGridNavigation()
if not Runtime.wantsHook("battle.move_grid_navigation") then return false end
return Runtime.call("battle.move_grid_navigation", function() return false end,
self) == true
end
-- Armed while a battle line waits for PromptButton (home/text.asm). Any
-- positive value means "hold until A/B"; the cart never times these out, so
-- the victory jingle can keep looping through the post-win prompts.
@@ -2050,22 +2044,7 @@ function BattleState:update(_dt)
if self.phase == "moves" then
local moves = self:playerMoves()
local grid
if self:moveGridNavigation() then
local index, count = self.moveIndex, #moves
if input:wasPressed("left") or input:wasPressed("right") then
local other = math.floor((index - 1) / 2) * 2
+ (1 - (index - 1) % 2) + 1
grid = other <= count and other or index
elseif input:wasPressed("up") or input:wasPressed("down") then
local other = (1 - math.floor((index - 1) / 2)) * 2
+ (index - 1) % 2 + 1
grid = other <= count and other or index
end
end
if grid then
self.moveIndex = grid
elseif input:wasPressed("up") then
if input:wasPressed("up") then
self.moveIndex = self.moveIndex > 1 and self.moveIndex - 1 or #moves
elseif input:wasPressed("down") then
self.moveIndex = self.moveIndex < #moves and self.moveIndex + 1 or 1
-1
View File
@@ -422,7 +422,6 @@ local GEN2_HOOKS = {
-- nextFn gets nil there).
"battle.catch_exp", "battle.low_health_alarm", "battle.overlay",
"battle.bottom_ui_visible", "battle.status_hud_visible",
"battle.move_grid_navigation",
-- One pic path resolver for both games: the Gen 1 site is the SHARED
-- src/pokemon/Sprites.lua and Gold's own battle screen calls the same hook
-- with the Gen 1 ctx keys plus `letter` and `shiny`, which Red has no
-36
View File
@@ -267,49 +267,13 @@ end
do
local BattleState = require("src.battle.BattleState")
local Gen2BattleState = require("src.ui.gen2.BattleState")
local battle = { wideLayout = function() return false end }
check(not BattleState.moveGridNavigation(battle),
"classic move navigation stays a list without a mod")
check(not Gen2BattleState.moveGridNavigation({}),
"Gold move navigation stays a list without a mod")
local unsub = wrap("battle.move_grid_navigation", function() return true end)
check(BattleState.moveGridNavigation(battle),
"a mod can opt the classic move menu into grid navigation")
check(Gen2BattleState.moveGridNavigation({}),
"the same hook opts Gold's move menu into grid navigation")
local pressed, moveCount = "right", 4
local gold = setmetatable({
phase = "moves", moveIndex = 1,
slideFrame = math.huge,
game = { input = {
wasPressed = function(_, key) return key == pressed end,
} },
updateAlarm = function() end,
stepHpAnim = function() return false end,
playerMoves = function()
local moves = {}
for i = 1, moveCount do moves[i] = {} end
return moves
end,
}, { __index = Gen2BattleState })
gold:update(0)
check(gold.moveIndex == 2,
"Gold grid navigation moves right across a companion move row")
pressed, gold.moveIndex = "down", 1
gold:update(0)
check(gold.moveIndex == 3,
"Gold grid navigation moves down the companion move column")
pressed, moveCount, gold.moveIndex = "down", 3, 2
gold:update(0)
check(gold.moveIndex == 2,
"Gold grid navigation does not select an empty fourth move slot")
unsub()
pressed, gold.moveIndex = "right", 1
gold:update(0)
check(gold.moveIndex == 1,
"removing the hook restores Gold's native vertical move list")
battle.wideLayout = function() return true end
check(BattleState.moveGridNavigation(battle),
"the native wide move grid remains enabled without a mod")
+15 -4
View File
@@ -988,14 +988,25 @@ def cmd_add_release_workflow(args, repo):
# ---------------------------------------------------------------- lint
def ahash(image):
"""Ink-mask hash over the 8x8 downscale: background (the lightest GB
shade) vs ink. Swapping the three ink shades -- the classic recolor --
leaves the mask intact, which is exactly what MK302 wants to catch."""
"""Ink-mask hash over the 8x8 downscale: background vs ink, split at
THIS image's own average brightness rather than a fixed shade. Swapping
the three ink shades -- the classic recolor -- leaves the mask intact,
which is exactly what MK302 wants to catch.
A fixed cutoff (e.g. "<=200 is ink") only makes sense for sprites with a
light background to split against; a mostly-opaque 16x16 icon has almost
no pixel above that cutoff, so every such icon collapsed onto the same
"all ink" hash and was flagged as a near-duplicate of anything else that
also collapsed -- which was most of them, boulder.png included.
Thresholding against the image's own mean keeps the split meaningful
(and roughly balanced) no matter how light or dark the source is."""
from PIL import Image
small = image.convert("L").resize((8, 8), Image.LANCZOS)
raw = (small.get_flattened_data() if hasattr(small, "get_flattened_data")
else small.getdata())
return sum((1 << i) for i, p in enumerate(raw) if p <= 200)
raw = list(raw)
average = sum(raw) / len(raw)
return sum((1 << i) for i, p in enumerate(raw) if p <= average)
def hamming(a, b):