mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-28 06:49:29 -04:00
Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev
This commit is contained in:
@@ -79,7 +79,7 @@ return {
|
||||
end)
|
||||
-- the PRNT stand-in always reports where the PNG landed
|
||||
game.stack:push(TextBox.new(game, saved
|
||||
and Strings("There you go!\fSaved as\n%s\vin the save\nfolder.", saved)
|
||||
and (Strings("There you go!\f") .. Printer.savedWhereText(saved))
|
||||
or Strings("Printer error!\n%s", tostring(err)), done))
|
||||
end))
|
||||
end))
|
||||
|
||||
@@ -119,8 +119,7 @@ return {
|
||||
Font.draw(Strings("%d pts", hi), 96, 40)
|
||||
end)
|
||||
push(game, saved
|
||||
and Strings("Printed!\fSaved as\n%s\vin the save\nfolder.",
|
||||
saved)
|
||||
and (Strings("Printed!\f") .. Printer.savedWhereText(saved))
|
||||
or Strings("Printer error!\n%s", tostring(err)), done)
|
||||
end))
|
||||
end)
|
||||
|
||||
@@ -428,11 +428,19 @@ local function returnToLauncher(opts)
|
||||
-- Leave the cart's scope behind: the launcher's own settings and slots are
|
||||
-- the base game's, not the cart's. The speed ladder is cart state too, so
|
||||
-- a 1x/2x cart must not pin the launcher or the next game.
|
||||
require("src.core.SaveData").setCart(nil)
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local cartId = SaveData.getCart()
|
||||
SaveData.setCart(nil)
|
||||
require("src.core.GameSpeed").setAllowed(nil)
|
||||
|
||||
SessionLifecycle.endMountedSession(currentVersion)
|
||||
|
||||
-- Slot lists are resolved once per process. Invalidate only the game
|
||||
-- (and cart, if any) we just left so the new launcher can migrate a flat
|
||||
-- in-game SAVE into a visible slot -- nothing else is rewritten.
|
||||
SaveData.refreshSlotResolution(currentVersion)
|
||||
if cartId then SaveData.refreshSlotResolution("cart_" .. cartId) end
|
||||
|
||||
applySavedOrientation()
|
||||
|
||||
local preload = require("src.mods.LauncherMods").translationStrings()
|
||||
@@ -444,6 +452,10 @@ local function returnToLauncher(opts)
|
||||
end
|
||||
|
||||
Importer = makeLauncher({ initialTab = opts and opts.tab or nil })
|
||||
-- Finger that confirmed EXIT GAME is often still down over Import Save.
|
||||
if Importer.ignoreReturningPointer then
|
||||
Importer:ignoreReturningPointer()
|
||||
end
|
||||
end
|
||||
|
||||
local pendingLauncherReturn
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# it in `app.name_byte_array`
|
||||
#app.name=LÖVE for Android
|
||||
|
||||
app.application_id=com.theboisclub.pokemonred
|
||||
app.application_id=com.theboisclub.pokemonred.androidfixes
|
||||
# fullUser: allow every orientation the player's device permits (portrait and
|
||||
# landscape), honouring their auto-rotate lock. Was "portrait" (locked).
|
||||
app.orientation=fullUser
|
||||
@@ -17,4 +17,4 @@ android.enableJetifier=false
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=true
|
||||
app.name=gen1recomp
|
||||
app.name=gen1recomp (test)
|
||||
|
||||
@@ -249,6 +249,32 @@ bool showCreateDocument(const char *suggestedName)
|
||||
return result;
|
||||
}
|
||||
|
||||
bool exportImageToGallery(const char *relativePath)
|
||||
{
|
||||
if (relativePath == nullptr || relativePath[0] == '\0')
|
||||
return false;
|
||||
|
||||
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||
jclass activity = env->FindClass("org/love2d/android/GameActivity");
|
||||
|
||||
jmethodID method = env->GetStaticMethodID(activity, "exportImageToGallery",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Z");
|
||||
if (method == nullptr)
|
||||
{
|
||||
env->ExceptionClear();
|
||||
env->DeleteLocalRef(activity);
|
||||
return false;
|
||||
}
|
||||
jstring jpath = env->NewStringUTF(relativePath);
|
||||
jstring jsavedir = env->NewStringUTF(bridgeSaveDirectory());
|
||||
jboolean result = env->CallStaticBooleanMethod(activity, method, jpath, jsavedir);
|
||||
env->DeleteLocalRef(jsavedir);
|
||||
env->DeleteLocalRef(jpath);
|
||||
|
||||
env->DeleteLocalRef(activity);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool syncHealthSteps()
|
||||
{
|
||||
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||
|
||||
@@ -74,6 +74,13 @@ bool showFilePicker(const char *destFilename = nullptr);
|
||||
**/
|
||||
bool showCreateDocument(const char *suggestedName = nullptr);
|
||||
|
||||
/**
|
||||
* Copy a PNG/JPEG under the LOVE save identity (relative path, e.g.
|
||||
* prints/diploma_….png) into Pictures/Gen1Recomp and media-scan the
|
||||
* in-app copy so USB / file managers can see it (#2103).
|
||||
**/
|
||||
bool exportImageToGallery(const char *relativePath = nullptr);
|
||||
|
||||
/**
|
||||
* Pokéwalker step bridge: asks GameActivity to read the hardware step
|
||||
* counter and stage steps_pending.json in the save identity dir (see
|
||||
|
||||
@@ -228,6 +228,16 @@ bool System::createFile(const char *suggestedName) const
|
||||
#endif
|
||||
}
|
||||
|
||||
bool System::exportImage(const char *relativePath) const
|
||||
{
|
||||
#ifdef LOVE_ANDROID
|
||||
return love::android::exportImageToGallery(relativePath);
|
||||
#else
|
||||
LOVE_UNUSED(relativePath);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool System::syncHealthSteps() const
|
||||
{
|
||||
#ifdef LOVE_ANDROID
|
||||
|
||||
@@ -129,6 +129,12 @@ public:
|
||||
**/
|
||||
virtual bool createFile(const char *suggestedName = nullptr) const;
|
||||
|
||||
/**
|
||||
* Copy a PNG/JPEG from the LOVE save identity into Pictures/Gen1Recomp
|
||||
* and media-scan the in-app file (#2103). Android only; false elsewhere.
|
||||
**/
|
||||
virtual bool exportImage(const char *relativePath = nullptr) const;
|
||||
|
||||
/**
|
||||
* Pokéwalker: stage pending real-world steps (steps_pending.json in the
|
||||
* save dir) from the platform step source. Android-only; false elsewhere.
|
||||
|
||||
@@ -119,6 +119,13 @@ int w_createFile(lua_State *L)
|
||||
return 1;
|
||||
}
|
||||
|
||||
int w_exportImage(lua_State *L)
|
||||
{
|
||||
const char *path = luaL_checkstring(L, 1);
|
||||
luax_pushboolean(L, instance()->exportImage(path));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int w_syncHealthSteps(lua_State *L)
|
||||
{
|
||||
luax_pushboolean(L, instance()->syncHealthSteps());
|
||||
@@ -341,6 +348,7 @@ static const luaL_Reg functions[] =
|
||||
{ "pickFile", w_pickFile },
|
||||
{ "pickFileKinds", w_pickFileKinds },
|
||||
{ "createFile", w_createFile },
|
||||
{ "exportImage", w_exportImage },
|
||||
{ "syncHealthSteps", w_syncHealthSteps },
|
||||
{ "restartApp", w_restartApp },
|
||||
{ "installApk", w_installApk },
|
||||
|
||||
@@ -46,6 +46,8 @@ import android.app.AlarmManager;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.UiModeManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.ClipData;
|
||||
import android.content.DialogInterface;
|
||||
@@ -64,12 +66,15 @@ import android.media.AudioDeviceCallback;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioFocusRequest;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Vibrator;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -1328,7 +1333,13 @@ public class GameActivity extends SDLActivity {
|
||||
self.pendingCreateSuggestedName = suggestedName;
|
||||
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("application/octet-stream");
|
||||
// PNG exports (diploma / dex prints) should open as images so the
|
||||
// picker offers Photos / Gallery, not only generic "Documents".
|
||||
String mime = "application/octet-stream";
|
||||
String lower = suggestedName.toLowerCase(Locale.US);
|
||||
if (lower.endsWith(".png")) mime = "image/png";
|
||||
else if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) mime = "image/jpeg";
|
||||
intent.setType(mime);
|
||||
intent.putExtra(Intent.EXTRA_TITLE, suggestedName);
|
||||
try {
|
||||
self.startActivityForResult(intent, FILE_CREATE_REQUEST_CODE);
|
||||
@@ -1339,6 +1350,116 @@ public class GameActivity extends SDLActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a PNG/JPEG already written under the LOVE save identity (e.g.
|
||||
* prints/diploma_….png) into the public Pictures/Gen1Recomp album and
|
||||
* media-scan the in-app copy so USB / file managers can see it (#2103).
|
||||
* No picker: diploma / dex print should not interrupt the game.
|
||||
*/
|
||||
@Keep
|
||||
public static boolean exportImageToGallery(String relativePath, String saveDir) {
|
||||
GameActivity self = (GameActivity) mSingleton;
|
||||
if (self == null) return false;
|
||||
if (relativePath == null || relativePath.length() == 0) return false;
|
||||
if (relativePath.indexOf("..") >= 0) {
|
||||
Log.d("GameActivity", "refusing unsafe export path: " + relativePath);
|
||||
return false;
|
||||
}
|
||||
self.pendingPickSaveDir = (saveDir != null) ? saveDir : "";
|
||||
File source = new File(self.saveIdentityDir(), relativePath);
|
||||
if (!source.isFile()) {
|
||||
Log.d("GameActivity", "exportImage missing at " + source);
|
||||
return false;
|
||||
}
|
||||
String name = source.getName();
|
||||
String lower = name.toLowerCase(Locale.US);
|
||||
String mime = "image/png";
|
||||
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) mime = "image/jpeg";
|
||||
else if (!lower.endsWith(".png")) {
|
||||
Log.d("GameActivity", "exportImage refusing non-image: " + name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make the app-private prints/ copy visible over MTP / file managers.
|
||||
MediaScannerConnection.scanFile(self,
|
||||
new String[]{ source.getAbsolutePath() },
|
||||
new String[]{ mime },
|
||||
null);
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
ContentResolver resolver = self.getContentResolver();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
|
||||
values.put(MediaStore.MediaColumns.MIME_TYPE, mime);
|
||||
values.put(MediaStore.MediaColumns.RELATIVE_PATH,
|
||||
Environment.DIRECTORY_PICTURES + "/Gen1Recomp");
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 1);
|
||||
Uri collection = MediaStore.Images.Media
|
||||
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
|
||||
Uri uri = resolver.insert(collection, values);
|
||||
if (uri == null) {
|
||||
Log.d("GameActivity", "MediaStore insert returned null");
|
||||
return false;
|
||||
}
|
||||
OutputStream out = resolver.openOutputStream(uri);
|
||||
if (out == null) {
|
||||
resolver.delete(uri, null, null);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
copyStream(new FileInputStream(source), out);
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
values.clear();
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0);
|
||||
resolver.update(uri, values, null, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pre-Q: write into public Pictures and ask the scanner to index it.
|
||||
File pictures = Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_PICTURES);
|
||||
File album = new File(pictures, "Gen1Recomp");
|
||||
if (!album.exists() && !album.mkdirs()) {
|
||||
Log.d("GameActivity", "could not create " + album);
|
||||
return false;
|
||||
}
|
||||
File dest = new File(album, name);
|
||||
copyFileToFile(source, dest);
|
||||
MediaScannerConnection.scanFile(self,
|
||||
new String[]{ dest.getAbsolutePath() },
|
||||
new String[]{ mime },
|
||||
null);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.d("GameActivity", "exportImage failed: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyStream(InputStream in, OutputStream out) throws IOException {
|
||||
try {
|
||||
byte[] buf = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(buf)) >= 0) {
|
||||
if (n > 0) out.write(buf, 0, n);
|
||||
}
|
||||
} finally {
|
||||
try { in.close(); } catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyFileToFile(File source, File dest) throws IOException {
|
||||
FileOutputStream out = new FileOutputStream(dest);
|
||||
try {
|
||||
copyStream(new FileInputStream(source), out);
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
private File saveIdentityDir() {
|
||||
// Prefer the mounted save dir the last bridge call reported: the
|
||||
// recomputation below can name a different volume than the one LOVE
|
||||
|
||||
+11
-13
@@ -552,7 +552,7 @@ function Game2:learnMoveOn(mon, moveId, onDone)
|
||||
end
|
||||
if ok then
|
||||
-- data/text/common_3.asm:119
|
||||
return self:say(("%s learned\n%s!"):format(name, moveName),
|
||||
return self:say(Strings("%s learned\n%s!", name, moveName),
|
||||
function() finish(true) end,
|
||||
TextBox.soundOpts(self, "Sfx_DexFanfare5079"))
|
||||
end
|
||||
@@ -560,15 +560,14 @@ function Game2:learnMoveOn(mon, moveId, onDone)
|
||||
local askForget, pickMove, askStop
|
||||
-- DidNotLearnMoveText, then `ld b, 0` (learn.asm:110-113).
|
||||
local function decline()
|
||||
self:say(("%s\ndid not learn\v%s."):format(name, moveName),
|
||||
self:say(Strings("%s\ndid not learn\v%s.", name, moveName),
|
||||
function() finish(false) end)
|
||||
end
|
||||
-- ForgetMove's AskForgetMoveText + YesNoBox (learn.asm:123-127).
|
||||
askForget = function()
|
||||
self.stack:push(TextBox.new(self,
|
||||
("%s is\ntrying to learn\v%s.\fBut %s\ncan't learn more\vthan four moves."
|
||||
.. "\fDelete an older\nmove to make room\vfor %s?")
|
||||
:format(name, moveName, name, moveName),
|
||||
Strings("%s is\ntrying to learn\v%s.\fBut %s\ncan't learn more\vthan four moves.\fDelete an older\nmove to make room\vfor %s?",
|
||||
name, moveName, name, moveName),
|
||||
nil, { choice = function(yes)
|
||||
if yes then return pickMove() end
|
||||
return askStop()
|
||||
@@ -577,7 +576,7 @@ function Game2:learnMoveOn(mon, moveId, onDone)
|
||||
-- StopLearningMoveText, whose NO is `jp c, .loop` (learn.asm:104-108).
|
||||
askStop = function()
|
||||
self.stack:push(TextBox.new(self,
|
||||
("Stop learning\n%s?"):format(moveName), nil,
|
||||
Strings("Stop learning\n%s?", moveName), nil,
|
||||
{ choice = function(yes)
|
||||
if yes then return decline() end
|
||||
return askForget()
|
||||
@@ -600,7 +599,7 @@ function Game2:learnMoveOn(mon, moveId, onDone)
|
||||
-- MoveCantForgetHMText, then `jr .loop` (learn.asm:183-197): the
|
||||
-- question stays up and the list comes back over it.
|
||||
if old and HM_MOVES[old.id] then
|
||||
return self:say("HM moves can't be\nforgotten now.", pushList)
|
||||
return self:say(Strings("HM moves can't be\nforgotten now."), pushList)
|
||||
end
|
||||
self.stack:pop() -- the question the list stood on
|
||||
local oldDef = (self.data.moves or {})[old and old.id]
|
||||
@@ -610,9 +609,8 @@ function Game2:learnMoveOn(mon, moveId, onDone)
|
||||
-- pokemon.move_learned is raised here too.
|
||||
ModRuntime.emit("pokemon.move_learned", { mon = mon, moveId = moveId })
|
||||
-- engine/pokemon/learn.asm:225-229, data/text/common_3.asm:165-173
|
||||
self:say(("1, 2 and…" .. TextBox.PAUSE .. " Poof!" .. TextBox.PAUSE
|
||||
.. "\f%s forgot\n%s.\fAnd…\f%s learned\n%s!")
|
||||
:format(name, oldName, name, moveName),
|
||||
self:say(Strings("1, 2 and…\1 Poof!\1\f%s forgot\n%s.\fAnd…\f%s learned\n%s!",
|
||||
name, oldName, name, moveName),
|
||||
function() finish(true) end,
|
||||
TextBox.soundOpts(self, "Sfx_DexFanfare5079",
|
||||
{ pauseSounds = { "Sfx_SwitchPokemon" } }))
|
||||
@@ -622,7 +620,7 @@ function Game2:learnMoveOn(mon, moveId, onDone)
|
||||
-- MoveAskForgetText, a `done` text: the box stays while the list stands on
|
||||
-- it (learn.asm:136-137).
|
||||
pickMove = function()
|
||||
self.stack:push(TextBox.new(self, "Which move should\nbe forgotten?", nil,
|
||||
self.stack:push(TextBox.new(self, Strings("Which move should\nbe forgotten?"), nil,
|
||||
{ stay = { onShown = pushList } }))
|
||||
end
|
||||
askForget()
|
||||
@@ -657,13 +655,13 @@ function Game2:useFieldItem(itemId)
|
||||
if id == moveId then allowed = true end
|
||||
end
|
||||
if not allowed then
|
||||
self:say(("%s can't learn %s!"):format(
|
||||
self:say(Strings("%s can't learn %s!",
|
||||
require("src.battle.gen2.Mon").displayName(mon), moveName))
|
||||
return
|
||||
end
|
||||
for _, move in ipairs(mon.moves or {}) do
|
||||
if move.id == moveId then
|
||||
self:say(("%s already knows %s!"):format(
|
||||
self:say(Strings("%s already knows %s!",
|
||||
require("src.battle.gen2.Mon").displayName(mon), moveName))
|
||||
return
|
||||
end
|
||||
|
||||
@@ -4,17 +4,45 @@
|
||||
-- under prints/ in the save directory instead, and the caller shows a
|
||||
-- dialog with where it landed. Scaled up 4x so the "print" is legible
|
||||
-- on a modern screen.
|
||||
--
|
||||
-- On Android the app-private prints/ folder is often invisible over USB /
|
||||
-- file managers even when the write succeeded (#2103). After the PNG is
|
||||
-- written, love.system.exportImage copies it into Pictures/Gen1Recomp and
|
||||
-- media-scans the in-app copy so both Gallery and the save folder work.
|
||||
|
||||
local Logger = require("src.core.Logger")
|
||||
|
||||
local Printer = {}
|
||||
|
||||
local SCALE = 4
|
||||
local ANDROID_ALBUM = "Pictures/Gen1Recomp"
|
||||
|
||||
-- After a successful Android gallery export, the public album path shown
|
||||
-- in the "Printed!" dialog. Nil on desktop / when the bridge is absent.
|
||||
Printer.lastAlbumPath = nil
|
||||
|
||||
local function exportToAndroidGallery(relPath)
|
||||
Printer.lastAlbumPath = nil
|
||||
if not (love.system and love.system.getOS
|
||||
and love.system.getOS() == "Android") then
|
||||
return false
|
||||
end
|
||||
local exportImage = love.system.exportImage
|
||||
if type(exportImage) ~= "function" then return false end
|
||||
local ok, exported = pcall(exportImage, relPath)
|
||||
if ok and exported then
|
||||
Printer.lastAlbumPath = ANDROID_ALBUM
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Render drawFn (which draws a w x h GB-pixel image at 0,0) into
|
||||
-- prints/<name>_<stamp>.png. Returns the save-dir-relative path, or nil
|
||||
-- and an error string (headless / no canvas support degrades gracefully).
|
||||
-- On Android a successful gallery copy also sets Printer.lastAlbumPath.
|
||||
function Printer.save(name, w, h, drawFn)
|
||||
Printer.lastAlbumPath = nil
|
||||
if not (love.graphics and love.graphics.newCanvas) then
|
||||
return nil, "no graphics"
|
||||
end
|
||||
@@ -38,7 +66,18 @@ function Printer.save(name, w, h, drawFn)
|
||||
if not encOk then return nil, tostring(err) end
|
||||
Logger.info("printed %s -> %s/%s",
|
||||
name, love.filesystem.getSaveDirectory(), path)
|
||||
exportToAndroidGallery(path)
|
||||
return path
|
||||
end
|
||||
|
||||
-- Second page of the "Printed!" dialog: public album on Android when the
|
||||
-- gallery bridge worked, otherwise the save-dir-relative PNG path.
|
||||
function Printer.savedWhereText(path)
|
||||
local Strings = require("src.core.Strings")
|
||||
if Printer.lastAlbumPath then
|
||||
return Strings("Saved to\n%s.", Printer.lastAlbumPath)
|
||||
end
|
||||
return Strings("Saved as\n%s\vin the save\nfolder.", path)
|
||||
end
|
||||
|
||||
return Printer
|
||||
|
||||
+18
-5
@@ -1440,13 +1440,26 @@ function SaveData.deleteSlot(version, slotId)
|
||||
return deleteSlotIn(version, slotId)
|
||||
end
|
||||
|
||||
-- Test seam: drop the process-global slot cache (and the active cart) so a
|
||||
-- suite can exercise migration/resolution against a freshly injected
|
||||
-- filesystem. Unused by the game, which resolves each scope exactly once per
|
||||
-- boot.
|
||||
function SaveData.resetSlotState()
|
||||
-- Drop the process-global "have we resolved slots for this scope" cache so
|
||||
-- the next listSlots/saveNames re-reads disk (and can migrate a flat legacy
|
||||
-- SAVE into slot1). Pass a version id or cart scope key to invalidate just
|
||||
-- that list; nil clears every scope (test seam / resetSlotState).
|
||||
-- Does not touch carts, seals, options.lua, or any other launcher setup.
|
||||
function SaveData.refreshSlotResolution(scope)
|
||||
if scope ~= nil then
|
||||
if type(scope) ~= "string" or scope == "" then return end
|
||||
activeSlotCache[scope] = nil
|
||||
slotsChecked[scope] = nil
|
||||
return
|
||||
end
|
||||
for k in pairs(activeSlotCache) do activeSlotCache[k] = nil end
|
||||
for k in pairs(slotsChecked) do slotsChecked[k] = nil end
|
||||
end
|
||||
|
||||
-- Test seam: full process-global reset (slot resolution + cart + seal) so a
|
||||
-- suite can exercise migration against a freshly injected filesystem.
|
||||
function SaveData.resetSlotState()
|
||||
SaveData.refreshSlotResolution()
|
||||
freshPlaythrough = nil
|
||||
activeCart, activeCartHash = nil, nil
|
||||
sealBroken = false
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
local Happiness = require("src.core.gen2.Happiness")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local Strings = require("src.core.Strings")
|
||||
|
||||
local ItemEffects = {}
|
||||
|
||||
@@ -72,26 +73,33 @@ local BITTER = {
|
||||
}
|
||||
|
||||
-- _ItemWontHaveEffectText / _ItemCantUseOnEggText (data/text/common_3.asm).
|
||||
ItemEffects.TEXT_NO_EFFECT = "It won't have any\neffect."
|
||||
ItemEffects.TEXT_CANT_USE_ON_EGG = "That can't be used\non an EGG."
|
||||
-- Strings.source keeps these in the catalog harvest even though they are
|
||||
-- declared here and only formatted/looked up at each use site below (#186,
|
||||
-- #245): every use site was a bare table read or a direct :format() call
|
||||
-- until now, which meant a mod's translation catalog never had a chance to
|
||||
-- apply -- every language showed this same English text.
|
||||
ItemEffects.TEXT_NO_EFFECT = Strings.source("It won't have any\neffect.")
|
||||
ItemEffects.TEXT_CANT_USE_ON_EGG = Strings.source("That can't be used\non an EGG.")
|
||||
-- _ItemCantUseOnMonText (data/text/common_3.asm:1265).
|
||||
ItemEffects.TEXT_CANT_USE_ON_MON = "That can't be used\non this #MON."
|
||||
ItemEffects.TEXT_CANT_USE_ON_MON = Strings.source("That can't be used\non this #MON.")
|
||||
-- _PPRestoredText (data/text/common_3.asm).
|
||||
ItemEffects.TEXT_PP_RESTORED = "PP was restored."
|
||||
ItemEffects.TEXT_PP_RESTORED = Strings.source("PP was restored.")
|
||||
-- _PPIsMaxedOutText / _PPsIncreasedText (data/text/common_3.asm).
|
||||
ItemEffects.TEXT_PP_MAXED = "%s's PP\nis maxed out."
|
||||
ItemEffects.TEXT_PP_INCREASED = "%s's PP\nincreased."
|
||||
ItemEffects.TEXT_PP_MAXED = Strings.source("%s's PP\nis maxed out.")
|
||||
ItemEffects.TEXT_PP_INCREASED = Strings.source("%s's PP\nincreased.")
|
||||
|
||||
-- PrintPartyMenuActionText's .MenuActionTexts (engine/pokemon/party_menu.asm),
|
||||
-- keyed by the class GetItemHealingAction resolves. Each is the two rows the
|
||||
-- cart prints: the nickname line, then the fixed line.
|
||||
-- cart prints: the nickname line, then the fixed line. Strings.source for
|
||||
-- the same reason as the constants above: healStatus() below formats these
|
||||
-- directly at each use, so without it a mod's catalog never sees them.
|
||||
local STATUS_TEXT = {
|
||||
psn = "%s's\ncured of poison.",
|
||||
par = "%s's\nrid of paralysis.",
|
||||
brn = "%s's\nburn was healed.",
|
||||
frz = "%s\nwas defrosted.",
|
||||
slp = "%s\nwoke up.",
|
||||
all = "%s's\nhealth returned.",
|
||||
psn = Strings.source("%s's\ncured of poison."),
|
||||
par = Strings.source("%s's\nrid of paralysis."),
|
||||
brn = Strings.source("%s's\nburn was healed."),
|
||||
frz = Strings.source("%s\nwas defrosted."),
|
||||
slp = Strings.source("%s\nwoke up."),
|
||||
all = Strings.source("%s's\nhealth returned."),
|
||||
}
|
||||
|
||||
-- The port's party records spell status several ways (the battle writes the
|
||||
@@ -145,7 +153,7 @@ local function restoreHp(itemId, mon)
|
||||
local amount = ItemEffects.HEAL_HP[itemId]
|
||||
local maxHp = maxHpOf(mon)
|
||||
if fainted(mon) or (mon.hp or 0) >= maxHp then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
local healed = math.min(maxHp, (mon.hp or 0) + amount)
|
||||
local gained = healed - (mon.hp or 0)
|
||||
@@ -157,7 +165,7 @@ local function restoreHp(itemId, mon)
|
||||
used = true,
|
||||
-- data/text/common_1.asm:30
|
||||
-- home/text.asm:772
|
||||
text = ("%s\nrecovered %dHP!"):format(monName(mon), gained),
|
||||
text = Strings("%s\nrecovered %dHP!", monName(mon), gained),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -181,23 +189,23 @@ end
|
||||
-- confusion arm reads wPlayerSubStatus3, which does not exist out of battle.
|
||||
local function healStatus(itemId, mon, class, data)
|
||||
if fainted(mon) then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
local have = ItemEffects.healClassOf(mon.status, data)
|
||||
if not have or (class ~= "all" and have ~= class) then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
clearStatus(mon)
|
||||
bitterHappiness(itemId, mon)
|
||||
local shape = STATUS_TEXT[class == "all" and "all" or have]
|
||||
return { used = true, text = shape:format(monName(mon)) }
|
||||
return { used = true, text = Strings(shape, monName(mon)) }
|
||||
end
|
||||
|
||||
-- RevivePokemon: only a fainted mon accepts; REVIVE stands it up at half max
|
||||
-- HP (ReviveHalfHP's `srl d / rr e`), the other two at full.
|
||||
local function revive(itemId, mon)
|
||||
if not fainted(mon) then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
local maxHp = maxHpOf(mon)
|
||||
mon.hp = (ItemEffects.REVIVE[itemId] == "half")
|
||||
@@ -206,7 +214,7 @@ local function revive(itemId, mon)
|
||||
bitterHappiness(itemId, mon)
|
||||
return {
|
||||
used = true,
|
||||
text = ("%s\nis revitalized."):format(monName(mon)),
|
||||
text = Strings("%s\nis revitalized.", monName(mon)),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -218,7 +226,7 @@ end
|
||||
-- exactly the new level, for the caller to offer.
|
||||
local function rareCandy(mon, data)
|
||||
if (mon.level or 0) >= Mon.MAX_LEVEL then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
local def = data and data.pokemon and data.pokemon[mon.species]
|
||||
-- through Mon.growthFor, so a growth_rates record a mod registered is the
|
||||
@@ -245,14 +253,18 @@ local function rareCandy(mon, data)
|
||||
learned = learned,
|
||||
-- data/text/common_1.asm:86
|
||||
sfx = "Sfx_DexFanfare5079",
|
||||
text = ("%s grew to\nlevel %d!"):format(monName(mon), newLevel),
|
||||
text = Strings("%s grew to\nlevel %d!", monName(mon), newLevel),
|
||||
}
|
||||
end
|
||||
|
||||
-- engine/items/item_effects.asm:1216 StatStrings.
|
||||
-- engine/items/item_effects.asm:1216 StatStrings. Strings.source per entry,
|
||||
-- the same pattern as the identical stat-name tables in MoveEffects.lua/
|
||||
-- TrainerAI.lua/gen2/Effects.lua/ContestMenu.lua/SummaryMenu.lua, so a mod's
|
||||
-- catalog harvest finds these independently of any other stat-name call site.
|
||||
local VITAMIN_LABEL = {
|
||||
hp = "HEALTH", attack = "ATTACK", defense = "DEFENSE",
|
||||
speed = "SPEED", special = "SPECIAL",
|
||||
hp = Strings.source("HEALTH"), attack = Strings.source("ATTACK"),
|
||||
defense = Strings.source("DEFENSE"), speed = Strings.source("SPEED"),
|
||||
special = Strings.source("SPECIAL"),
|
||||
}
|
||||
|
||||
-- engine/items/item_effects.asm:1149 VitaminEffect.
|
||||
@@ -261,7 +273,7 @@ local function vitamin(itemId, mon, data)
|
||||
mon.statExp = mon.statExp or Mon.newStatExp()
|
||||
local cur = mon.statExp[stat] or 0
|
||||
if cur >= 25600 then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
mon.statExp[stat] = math.min(Mon.MAX_STAT_EXP, cur + 2560)
|
||||
local def = data and data.pokemon and data.pokemon[mon.species]
|
||||
@@ -272,7 +284,7 @@ local function vitamin(itemId, mon, data)
|
||||
Happiness.change(mon, "USEDITEM")
|
||||
return {
|
||||
used = true,
|
||||
text = ("%s's\n%s rose."):format(monName(mon), VITAMIN_LABEL[stat]),
|
||||
text = Strings("%s's\n%s rose.", monName(mon), Strings(VITAMIN_LABEL[stat])),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -392,15 +404,15 @@ end
|
||||
-- The one-call families (everything but PP, which needs a move pick first).
|
||||
-- Returns { used, text, learned?, level? }.
|
||||
function ItemEffects.useOnMon(itemId, mon, data)
|
||||
if not mon then return { used = false, text = ItemEffects.TEXT_NO_EFFECT } end
|
||||
if not mon then return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) } end
|
||||
if mon.isEgg then
|
||||
return { used = false, text = ItemEffects.TEXT_CANT_USE_ON_EGG }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_CANT_USE_ON_EGG) }
|
||||
end
|
||||
local record = ItemEffects.recordFor(itemId, data)
|
||||
-- The PP family has its own entry point; reaching it here is the same
|
||||
-- "nothing happens" the unported items get.
|
||||
if not record or not record.use or record.action == "pp" then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
return record.use({ item = itemId, mon = mon, data = data })
|
||||
end
|
||||
@@ -423,13 +435,13 @@ end
|
||||
-- the ELIXER family (Elixer_RestorePPofAllMoves) walks every slot and counts
|
||||
-- -- one restored move is enough for the item to be spent.
|
||||
function ItemEffects.usePpItem(itemId, mon, slot, data)
|
||||
if not mon then return { used = false, text = ItemEffects.TEXT_NO_EFFECT } end
|
||||
if not mon then return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) } end
|
||||
if mon.isEgg then
|
||||
return { used = false, text = ItemEffects.TEXT_CANT_USE_ON_EGG }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_CANT_USE_ON_EGG) }
|
||||
end
|
||||
local record = ItemEffects.recordFor(itemId, data)
|
||||
if not record or not record.use or record.action ~= "pp" then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
return record.use({ item = itemId, mon = mon, data = data, slot = slot })
|
||||
end
|
||||
@@ -478,9 +490,9 @@ for itemId, row in pairs(ItemEffects.RESTORE_PP) do
|
||||
any = restoreMove(moves[ctx.slot], row.amount)
|
||||
end
|
||||
if not any then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
return { used = true, text = ItemEffects.TEXT_PP_RESTORED }
|
||||
return { used = true, text = Strings(ItemEffects.TEXT_PP_RESTORED) }
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -488,24 +500,24 @@ end
|
||||
record("PP_UP", "pp", function(ctx)
|
||||
local move = (ctx.mon.moves or {})[ctx.slot]
|
||||
if type(move) ~= "table" or not move.id then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
local row = ((ctx.data and ctx.data.moves) or {})[move.id]
|
||||
local name = (row and row.name) or move.id
|
||||
-- constants/pokemon_data_constants.asm:216 PP_UP_MASK.
|
||||
if move.id == "SKETCH" or (move.ppUps or 0) >= 3 then
|
||||
return { used = false, text = ItemEffects.TEXT_PP_MAXED:format(name) }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_PP_MAXED, name) }
|
||||
end
|
||||
local base = (row and row.pp) or move.maxPp
|
||||
if not base then
|
||||
return { used = false, text = ItemEffects.TEXT_PP_MAXED:format(name) }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_PP_MAXED, name) }
|
||||
end
|
||||
-- engine/items/item_effects.asm:2736 ComputeMaxPP.
|
||||
local bonus = math.min(math.floor(base / 5), 7)
|
||||
move.ppUps = (move.ppUps or 0) + 1
|
||||
move.maxPp = base + move.ppUps * bonus
|
||||
move.pp = (move.pp or 0) + bonus
|
||||
return { used = true, text = ItemEffects.TEXT_PP_INCREASED:format(name) }
|
||||
return { used = true, text = Strings(ItemEffects.TEXT_PP_INCREASED, name) }
|
||||
end)
|
||||
|
||||
for itemId in pairs(ItemEffects.VITAMIN) do
|
||||
@@ -522,12 +534,12 @@ for _, itemId in ipairs({ "SUN_STONE", "MOON_STONE", "FIRE_STONE",
|
||||
"THUNDERSTONE", "WATER_STONE", "LEAF_STONE" }) do
|
||||
record(itemId, "stone", function(ctx)
|
||||
if ctx.mon.item == "EVERSTONE" then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) }
|
||||
end
|
||||
local Evolution = require("src.core.gen2.Evolution")
|
||||
local entry = Evolution.checkMon(ctx.data, ctx.mon,
|
||||
{ force = true, item = ctx.item })
|
||||
if not entry then return { used = false, text = ItemEffects.TEXT_NO_EFFECT } end
|
||||
if not entry then return { used = false, text = Strings(ItemEffects.TEXT_NO_EFFECT) } end
|
||||
return { used = true, evolution = entry }
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -249,6 +249,10 @@ end
|
||||
|
||||
function LauncherView.touchpressed(imp, id, x, y)
|
||||
if not imp._flex then return end
|
||||
-- Leftover finger from EXIT GAME / Close editor: do not start a tap.
|
||||
if imp._ignoreTouch and imp._ignoreTouch[tostring(id)] then
|
||||
return
|
||||
end
|
||||
imp._touchAt = imp._touchAt or {}
|
||||
imp._touchAt[tostring(id)] = {
|
||||
x = x, y = y, started = love.timer.getTime(),
|
||||
@@ -284,8 +288,21 @@ end
|
||||
-- A tap dispatches on RELEASE (not press) so a drag can disqualify it.
|
||||
function LauncherView.touchreleased(imp, id, x, y)
|
||||
if not imp._flex then return end
|
||||
local start = imp._touchAt and imp._touchAt[tostring(id)]
|
||||
if imp._touchAt then imp._touchAt[tostring(id)] = nil end
|
||||
local tid = tostring(id)
|
||||
if imp._ignoreTouch and imp._ignoreTouch[tid] then
|
||||
imp._ignoreTouch[tid] = nil
|
||||
if imp._touchAt then imp._touchAt[tid] = nil end
|
||||
imp._suppressMouseUntil = love.timer.getTime() + ACT_DEDUP
|
||||
return
|
||||
end
|
||||
local start = imp._touchAt and imp._touchAt[tid]
|
||||
if imp._touchAt then imp._touchAt[tid] = nil end
|
||||
-- A release with no matching press is leftover from the previous host
|
||||
-- (game / save editor), not a launcher tap (#2079).
|
||||
if not start then
|
||||
imp._suppressMouseUntil = love.timer.getTime() + ACT_DEDUP
|
||||
return
|
||||
end
|
||||
if start and (start.dragged or start.longPressed) then
|
||||
-- Suppress the mouse click SDL will synthesize for this same gesture.
|
||||
imp._suppressClickUntil = love.timer.getTime() + ACT_DEDUP
|
||||
|
||||
@@ -3176,10 +3176,48 @@ function RomImporter:prepareOverlayHandoff()
|
||||
end
|
||||
end
|
||||
|
||||
-- EXIT GAME / Close editor leave the confirming finger still down, often
|
||||
-- sitting where Import Save is drawn. The launcher must not treat that
|
||||
-- leftover hold as a new press once a short suppress window expires
|
||||
-- (#2079): update() would then arm on the still-down pointer and the
|
||||
-- later lift would open the system file picker. Swallow this gesture
|
||||
-- (held mouse, already-down touches) and debounce clicks briefly.
|
||||
local RETURN_POINTER_HOLD = 0.5
|
||||
|
||||
function RomImporter:ignoreReturningPointer()
|
||||
local now = 0
|
||||
if love.timer and love.timer.getTime then
|
||||
now = love.timer.getTime()
|
||||
end
|
||||
self._suppressClickUntil = now + RETURN_POINTER_HOLD
|
||||
self._suppressMouseUntil = now + RETURN_POINTER_HOLD
|
||||
self._clickPt = nil
|
||||
self._mouseAt = nil
|
||||
self._touchAt = nil
|
||||
-- Already-down is not a rising edge. Always mark the poll as held so
|
||||
-- the first frame after remount cannot mint a press from a leftover.
|
||||
self._prevMouseDown = true
|
||||
local ignore = {}
|
||||
if love.touch and love.touch.getTouches then
|
||||
local ok, ids = pcall(love.touch.getTouches)
|
||||
if ok and type(ids) == "table" then
|
||||
for i = 1, #ids do
|
||||
ignore[tostring(ids[i])] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
self._ignoreTouch = ignore
|
||||
if package.loaded["src.ui.kit.Kit"] then
|
||||
local Kit = require("src.ui.kit.Kit")
|
||||
if Kit.dragEnd then pcall(Kit.dragEnd) end
|
||||
end
|
||||
end
|
||||
|
||||
-- After an overlay closes: re-arm the pad cursor when a stick is already
|
||||
-- connected so NX / handhelds are not stranded without a pointer until the
|
||||
-- next stick bump (same class of bug as opening Touch Controls).
|
||||
function RomImporter:resumeAfterOverlay()
|
||||
self:ignoreReturningPointer()
|
||||
if not self.launcher then return end
|
||||
if not (love.joystick and love.joystick.getJoystickCount) then return end
|
||||
if love.joystick.getJoystickCount() <= 0 then return end
|
||||
|
||||
+2
-2
@@ -340,8 +340,8 @@ local function printBox(game)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end)
|
||||
game.stack:push(TextBox.new(game, saved
|
||||
and Strings("Printed BOX %d!\fSaved as\n%s\vin the save\nfolder.",
|
||||
game.save.currentBox or 1, saved)
|
||||
and (Strings("Printed BOX %d!\f", game.save.currentBox or 1)
|
||||
.. Printer.savedWhereText(saved))
|
||||
or Strings("Printer error!\n%s", tostring(err))))
|
||||
end
|
||||
|
||||
|
||||
@@ -237,8 +237,8 @@ local function chooseEntry(item, dexList)
|
||||
DexEntryMenu.render(game, def, ok and sprite or nil, false)
|
||||
end)
|
||||
game.stack:push(TextBox.new(game, saved
|
||||
and Strings("Printed %s's\ndata!\fSaved as\n%s\vin the save\nfolder.",
|
||||
def.name, saved)
|
||||
and (Strings("Printed %s's\ndata!\f", def.name)
|
||||
.. Printer.savedWhereText(saved))
|
||||
or Strings("Printer error!\n%s", tostring(err))))
|
||||
end }
|
||||
end
|
||||
|
||||
@@ -3909,7 +3909,7 @@ function BattleState:useItem(itemId)
|
||||
if not ok then
|
||||
-- _ItemWontHaveEffectText's own `line` break, the same one
|
||||
-- ItemEffects.TEXT_NO_EFFECT carries (data/text/common_3.asm).
|
||||
self.message = ItemEffects.TEXT_NO_EFFECT
|
||||
self.message = Strings(ItemEffects.TEXT_NO_EFFECT)
|
||||
self.messageTimer = MESSAGE_FRAMES
|
||||
self.phase = "resolving"
|
||||
return
|
||||
@@ -4014,7 +4014,7 @@ function BattleState:cureBattleConfusion(itemId)
|
||||
local mon = self.battle.player
|
||||
local state = mon and self.battle:volatile(mon)
|
||||
if not (state and state.confuseCount) then
|
||||
self.message = oneLine(ItemEffects.TEXT_NO_EFFECT)
|
||||
self.message = oneLine(Strings(ItemEffects.TEXT_NO_EFFECT))
|
||||
self.messageTimer = MESSAGE_FRAMES
|
||||
self.phase = "resolving"
|
||||
return
|
||||
|
||||
@@ -76,7 +76,7 @@ function BattleTowerMenu.new(game, opts)
|
||||
-- `ld a, $1 / ld [wcd4f], a` (../pokecrystal/mobile/mobile_46.asm:1152-1153)
|
||||
self.cursor = 1
|
||||
self.phase = "pick"
|
||||
self.message = PICK_TEXT
|
||||
self.message = Strings(PICK_TEXT)
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -116,17 +116,17 @@ function BattleTowerMenu:confirm()
|
||||
if not row then
|
||||
-- ../pokecrystal/mobile/mobile_46.asm:1291-1303 `.asm_118a3c`
|
||||
self.phase = "quit"
|
||||
self.message = QUIT_TEXT
|
||||
self.message = Strings(QUIT_TEXT)
|
||||
self.yes = true
|
||||
return
|
||||
end
|
||||
if BattleTower.levelCheck(self.party, row.group) then
|
||||
return self:refuse(TOPS_TEXT)
|
||||
return self:refuse(Strings(TOPS_TEXT))
|
||||
end
|
||||
local uber = BattleTower.ubersCheck(self.party, row.group)
|
||||
if uber then
|
||||
local name = (self.monName and self.monName(uber)) or uber
|
||||
return self:refuse(string.format(UBER_TEXT, name))
|
||||
return self:refuse(Strings(UBER_TEXT, name))
|
||||
end
|
||||
self:finish(row.group)
|
||||
end
|
||||
@@ -147,7 +147,7 @@ function BattleTowerMenu:updatePick()
|
||||
elseif input:wasPressed("b") then
|
||||
self:playSfx("Sfx_ReadText2")
|
||||
self.phase = "quit"
|
||||
self.message = QUIT_TEXT
|
||||
self.message = Strings(QUIT_TEXT)
|
||||
self.yes = true
|
||||
end
|
||||
end
|
||||
@@ -165,12 +165,12 @@ function BattleTowerMenu:updateQuit()
|
||||
self:playSfx("Sfx_ReadText2")
|
||||
if self.yes then return self:finish(nil) end
|
||||
self.phase = "pick"
|
||||
self.message = PICK_TEXT
|
||||
self.message = Strings(PICK_TEXT)
|
||||
self.cursor = 1
|
||||
elseif input:wasPressed("b") then
|
||||
self:playSfx("Sfx_ReadText2")
|
||||
self.phase = "pick"
|
||||
self.message = PICK_TEXT
|
||||
self.message = Strings(PICK_TEXT)
|
||||
self.cursor = 1
|
||||
end
|
||||
end
|
||||
@@ -181,7 +181,7 @@ function BattleTowerMenu:update(_dt)
|
||||
self.wait = (self.wait or 0) - 1
|
||||
if self.wait > 0 then return end
|
||||
self.phase = "pick"
|
||||
self.message = PICK_TEXT
|
||||
self.message = Strings(PICK_TEXT)
|
||||
self.cursor = 1
|
||||
return
|
||||
end
|
||||
@@ -198,15 +198,15 @@ function BattleTowerMenu:drawPanel()
|
||||
end
|
||||
if self.phase == "quit" then
|
||||
Chrome.box(YN_X, YN_Y, YN_W, YN_H)
|
||||
Chrome.print(YES_LABEL, YN_TEXT_X, YES_Y)
|
||||
Chrome.print(NO_LABEL, YN_TEXT_X, NO_Y)
|
||||
Chrome.print(Strings(YES_LABEL), YN_TEXT_X, YES_Y)
|
||||
Chrome.print(Strings(NO_LABEL), YN_TEXT_X, NO_Y)
|
||||
Chrome.cursor(YN_TEXT_X - 1, self.yes and YES_Y or NO_Y)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
return
|
||||
end
|
||||
Chrome.box(PICK_X, PICK_Y, PICK_W, PICK_H)
|
||||
local row = self.rows[self.cursor]
|
||||
Chrome.print(row and BattleTowerMenu.levelLabel(row.group) or CANCEL_LABEL,
|
||||
Chrome.print(row and BattleTowerMenu.levelLabel(row.group) or Strings(CANCEL_LABEL),
|
||||
ROW_X, ROW_Y)
|
||||
Chrome.print(UP_ARROW, ARROW_X, UP_Y)
|
||||
Chrome.print(DOWN_ARROW, ARROW_X, DOWN_Y)
|
||||
|
||||
@@ -396,7 +396,7 @@ function PartyMenu:finishSoftboiled()
|
||||
local before, after =
|
||||
FieldMoves.softboiledTransfer(user, target, self.softboiledCost or 0)
|
||||
if not before then
|
||||
self:showItemResult(slot, { text = ItemEffects.TEXT_CANT_USE_ON_MON })
|
||||
self:showItemResult(slot, { text = Strings(ItemEffects.TEXT_CANT_USE_ON_MON) })
|
||||
return
|
||||
end
|
||||
self.softboiledFrom, self.softboiledCost = nil, nil
|
||||
|
||||
@@ -825,8 +825,7 @@ function PokedexMenu:printEntry()
|
||||
-- Word for word what Yellow's PRNT says (src/ui/PokedexMenu.lua), so the two
|
||||
-- generations share one catalog entry and a translation covers both.
|
||||
local text = saved
|
||||
and Strings("Printed %s's\ndata!\fSaved as\n%s\vin the save\nfolder.",
|
||||
name, tostring(saved))
|
||||
and (Strings("Printed %s's\ndata!\f", name) .. Printer.savedWhereText(saved))
|
||||
or Strings("Printer error!\n%s", tostring(err))
|
||||
if self.game and self.game.stack then
|
||||
self.game.stack:push(TextBox.new(self.game, text))
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
-- #2079: EXIT GAME / Close editor leave the confirming finger down over
|
||||
-- Import Save. After remount the launcher must not treat that leftover as
|
||||
-- a new tap (the file picker opening a few seconds later is that lift).
|
||||
-- luajit tests/engine/launcher_return_pointer_bug2079.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local clock = 1000
|
||||
love.timer.getTime = function() return clock end
|
||||
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
local LauncherView = require("src.import.LauncherView")
|
||||
|
||||
local function launcher()
|
||||
return setmetatable({
|
||||
_flex = true,
|
||||
_actAt = {},
|
||||
_uiActions = {},
|
||||
}, RomImporter)
|
||||
end
|
||||
|
||||
-- ------- ignoreReturningPointer swallows a leftover hold
|
||||
|
||||
do
|
||||
local down = true
|
||||
love.mouse.isDown = function() return down end
|
||||
love.mouse.getPosition = function() return 280, 640 end
|
||||
local imp = launcher()
|
||||
imp:ignoreReturningPointer()
|
||||
check(imp._prevMouseDown == true,
|
||||
"returning marks the pointer already down so the first poll is not a press")
|
||||
check(imp._suppressClickUntil > clock,
|
||||
"and raises a click debounce over the leftover gesture")
|
||||
check(imp._clickPt == nil, "any queued click is cleared")
|
||||
|
||||
LauncherView.update(imp, 0.016)
|
||||
check(imp._mouseAt == nil, "the leftover hold does not arm a drag/tap")
|
||||
check(imp._clickPt == nil)
|
||||
|
||||
clock = 1000.7
|
||||
LauncherView.update(imp, 0.016)
|
||||
check(imp._mouseAt == nil,
|
||||
"a still-down finger after the debounce window still does not arm (#2079)")
|
||||
check(imp._clickPt == nil)
|
||||
|
||||
down = false
|
||||
LauncherView.update(imp, 0.016)
|
||||
check(imp._clickPt == nil, "lifting the leftover finger is not a launcher tap")
|
||||
check(imp._prevMouseDown == false, "the poll then sees the pointer idle")
|
||||
|
||||
down = true
|
||||
LauncherView.update(imp, 0.016)
|
||||
check(imp._mouseAt ~= nil, "a new press after the leftover lifts arms normally")
|
||||
down = false
|
||||
LauncherView.update(imp, 0.016)
|
||||
check(imp._clickPt ~= nil, "and that new press+release is a tap")
|
||||
love.mouse.isDown = nil
|
||||
love.mouse.getPosition = nil
|
||||
end
|
||||
|
||||
-- ------- unmatched / leftover touch release is not a tap
|
||||
|
||||
do
|
||||
clock = 2000
|
||||
local imp = launcher()
|
||||
LauncherView.touchreleased(imp, "finger", 280, 640)
|
||||
check(imp._clickPt == nil,
|
||||
"a touch up with no matching press does not click Import Save")
|
||||
end
|
||||
|
||||
do
|
||||
clock = 3000
|
||||
love.touch = love.touch or {}
|
||||
love.touch.getTouches = function() return { "held" } end
|
||||
local imp = launcher()
|
||||
imp:ignoreReturningPointer()
|
||||
check(imp._ignoreTouch.held == true, "already-down touch ids are ignored")
|
||||
LauncherView.touchpressed(imp, "held", 280, 640)
|
||||
check(imp._touchAt == nil or imp._touchAt.held == nil,
|
||||
"the leftover finger does not start a launcher tap")
|
||||
LauncherView.touchreleased(imp, "held", 280, 640)
|
||||
check(imp._clickPt == nil, "and its lift does not click")
|
||||
|
||||
local ran = false
|
||||
LauncherView.queueAction(imp, "import-save", function() ran = true end)
|
||||
eq(#imp._uiActions, 0, "debounce drops Import Save during the hold window")
|
||||
check(not ran)
|
||||
|
||||
clock = 3000.6
|
||||
LauncherView.touchpressed(imp, "fresh", 40, 40)
|
||||
LauncherView.touchreleased(imp, "fresh", 40, 40)
|
||||
check(imp._clickPt ~= nil, "a new finger after the window still taps")
|
||||
love.touch.getTouches = nil
|
||||
end
|
||||
|
||||
-- ------- Close editor / overlay resume always debounces, even with no stick
|
||||
|
||||
do
|
||||
clock = 4000
|
||||
local imp = launcher()
|
||||
imp.launcher = true
|
||||
love.joystick = love.joystick or {}
|
||||
love.joystick.getJoystickCount = function() return 0 end
|
||||
imp:resumeAfterOverlay()
|
||||
check(imp._prevMouseDown == true,
|
||||
"resumeAfterOverlay debounces even when it does not re-arm the pad")
|
||||
check(imp._suppressClickUntil > clock,
|
||||
"so Close editor cannot click Import Save")
|
||||
end
|
||||
|
||||
-- ------- EXIT GAME rebuilds the launcher with the same guard
|
||||
|
||||
do
|
||||
local main = assert(io.open("main.lua")):read("*a")
|
||||
local body = main:match("local function returnToLauncher%(opts%)(.-)\nend\n")
|
||||
check(body ~= nil, "main.lua still has returnToLauncher")
|
||||
check(body:find("ignoreReturningPointer", 1, true) ~= nil,
|
||||
"EXIT GAME asks the new launcher to ignore the leftover pointer (#2079)")
|
||||
end
|
||||
|
||||
T.finish("launcher_return_pointer_bug2079")
|
||||
@@ -0,0 +1,108 @@
|
||||
-- #2103: Android diploma / dex prints write under prints/ in the app-private
|
||||
-- save dir, which USB and file managers often show as empty. Printer.save
|
||||
-- must call love.system.exportImage so GameActivity can copy into
|
||||
-- Pictures/Gen1Recomp and media-scan the in-app file.
|
||||
-- luajit tests/engine/printer_android_gallery_bug2103.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local Printer = require("src.core.Printer")
|
||||
|
||||
-- ------- Android: gallery bridge is called with the prints/ path
|
||||
|
||||
do
|
||||
local exported = {}
|
||||
love.system.getOS = function() return "Android" end
|
||||
love.system.exportImage = function(path)
|
||||
exported[#exported + 1] = path
|
||||
return true
|
||||
end
|
||||
|
||||
local path, err = Printer.save("diploma", 40, 36, function() end)
|
||||
check(path ~= nil, "Printer.save still returns the prints/ path (" .. tostring(err) .. ")")
|
||||
check(path:match("^prints/diploma_.*%.png$") ~= nil,
|
||||
"the PNG stays under prints/ for the in-app copy")
|
||||
eq(#exported, 1, "Android asks the native bridge to export the image")
|
||||
eq(exported[1], path, "and hands it the same relative path")
|
||||
eq(Printer.lastAlbumPath, "Pictures/Gen1Recomp",
|
||||
"the Printed dialog can say Pictures/Gen1Recomp")
|
||||
local where = Printer.savedWhereText(path)
|
||||
check(where:find("Pictures/Gen1Recomp", 1, true) ~= nil,
|
||||
"savedWhereText points at the public album")
|
||||
check(where:find("save", 1, true) == nil
|
||||
or where:find("Pictures", 1, true) ~= nil,
|
||||
"and does not claim the invisible save folder alone")
|
||||
end
|
||||
|
||||
-- ------- Desktop: no gallery bridge call
|
||||
|
||||
do
|
||||
local calls = 0
|
||||
love.system.getOS = function() return "Linux" end
|
||||
love.system.exportImage = function()
|
||||
calls = calls + 1
|
||||
return true
|
||||
end
|
||||
local path = Printer.save("dex_1", 40, 36, function() end)
|
||||
check(path ~= nil, "desktop print still writes")
|
||||
eq(calls, 0, "desktop does not call exportImage")
|
||||
eq(Printer.lastAlbumPath, nil, "desktop has no album hint")
|
||||
local where = Printer.savedWhereText(path)
|
||||
check(where:find(path, 1, true) ~= nil,
|
||||
"desktop still names the save-folder PNG")
|
||||
end
|
||||
|
||||
-- ------- Missing bridge: print still succeeds (new .love on old APK)
|
||||
|
||||
do
|
||||
love.system.getOS = function() return "Android" end
|
||||
love.system.exportImage = nil
|
||||
local path = Printer.save("box_1", 40, 36, function() end)
|
||||
check(path ~= nil, "print works when exportImage is absent (old APK)")
|
||||
eq(Printer.lastAlbumPath, nil, "no album hint without the bridge")
|
||||
local where = Printer.savedWhereText(path)
|
||||
check(where:find(path, 1, true) ~= nil,
|
||||
"old APK falls back to the save-folder path in the Printed dialog")
|
||||
check(where:find("Pictures/Gen1Recomp", 1, true) == nil,
|
||||
"and does not claim the gallery copy that never happened")
|
||||
end
|
||||
|
||||
-- ------- Bridge present but returns false / throws: same soft fallback
|
||||
|
||||
do
|
||||
love.system.getOS = function() return "Android" end
|
||||
love.system.exportImage = function() error("JNI method missing") end
|
||||
local path = Printer.save("surf_hiscore", 40, 20, function() end)
|
||||
check(path ~= nil, "print survives a throwing exportImage bridge")
|
||||
eq(Printer.lastAlbumPath, nil, "a failed bridge leaves no album hint")
|
||||
|
||||
love.system.exportImage = function() return false end
|
||||
path = Printer.save("box_2", 40, 20, function() end)
|
||||
check(path ~= nil, "print survives exportImage returning false")
|
||||
eq(Printer.lastAlbumPath, nil, "false from the bridge is not a gallery success")
|
||||
end
|
||||
|
||||
-- ------- Native + Lua surface pins
|
||||
|
||||
do
|
||||
local java = assert(io.open(
|
||||
"mobile/android/love/src/main/java/org/love2d/android/GameActivity.java")):read("*a")
|
||||
check(java:find("exportImageToGallery", 1, true) ~= nil,
|
||||
"GameActivity exports exportImageToGallery")
|
||||
check(java:find("Pictures", 1, true) ~= nil
|
||||
and java:find("Gen1Recomp", 1, true) ~= nil,
|
||||
"gallery target is Pictures/Gen1Recomp")
|
||||
check(java:find("MediaScannerConnection", 1, true) ~= nil,
|
||||
"and media-scans the in-app prints/ copy for USB")
|
||||
|
||||
local wrap = assert(io.open(
|
||||
"mobile/android/love/src/jni/love/src/modules/system/wrap_System.cpp")):read("*a")
|
||||
check(wrap:find('"exportImage"', 1, true) ~= nil,
|
||||
"love.system.exportImage is registered")
|
||||
end
|
||||
|
||||
T.finish("printer_android_gallery_bug2103")
|
||||
@@ -300,6 +300,47 @@ do
|
||||
"saveFilename still resolves the flat name with no slot in use")
|
||||
end
|
||||
|
||||
-- ---------------------------------------------- returnToLauncher slot refresh
|
||||
-- In-process EXIT GAME (Android/iOS) keeps the process-wide slotsChecked
|
||||
-- cache. Without refreshSlotResolution, a flat SAVE written after the
|
||||
-- launcher already resolved "no slots" stays invisible until cold start.
|
||||
|
||||
do
|
||||
local files = fresh()
|
||||
T.eq(#SaveData.listSlots("red"), 0, "session starts with no slots resolved")
|
||||
|
||||
local save = SaveData.newGame()
|
||||
save.player.name = "EXIT"
|
||||
T.check(SaveData.save(save), "in-game SAVE writes the flat legacy file")
|
||||
T.check(files["save.lua"] ~= nil, "flat save.lua exists")
|
||||
|
||||
T.eq(#SaveData.listSlots("red"), 0,
|
||||
"without a refresh the cached 'no slots' answer sticks")
|
||||
T.eq(files["saves/red/slot1.lua"], nil, "and migration has not run yet")
|
||||
|
||||
SaveData.setCart("nuzlocke", "abc")
|
||||
SaveData.refreshSlotResolution("red")
|
||||
T.eq(SaveData.getCart(), "nuzlocke",
|
||||
"refreshSlotResolution leaves the active cart alone")
|
||||
|
||||
local slots = SaveData.listSlots("red")
|
||||
T.eq(#slots, 1, "after refresh, listSlots migrates the flat save")
|
||||
T.eq(slots[1].id, "slot1", "into slot1")
|
||||
T.eq(slots[1].name, "EXIT", "with the saved player name")
|
||||
T.eq(files["save.lua"], nil, "and removes the flat legacy file")
|
||||
T.check(files["saves/red/slot1.lua"] ~= nil, "as saves/red/slot1.lua")
|
||||
end
|
||||
|
||||
do
|
||||
local main = assert(io.open("main.lua")):read("*a")
|
||||
local body = main:match("local function returnToLauncher%(opts%)(.-)\nend\n")
|
||||
T.check(body ~= nil, "main.lua still has returnToLauncher")
|
||||
T.check(body:find("refreshSlotResolution%(currentVersion%)", 1, false) ~= nil,
|
||||
"EXIT GAME refreshes only the version just left")
|
||||
T.check(body:find("resetSlotState", 1, true) == nil,
|
||||
"and does not call resetSlotState (that would clear cart/seal state)")
|
||||
end
|
||||
|
||||
love.filesystem = realFS
|
||||
|
||||
T.finish("save_slots")
|
||||
|
||||
+10
-1
@@ -58,6 +58,9 @@ stub.graphics = {
|
||||
newCanvas = function(w, h)
|
||||
local canvas = setmetatable({ w = w, h = h, setFilter = noop, released = false }, Image)
|
||||
function canvas:release() self.released = true end
|
||||
function canvas:newImageData()
|
||||
return love.image.newImageData(self.w or 8, self.h or 8)
|
||||
end
|
||||
return canvas
|
||||
end,
|
||||
newSpriteBatch = function(image, size)
|
||||
@@ -352,7 +355,13 @@ function ImageData:getPixel() return 0, 0, 0, 1 end
|
||||
function ImageData:setPixel() end
|
||||
function ImageData:mapPixel() end
|
||||
function ImageData:paste() end
|
||||
function ImageData:encode() return { getString = function() return "" end } end
|
||||
function ImageData:encode(format, filename)
|
||||
local bytes = "\137PNG\r\n\26\n"
|
||||
if type(filename) == "string" and love.filesystem and love.filesystem.write then
|
||||
love.filesystem.write(filename, bytes)
|
||||
end
|
||||
return { getString = function() return bytes end }
|
||||
end
|
||||
|
||||
stub.image = {
|
||||
newImageData = function(a, b)
|
||||
|
||||
Reference in New Issue
Block a user