Threads open a fresh Lua state with a full standard library, so they
stayed blocked wholesale. PotatoVoxel's prebuilder wants to run its
pure geometry phase on worker threads; the mod declares the new
"compute" permission and the sandbox hands out love.thread only
then. The worker runs the mod's own source (source-only, like every
mod file) and receives data only through channels.
Android ships no curl and the JNI bridge was GET-only, so mod.postLog
failed there with 'no POST transport on this platform' (HostShell.lua).
Add the mirror of httpDownload: GameActivity.httpPost (https-only,
hand-followed redirects re-POSTing the body, one-way), the JNI bridge
with the same old-APK-skew tolerance, the love.system.httpPost binding,
and the HostShell arm that rides it when curl is absent. The body
crosses the JNI as raw bytes (jbyteArray) so a log ring with arbitrary
UTF-8 cannot corrupt through modified-UTF-8 jstring conversion.
Font.drawBox leaves the caller's color at white; every other screen
resets to black right after calling it, but ManagerState.lua's draw()
and drawOverlay() never did. That's invisible on the vanilla tile font
(tile glyphs are black-on-transparent regardless of color) but renders
fully invisible white-on-white text once a mod's TTF font is active.
The targeted test only covered the RBY side (ItemEffects.lua,
TrainerAI.lua); src/battle/gen2/Battle.lua's EFFECT_LIGHT_SCREEN/
EFFECT_REFLECT fix had no test at all, spotted when asked whether the
Gold changes were covered.
Adds a minimal MACHOP/TACKLE gen2 fixture (same shape as
tests/gen2_move_effects_test.lua's), calls both MOVE_EFFECTS directly,
and checks a loaded catalog reaches the whole message template (Gold
wraps the full sentence, not just the stat name). Confirmed it catches
the regression: reverting Battle.lua to its pre-fix state fails 2 of
the suite's now 8 checks.
No existing test could tell a translated stat name apart from a raw
stat:upper() that never went through Strings() at all: every rose!
message assertion in the suite runs with no catalog loaded, where
Strings() is an identity function either way.
Loads a real catalog (Strings.load) that translates one stat name at a
time and checks it actually reaches the X-item, vitamin, and AI-trainer
X-item messages -- ROM-free, over tests/fixture_data. Confirmed this
catches the regression: reverting src/inventory/ItemEffects.lua and
src/battle/TrainerAI.lua to their pre-fix state fails 5 of 6 checks.
Strings(stat:upper()) is a dynamic argument -- tools/modkit.py's
STRINGS_CALL harvester only matches a literal string right after
Strings(/Strings.source(, so it can't discover "ATTACK"/"DEFENSE"/etc.
from these call sites. Translation coverage happened to still work
only because the same literals are independently harvested from
unrelated call sites (MoveEffects.lua's STAT_LABEL, BattleState.lua's
literal Strings("ATTACK") calls) -- real but fragile, found in review.
Reuse the codebase's existing pattern for exactly this situation
(MoveEffects.lua's STAT_LABEL): a local table built at require time
with Strings.source(...), which the harvester can see, resolved to a
translated label at use time with Strings(TABLE[key]). Adds one such
table to ItemEffects.lua (covering its X-item and vitamin call sites,
including "hp") and one to TrainerAI.lua.
Same theme as the RBY fix, found while checking whether Gold had the
same gap: EFFECT_LIGHT_SCREEN and EFFECT_REFLECT built their "'s
SPCL.DEF/DEFENSE rose!" message by raw string concatenation, bypassing
Strings() entirely -- unlike most other messages in this file (e.g.
"%s\nused %s!" a few lines up), which already go through it.
Wrap the whole message template in Strings(), matching that existing
pattern; the substituted name still comes from monName() as before.
Gold's gen2/Battle.lua has many more messages built the same
unwrapped way (fainted!, learned..., missed!, and so on) -- that is
the much larger "Battle messages" gap already tracked separately and
deliberately left out of this change.
Both the player-side and AI-trainer stat-rise messages (X ATTACK/
DEFENSE/etc. and the vitamins) passed the raised stat's name as a raw
uppercase Lua string (stat:upper()), bypassing Strings() entirely, so
it always rendered in English regardless of the active language even
though the surrounding sentence template was already translated.
Wrap the substituted stat name in Strings() at every call site
(src/inventory/ItemEffects.lua's two player-side messages and
src/battle/TrainerAI.lua's AI-trainer X-item message, found in
review), reusing the same "ATTACK"/"DEFENSE"/"SPEED"/"SPECIAL"/"HP"
keys SummaryMenu.lua's stat labels already look up the same way.
syncIdentity unconditionally recomputed mon.shiny from the mon's DVs,
overwriting whatever was there. It is wired into refreshStats, which
SummaryMenu.new calls on every menu open, so a forced shiny -- Mon.new's
opts.shiny path, DVs that do not themselves read as shiny -- got
un-shinied the moment the summary screen opened, even though opts.shiny
already wins over shiny.roll at construction for exactly this case (a
scripted shiny is the cart overriding the roll, not a roll to be
hooked).
mon.shiny now only ever gets PROMOTED by the DV check, never demoted:
`mon.shiny or Mon.isShiny(...)`. A naturally shiny mon and a plain one
are unaffected -- the DV check still runs and still decides the first
time -- and a mon whose DVs are edited to justify shininess later still
promotes normally; only an already-true shiny stops being able to flip
back to false on a later refresh.
Both call sites -- Game2:usePartyItem (the field pack) and
BattleState:useItem (the battle pack) -- asked ItemEffects.partyAction
for an item's family with no `data` argument, even though every other
call in the same functions (useOnMon, usePpItem, applyPartyItem) passed
it through correctly. partyAction resolves through recordFor, which
reads data.gen2ItemEffects when given a dataset and falls back to the
module's own built-in RECORDS table when not -- so with no data, a
mod's own item_effects record was invisible and every mod-defined Gen 2
field or battle item resolved to a nil action, falling straight through
to "isn't going to help here" / "isn't going to help here" without ever
opening the party picker.
Both now pass the live dataset (self.data on Game2, self.game.data on
the battle screen) the same way their sibling calls already did.
bindGoldData points gen2Palettes, gen2Icons, gen2Pokedex, gen2Landmarks,
gen2Roofs and gen2Sprites at the extractor's own Gold tables through
loadGen, but never gen2Constants -- despite Schemas.GEN2 routing
`constants` to that same namespaced-and-differently-shaped category
palettes and icons are in. A save editor boot left data.gen2Constants
unset, so mod.content.constants:get(...) read an empty table instead
of the cart's ordered name lists, misreading the generation and
rejecting every record a mod shaped off it.
data.gen2Constants now goes through the same loadGen("constants") path
the other five already use, falling back the same way they do when no
ROM cache is active.
R.maps.objects was f.opt(f.list(f.any)): a static wild encounter's
species (OverworldController.lua's d.pokemon, handed straight to
BattleState.newWild) went completely unchecked at load time, unlike an
encounter slot's species. A typo'd or removed id sat in a loaded mod
and only surfaced as a crash the moment a player reached that object.
Objects share one array across every kind -- NPCs, signs, warps and
static encounters all coexist with no field the loader could use to
tell them apart ahead of time -- so a strict f.rec covering the whole
shape would reject every kind this schema does not enumerate. Added
f.partial, an open counterpart to f.rec: it type-checks (and, through
collectRefs, cross-reference-checks) only the fields it is given and
leaves everything else on the value alone, the same extensibility
f.rec already grants at a record's top level but nowhere further in.
R.maps.objects now types just `pokemon` through it, so a bad species
id is a load-time "unresolved reference" error instead of a runtime
crash, while an NPC object's sprite/movement/range/... fields -- never
named in this schema -- still pass through untouched.
ItemEffects.use walked speciesDef.tmhm with a bare ipairs() to check
whether the species could learn the machine's move. A record with no
tmhm field at all -- a mod species that never set one, or any record
missing it for whatever reason -- hit ipairs(nil) and took the whole
game down on the first TM/HM use, rather than reaching the ordinary
"can't learn that move" refusal a species whose list simply omits the
move already gets.
An absent list now reads the same as an empty one: nothing to learn,
same refusal, same sound, same text.
useOn was a plain Lua local: every result ItemEffects.use returned fell
through to one unconditional showMessages with no seam a mod could
reach, unlike menu.lua/boxmark.lua/formview.lua's screens, which wrap
their own default behavior as a table field or a Runtime hook. A mod
could not suppress a message, delay it behind a screen of its own, or
substitute a different outcome for one item id -- exactly the gap noted
against Ultra Burst's item-driven fusion, which had nowhere left to
attach a bespoke animation once TextBox.new turned out to be the only
other reachable seam.
This wraps the whole dispatch in a Runtime.call("item.use", ...) hook,
the same mechanism "battle.overlay", "ui.party.submenu" and the rest of
src/ui already use, rather than exporting BagMenu.useOn as a table
field. A hook is the smaller commitment: it is additive (a fresh
Runtime.call site needs no schema or manifest change and costs nothing
unsubscribed -- see tests/engine/gate_hooks.lua's null-object case) and
a mod can still run the vanilla flow unchanged by calling the handed-in
vanilla function, whereas a table field would fix useOn's exact
signature as public API the moment it shipped. If the maintainer would
rather match the sibling screens' convention directly, exporting
BagMenu.useOn is the alternative and does not conflict with this hook
existing alongside it.
vanillaUseOn keeps the original function body; useOn is now the thin
wrapper mods observe through, and every internal caller in this file
still goes through useOn so the hook fires on every path into it.
The comment above the gatedFor skip in Schemas.crossValidate still
described growth_rates and evolution_methods as unconfirmable Gen 2
namespaces, the way they were before each got a real Gen 2 id space:
growth_rates keeps its Gen 1 target and is seeded from the extractor's
data.pokemon.growthRates (src/battle/gen2/Mon.lua), and
evolution_methods routes to gen2EvolutionMethods, a fixed literal set
(src/core/gen2/Evolution.lua) that exists with or without a ROM
import. Schemas.GEN2 does not gate either name -- gate_gen2_mod_api.lua
pins that directly, including a case that a bad evolution method on a
Gold species is still caught -- so the two are validated like any other
reference today, not skipped. Nothing here changes that behavior;
only the comment, which was describing an earlier state of the code,
is corrected.
DENIED_PREFIX blocked love.* and ffi.* submodule requires but had no
entry for jit, so require("jit.util") walked straight through to the
real module. jit.util is LuaJIT's own equivalent of the debug library
this file already denies by name: funcbc, funck and the rest read the
bytecode and constants of any function a chunk can reach, which is
enough to recover upvalues -- the real _G, love, io -- that the sandbox
exists to keep out of a mod's hands.
Adding "jit" to DENIED_PREFIX blocks jit.* submodule requires the same
way love.* and ffi.* already are, while leaving the bare jit global
(env.jit, handed over directly for jit.on/off/flush) and a bare
require("jit") untouched -- jit.util is not a field of that table
without its own require, so neither route was ever a way to reach it.
stepHPDrain counts drainHold down to 0 as the last step of every phase
(pixel slide, HP-number step, closing frames) but never let go of the
field afterward, so it sat at 0 -- not nil -- for the rest of the
battle. BattleSafety.inspect uses drainHold ~= nil as its
settled-presentation gate for checkpoint capture, so the very first HP
change in a battle permanently refused every checkpoint after it with
battle_phase_busy, even once the bar had long since caught up.
Only nil the field when the whole drain is actually over (bar pixel,
HP number and the closing-frame hold all settled), not on every
mid-sequence 0 -- a fresh HP change still needs drainHold to read as
busy so BattleSafety keeps refusing captures until that one settles
too.
and updated the behavior of the patch notes
also fixed manual update checking
added test to make sure no prs or build tasks are able to pass if the luajit limits ar exceeded.
DAYS (SUNDAY..SATURDAY), the MORN/DAY/NITE word PrintHour prints, and the
"o'clock"/"min." suffixes bypassed src/core/Strings.lua entirely -- they
were plain Lua literals with no lookup, so a translation mod's `strings`
registry had nothing to catch and Oak's clock screens, the day-of-week
wheel, the main menu clock box and the Pokegear's clock card kept printing
English regardless of the loaded language (reported from a real Spanish
Gold build).
Both live in src/core/gen2/Clock.lua, which already owns weekday/hour
arithmetic and is already required by InitClock.lua, MainMenu.lua and
Pokegear.lua: Clock.DAY_NAMES + Clock.weekdayName(day) is the one place the
three screens read a weekday's name from, so a fix to it cannot land on one
screen and silently miss the other two. Clock.daytimeLabel(hour) is the
translated counterpart to Palettes.clockDaytime, which keeps answering the
untranslated MORN/DAY/NITE key every FORCED_DAYTIME lookup in Palettes.lua
compares against -- src/world/gen2/Palettes.lua itself is untouched, so
that module stays pure table/color math with no Strings coupling.