Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev
@@ -512,8 +512,9 @@ gains a field instead of the name gaining a prefix.
|
|||||||
id under Gen 1's `name` key, which is the one payload difference the
|
id under Gen 1's `name` key, which is the one payload difference the
|
||||||
numeric flag space forces.
|
numeric flag space forces.
|
||||||
- *Menus (`src/ui/gen2/`):* `ui.start_menu.items`, `ui.title_menu.items`,
|
- *Menus (`src/ui/gen2/`):* `ui.start_menu.items`, `ui.title_menu.items`,
|
||||||
`ui.options.rows`, `ui.party.submenu`, `ui.naming.grid`, `ui.pc.items`,
|
`ui.options.rows`, `ui.party.submenu`, `ui.party.grid_navigation`,
|
||||||
`ui.list_menu`, `transition.style`. `ui.list_menu` covers Gold's script
|
`ui.naming.grid`, `ui.pc.items`, `ui.list_menu`, `transition.style`.
|
||||||
|
`ui.list_menu` covers Gold's script
|
||||||
menus (`ScriptMenu.lua`); the `Chrome.List` widget the START and title
|
menus (`ScriptMenu.lua`); the `Chrome.List` widget the START and title
|
||||||
menus draw with does not raise it yet, so those two are composed through
|
menus draw with does not raise it yet, so those two are composed through
|
||||||
their own hooks only.
|
their own hooks only.
|
||||||
|
|||||||
@@ -291,6 +291,78 @@ function closeSkinStudio()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function makeLauncher()
|
||||||
|
local RomImporter = require("src.import.RomImporter")
|
||||||
|
local forceImport = os.getenv("POKEPORT_FORCE_IMPORT") == "1"
|
||||||
|
return RomImporter.new(function(version)
|
||||||
|
Importer = nil
|
||||||
|
bootGame(version)
|
||||||
|
end, {
|
||||||
|
launcher = true,
|
||||||
|
forceImport = forceImport,
|
||||||
|
onEditSave = openEditor,
|
||||||
|
onEditTouchControls = openTouchControlsEditor,
|
||||||
|
onOpenSkinStudio = require("src.ui.SkinStudio").available_desktop()
|
||||||
|
and openSkinStudio or nil,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function returnToLauncher()
|
||||||
|
if not Game then return end
|
||||||
|
|
||||||
|
pcall(function() require("src.core.Music").stop() end)
|
||||||
|
pcall(function() require("src.core.Sound").stop() end)
|
||||||
|
if package.loaded["src.core.ChipAudio"] then
|
||||||
|
pcall(package.loaded["src.core.ChipAudio"].shutdown)
|
||||||
|
end
|
||||||
|
if package.loaded["src.core.DiscordPresence"] then
|
||||||
|
pcall(package.loaded["src.core.DiscordPresence"].shutdown)
|
||||||
|
end
|
||||||
|
if package.loaded["src.core.gen2.Clock"] then
|
||||||
|
pcall(package.loaded["src.core.gen2.Clock"].shutdown)
|
||||||
|
end
|
||||||
|
if package.loaded["src.net.Gen1Tls"] then
|
||||||
|
pcall(package.loaded["src.net.Gen1Tls"].shutdown)
|
||||||
|
end
|
||||||
|
if love.audio and love.audio.stop then
|
||||||
|
pcall(love.audio.stop)
|
||||||
|
end
|
||||||
|
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
local currentVersion = GameVersion.get()
|
||||||
|
if currentVersion then
|
||||||
|
require("src.import.CacheFs").unmountVersion(currentVersion)
|
||||||
|
end
|
||||||
|
require("src.core.Data"):unloadGenerated()
|
||||||
|
|
||||||
|
local Runtime = require("src.mods.Runtime")
|
||||||
|
if Runtime.reset then
|
||||||
|
Runtime.reset()
|
||||||
|
end
|
||||||
|
|
||||||
|
Game = nil
|
||||||
|
autopilot = nil
|
||||||
|
driverCo = nil
|
||||||
|
|
||||||
|
local Input = require("src.core.Input")
|
||||||
|
local TouchControls = require("src.core.TouchControls")
|
||||||
|
Input:reset()
|
||||||
|
TouchControls:reset()
|
||||||
|
|
||||||
|
require("src.core.Orientation").applyOptions(
|
||||||
|
require("src.core.SaveData").loadOptions())
|
||||||
|
|
||||||
|
local preload = require("src.mods.LauncherMods").translationStrings()
|
||||||
|
if preload then require("src.core.Strings").load({ strings = preload }) end
|
||||||
|
|
||||||
|
if love.window and love.window.setTitle then
|
||||||
|
local Version = require("src.core.Version")
|
||||||
|
love.window.setTitle(Version.title("Gen 1 Recompilation Project"))
|
||||||
|
end
|
||||||
|
|
||||||
|
Importer = makeLauncher()
|
||||||
|
end
|
||||||
|
|
||||||
function bootGame(version)
|
function bootGame(version)
|
||||||
-- The launcher hands us the chosen game (Red / Blue / Yellow / Gold);
|
-- The launcher hands us the chosen game (Red / Blue / Yellow / Gold);
|
||||||
-- scripted and headless runs fall back to POKEPORT_VERSION, then Red.
|
-- scripted and headless runs fall back to POKEPORT_VERSION, then Red.
|
||||||
@@ -382,7 +454,7 @@ function love.load(args)
|
|||||||
|
|
||||||
-- Apply the persisted Android orientation lock (#592) before the launcher
|
-- Apply the persisted Android orientation lock (#592) before the launcher
|
||||||
-- shows: SDL created the window with no orientation hint, so without this
|
-- shows: SDL created the window with no orientation hint, so without this
|
||||||
-- the launcher would rotate freely until Game:applyOptions runs at boot.
|
-- the launcher would rotate freely until options are applied at boot.
|
||||||
-- No-op on desktop / iOS / when options.lua does not exist yet.
|
-- No-op on desktop / iOS / when options.lua does not exist yet.
|
||||||
require("src.core.Orientation").applyOptions(
|
require("src.core.Orientation").applyOptions(
|
||||||
require("src.core.SaveData").loadOptions())
|
require("src.core.SaveData").loadOptions())
|
||||||
@@ -442,8 +514,8 @@ function love.load(args)
|
|||||||
-- (#767) only pays off if something fills that catalog this early, and no
|
-- (#767) only pays off if something fills that catalog this early, and no
|
||||||
-- restart could: the ordering is the same on every launch. Read the
|
-- restart could: the ordering is the same on every launch. Read the
|
||||||
-- enabled mods' string catalogs -- data only, no entry chunk -- so a
|
-- enabled mods' string catalogs -- data only, no entry chunk -- so a
|
||||||
-- translation reaches the launcher too. Game:load replaces this with the
|
-- translation reaches the launcher too. The active game's loader replaces
|
||||||
-- real merged catalog once a version boots.
|
-- this with the real merged catalog once a version boots.
|
||||||
do
|
do
|
||||||
local preload = require("src.mods.LauncherMods").translationStrings()
|
local preload = require("src.mods.LauncherMods").translationStrings()
|
||||||
if preload then require("src.core.Strings").load({ strings = preload }) end
|
if preload then require("src.core.Strings").load({ strings = preload }) end
|
||||||
@@ -484,17 +556,7 @@ function love.load(args)
|
|||||||
-- by its SHA-1 (GameVersion.forSha1); pressing Play boots that game (Gold
|
-- by its SHA-1 (GameVersion.forSha1); pressing Play boots that game (Gold
|
||||||
-- goes to its own service owner, src/core/Game2.lua -- docs/gold-phase1.md).
|
-- goes to its own service owner, src/core/Game2.lua -- docs/gold-phase1.md).
|
||||||
-- Edit on a save row opens the bundled editor on that slot (openEditor).
|
-- Edit on a save row opens the bundled editor on that slot (openEditor).
|
||||||
Importer = RomImporter.new(function(version)
|
Importer = makeLauncher()
|
||||||
Importer = nil
|
|
||||||
bootGame(version)
|
|
||||||
end, {
|
|
||||||
launcher = true,
|
|
||||||
forceImport = forceImport,
|
|
||||||
onEditSave = openEditor,
|
|
||||||
onEditTouchControls = openTouchControlsEditor,
|
|
||||||
onOpenSkinStudio = require("src.ui.SkinStudio").available_desktop()
|
|
||||||
and openSkinStudio or nil,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.update(dt)
|
function love.update(dt)
|
||||||
@@ -827,6 +889,27 @@ function love.handlers.audioreset()
|
|||||||
if Sound then pcall(Sound.onDeviceReset) end
|
if Sound then pcall(Sound.onDeviceReset) end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function love.handlers.intent_game(version)
|
||||||
|
if type(version) ~= "string" or version == "" then return end
|
||||||
|
version = version:lower():gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
if GameVersion.VERSIONS and not GameVersion.VERSIONS[version] then return end
|
||||||
|
|
||||||
|
local RomImporter = require("src.import.RomImporter")
|
||||||
|
if not RomImporter.isReady(version) then return end
|
||||||
|
|
||||||
|
local currentVersion = GameVersion.get()
|
||||||
|
if Game and currentVersion == version then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if Game then
|
||||||
|
returnToLauncher()
|
||||||
|
end
|
||||||
|
Importer = nil
|
||||||
|
bootGame(version)
|
||||||
|
end
|
||||||
|
|
||||||
function love.touchpressed(id, x, y, dx, dy, pressure)
|
function love.touchpressed(id, x, y, dx, dy, pressure)
|
||||||
if editorMode then
|
if editorMode then
|
||||||
-- iOS synthesizes mousepressed for the primary touch; forwarding here
|
-- iOS synthesizes mousepressed for the primary touch; forwarding here
|
||||||
@@ -1032,11 +1115,16 @@ function love.quit()
|
|||||||
-- docs/modding.md's core.quit_to_launcher entry) may veto returning to
|
-- docs/modding.md's core.quit_to_launcher entry) may veto returning to
|
||||||
-- this Lua launcher via that hook. Vanilla behavior (used when no mod
|
-- this Lua launcher via that hook. Vanilla behavior (used when no mod
|
||||||
-- claims the hook) is exactly the condition below.
|
-- claims the hook) is exactly the condition below.
|
||||||
|
local isAndroid = (love.system and love.system.getOS and love.system.getOS() == "Android")
|
||||||
local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function()
|
local wouldReturnToLauncher = PlatformHooks.quitToLauncher(function()
|
||||||
return Game and not Importer and not quitToLauncher and not scripted
|
return Game and not Importer and not quitToLauncher and not scripted
|
||||||
and not launchedIntoGame
|
and (isAndroid or not launchedIntoGame)
|
||||||
end)
|
end)
|
||||||
if wouldReturnToLauncher then
|
if wouldReturnToLauncher then
|
||||||
|
if isAndroid then
|
||||||
|
returnToLauncher()
|
||||||
|
return true -- abort this quit; the restart lands back in the launcher
|
||||||
|
end
|
||||||
quitToLauncher = true
|
quitToLauncher = true
|
||||||
-- Tell the fresh boot to ignore any boot-straight-into-a-game option this
|
-- Tell the fresh boot to ignore any boot-straight-into-a-game option this
|
||||||
-- once, so the restart really does land in the launcher (#887). A failed
|
-- once, so the restart really does land in the launcher (#887). A failed
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/love"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="${NAME}" >
|
android:label="${NAME}" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.allow_multiple_resumed_activities"
|
android:name="android.allow_multiple_resumed_activities"
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||||
android:label="${NAME}"
|
android:label="${NAME}"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleTask"
|
||||||
android:screenOrientation="${ORIENTATION}"
|
android:screenOrientation="${ORIENTATION}"
|
||||||
android:resizeableActivity="false"
|
android:resizeableActivity="false"
|
||||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
|
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
|
||||||
|
|||||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -3,4 +3,9 @@
|
|||||||
<color name="colorPrimary">#3F51B5</color>
|
<color name="colorPrimary">#3F51B5</color>
|
||||||
<color name="colorPrimaryDark">#303F9F</color>
|
<color name="colorPrimaryDark">#303F9F</color>
|
||||||
<color name="colorAccent">#FF4081</color>
|
<color name="colorAccent">#FF4081</color>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
<color name="shortcut_red">#E53935</color>
|
||||||
|
<color name="shortcut_blue">#1E88E5</color>
|
||||||
|
<color name="shortcut_yellow">#FDD835</color>
|
||||||
|
<color name="shortcut_gold">#D4AF37</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
#include "common/Module.h"
|
#include "common/Module.h"
|
||||||
#include "audio/Audio.h"
|
#include "audio/Audio.h"
|
||||||
#include "audio/openal/Audio.h"
|
#include "audio/openal/Audio.h"
|
||||||
|
#include "event/Event.h"
|
||||||
|
|
||||||
namespace love
|
namespace love
|
||||||
{
|
{
|
||||||
@@ -282,6 +283,70 @@ bool restartApp()
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool updateAppShortcuts(const std::vector<std::string> &versions)
|
||||||
|
{
|
||||||
|
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||||
|
jclass activity = env->FindClass("org/love2d/android/GameActivity");
|
||||||
|
if (activity == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
jmethodID method = env->GetStaticMethodID(activity, "updateAppShortcuts", "([Ljava/lang/String;)Z");
|
||||||
|
if (method == nullptr)
|
||||||
|
{
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
jclass stringClass = env->FindClass("java/lang/String");
|
||||||
|
jobjectArray array = env->NewObjectArray((jsize) versions.size(), stringClass, nullptr);
|
||||||
|
for (size_t i = 0; i < versions.size(); ++i)
|
||||||
|
{
|
||||||
|
jstring jstr = env->NewStringUTF(versions[i].c_str());
|
||||||
|
env->SetObjectArrayElement(array, (jsize) i, jstr);
|
||||||
|
env->DeleteLocalRef(jstr);
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean result = env->CallStaticBooleanMethod(activity, method, array);
|
||||||
|
|
||||||
|
env->DeleteLocalRef(array);
|
||||||
|
env->DeleteLocalRef(stringClass);
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getLaunchGame()
|
||||||
|
{
|
||||||
|
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||||
|
jclass activity = env->FindClass("org/love2d/android/GameActivity");
|
||||||
|
if (activity == nullptr)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
jmethodID method = env->GetStaticMethodID(activity, "getLaunchGame", "()Ljava/lang/String;");
|
||||||
|
if (method == nullptr)
|
||||||
|
{
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring jgame = (jstring) env->CallStaticObjectMethod(activity, method);
|
||||||
|
if (jgame == nullptr)
|
||||||
|
{
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *str = env->GetStringUTFChars(jgame, nullptr);
|
||||||
|
std::string result = (str != nullptr) ? str : "";
|
||||||
|
if (str != nullptr)
|
||||||
|
env->ReleaseStringUTFChars(jgame, str);
|
||||||
|
|
||||||
|
env->DeleteLocalRef(jgame);
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
bool httpDownload(const char *url, const char *destPath, const char *userAgent, const char *accept)
|
bool httpDownload(const char *url, const char *destPath, const char *userAgent, const char *accept)
|
||||||
{
|
{
|
||||||
if (url == nullptr || destPath == nullptr)
|
if (url == nullptr || destPath == nullptr)
|
||||||
@@ -1488,4 +1553,32 @@ Java_org_love2d_android_GameActivity_nativeAudioDeviceChanged(JNIEnv *env, jclas
|
|||||||
love::audio::openal::pushAudioResetEvent();
|
love::audio::openal::pushAudioResetEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void pushGameIntentEvent(const char *game)
|
||||||
|
{
|
||||||
|
auto eventmodule = love::Module::getInstance<love::event::Event>(love::Module::M_EVENT);
|
||||||
|
if (eventmodule == nullptr || game == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::vector<love::Variant> args;
|
||||||
|
args.push_back(love::Variant(std::string(game)));
|
||||||
|
|
||||||
|
love::event::Message *msg = new love::event::Message("intent_game", args);
|
||||||
|
eventmodule->push(msg);
|
||||||
|
msg->release();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT void JNICALL
|
||||||
|
Java_org_love2d_android_GameActivity_nativeOnGameIntent(JNIEnv *env, jclass cls, jstring game)
|
||||||
|
{
|
||||||
|
(void) cls;
|
||||||
|
if (game == nullptr)
|
||||||
|
return;
|
||||||
|
const char *str = env->GetStringUTFChars(game, nullptr);
|
||||||
|
if (str != nullptr)
|
||||||
|
{
|
||||||
|
pushGameIntentEvent(str);
|
||||||
|
env->ReleaseStringUTFChars(game, str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endif // LOVE_ANDROID
|
#endif // LOVE_ANDROID
|
||||||
|
|||||||
@@ -90,6 +90,16 @@ bool syncHealthSteps();
|
|||||||
**/
|
**/
|
||||||
bool restartApp();
|
bool restartApp();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic App Shortcuts: updates Android ShortcutManager with ready game versions.
|
||||||
|
**/
|
||||||
|
bool updateAppShortcuts(const std::vector<std::string> &versions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the game version requested via initial launch Intent (if any).
|
||||||
|
**/
|
||||||
|
std::string getLaunchGame();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocking HTTPS GET into destPath (GameActivity.httpDownload). Android has
|
* Blocking HTTPS GET into destPath (GameActivity.httpDownload). Android has
|
||||||
* no curl binary, so this is the transport src/core/HostShell.lua uses there
|
* no curl binary, so this is the transport src/core/HostShell.lua uses there
|
||||||
|
|||||||
@@ -245,6 +245,25 @@ bool System::restartApp() const
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool System::updateShortcuts(const std::vector<std::string> &versions) const
|
||||||
|
{
|
||||||
|
#ifdef LOVE_ANDROID
|
||||||
|
return love::android::updateAppShortcuts(versions);
|
||||||
|
#else
|
||||||
|
LOVE_UNUSED(versions);
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string System::getLaunchGame() const
|
||||||
|
{
|
||||||
|
#ifdef LOVE_ANDROID
|
||||||
|
return love::android::getLaunchGame();
|
||||||
|
#else
|
||||||
|
return "";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
bool System::httpDownload(const char *url, const char *destPath,
|
bool System::httpDownload(const char *url, const char *destPath,
|
||||||
const char *userAgent, const char *accept) const
|
const char *userAgent, const char *accept) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -143,6 +143,9 @@ public:
|
|||||||
**/
|
**/
|
||||||
virtual bool restartApp() const;
|
virtual bool restartApp() const;
|
||||||
|
|
||||||
|
virtual bool updateShortcuts(const std::vector<std::string> &versions) const;
|
||||||
|
virtual std::string getLaunchGame() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocking HTTPS GET into an absolute host path (Android only; false
|
* Blocking HTTPS GET into an absolute host path (Android only; false
|
||||||
* elsewhere). Android has no curl, which is what every other platform
|
* elsewhere). Android has no curl, which is what every other platform
|
||||||
|
|||||||
@@ -283,6 +283,34 @@ int w_tlsClose(lua_State *L)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int w_updateShortcuts(lua_State *L)
|
||||||
|
{
|
||||||
|
if (!lua_istable(L, 1))
|
||||||
|
return luaL_error(L, "Expected table of game version strings");
|
||||||
|
|
||||||
|
std::vector<std::string> versions;
|
||||||
|
int len = (int) luax_objlen(L, 1);
|
||||||
|
for (int i = 1; i <= len; ++i)
|
||||||
|
{
|
||||||
|
lua_rawgeti(L, 1, i);
|
||||||
|
if (lua_isstring(L, -1))
|
||||||
|
versions.push_back(lua_tostring(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
luax_pushboolean(L, instance()->updateShortcuts(versions));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int w_getLaunchGame(lua_State *L)
|
||||||
|
{
|
||||||
|
std::string game = instance()->getLaunchGame();
|
||||||
|
if (game.empty())
|
||||||
|
lua_pushnil(L);
|
||||||
|
else
|
||||||
|
luax_pushstring(L, game);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static const luaL_Reg functions[] =
|
static const luaL_Reg functions[] =
|
||||||
{
|
{
|
||||||
{ "getOS", w_getOS },
|
{ "getOS", w_getOS },
|
||||||
@@ -297,6 +325,8 @@ static const luaL_Reg functions[] =
|
|||||||
{ "createFile", w_createFile },
|
{ "createFile", w_createFile },
|
||||||
{ "syncHealthSteps", w_syncHealthSteps },
|
{ "syncHealthSteps", w_syncHealthSteps },
|
||||||
{ "restartApp", w_restartApp },
|
{ "restartApp", w_restartApp },
|
||||||
|
{ "updateShortcuts", w_updateShortcuts },
|
||||||
|
{ "getLaunchGame", w_getLaunchGame },
|
||||||
{ "httpDownload", w_httpDownload },
|
{ "httpDownload", w_httpDownload },
|
||||||
{ "httpPost", w_httpPost },
|
{ "httpPost", w_httpPost },
|
||||||
{ "httpRequest", w_httpRequest },
|
{ "httpRequest", w_httpRequest },
|
||||||
|
|||||||
@@ -69,8 +69,11 @@ import android.os.Vibrator;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.view.*;
|
import android.content.pm.ShortcutInfo;
|
||||||
|
import android.content.pm.ShortcutManager;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.drawable.Icon;
|
||||||
|
import android.view.*;
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
import androidx.annotation.Keep;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
@@ -159,6 +162,10 @@ public class GameActivity extends SDLActivity {
|
|||||||
|
|
||||||
private static native void nativeAudioDeviceChanged();
|
private static native void nativeAudioDeviceChanged();
|
||||||
|
|
||||||
|
private static native void nativeOnGameIntent(String game);
|
||||||
|
|
||||||
|
private static String initialGame = "";
|
||||||
|
|
||||||
private AudioManager.OnAudioFocusChangeListener audioFocusListener = null;
|
private AudioManager.OnAudioFocusChangeListener audioFocusListener = null;
|
||||||
private Object audioFocusRequest = null;
|
private Object audioFocusRequest = null;
|
||||||
private Object audioDeviceCallback = null;
|
private Object audioDeviceCallback = null;
|
||||||
@@ -228,6 +235,10 @@ public class GameActivity extends SDLActivity {
|
|||||||
embed = getResources().getBoolean(R.bool.embed);
|
embed = getResources().getBoolean(R.bool.embed);
|
||||||
needToCopyGameInArchive = embed;
|
needToCopyGameInArchive = embed;
|
||||||
|
|
||||||
|
Intent startIntent = getIntent();
|
||||||
|
if (startIntent != null && startIntent.hasExtra("game")) {
|
||||||
|
initialGame = startIntent.getStringExtra("game");
|
||||||
|
}
|
||||||
if (!embed) {
|
if (!embed) {
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
handleIntent(intent);
|
handleIntent(intent);
|
||||||
@@ -261,6 +272,12 @@ public class GameActivity extends SDLActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onNewIntent(Intent intent) {
|
protected void onNewIntent(Intent intent) {
|
||||||
Log.d("GameActivity", "onNewIntent() with " + intent);
|
Log.d("GameActivity", "onNewIntent() with " + intent);
|
||||||
|
if (intent != null && intent.hasExtra("game")) {
|
||||||
|
String game = intent.getStringExtra("game");
|
||||||
|
if (game != null && !game.isEmpty()) {
|
||||||
|
nativeOnGameIntent(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!embed) {
|
if (!embed) {
|
||||||
handleIntent(intent);
|
handleIntent(intent);
|
||||||
resetNative();
|
resetNative();
|
||||||
@@ -673,6 +690,95 @@ public class GameActivity extends SDLActivity {
|
|||||||
return true; // unreachable, but keeps the JNI signature honest
|
return true; // unreachable, but keeps the JNI signature honest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public static String getLaunchGame() {
|
||||||
|
return initialGame != null ? initialGame : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public static boolean updateAppShortcuts(String[] readyVersions) {
|
||||||
|
GameActivity self = (GameActivity) mSingleton;
|
||||||
|
if (self == null) return false;
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < 25) return false;
|
||||||
|
try {
|
||||||
|
Context context = self.getApplicationContext();
|
||||||
|
ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
|
||||||
|
if (shortcutManager == null) return false;
|
||||||
|
|
||||||
|
if (readyVersions == null || readyVersions.length == 0) {
|
||||||
|
shortcutManager.removeAllDynamicShortcuts();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ShortcutInfo> shortcuts = new ArrayList<>();
|
||||||
|
int maxShortcuts = Math.min(readyVersions.length, 4);
|
||||||
|
|
||||||
|
for (int i = 0; i < maxShortcuts; i++) {
|
||||||
|
String ver = readyVersions[i];
|
||||||
|
if (ver == null || ver.isEmpty()) continue;
|
||||||
|
String lower = ver.toLowerCase();
|
||||||
|
String shortLabel;
|
||||||
|
String longLabel;
|
||||||
|
int iconResId;
|
||||||
|
|
||||||
|
switch (lower) {
|
||||||
|
case "red":
|
||||||
|
shortLabel = "Play Red";
|
||||||
|
longLabel = "Play Red";
|
||||||
|
iconResId = context.getResources().getIdentifier("ic_shortcut_red", "drawable", context.getPackageName());
|
||||||
|
break;
|
||||||
|
case "blue":
|
||||||
|
shortLabel = "Play Blue";
|
||||||
|
longLabel = "Play Blue";
|
||||||
|
iconResId = context.getResources().getIdentifier("ic_shortcut_blue", "drawable", context.getPackageName());
|
||||||
|
break;
|
||||||
|
case "yellow":
|
||||||
|
shortLabel = "Play Yellow";
|
||||||
|
longLabel = "Play Yellow";
|
||||||
|
iconResId = context.getResources().getIdentifier("ic_shortcut_yellow", "drawable", context.getPackageName());
|
||||||
|
break;
|
||||||
|
case "gold":
|
||||||
|
shortLabel = "Play Gold";
|
||||||
|
longLabel = "Play Gold";
|
||||||
|
iconResId = context.getResources().getIdentifier("ic_shortcut_gold", "drawable", context.getPackageName());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
String capitalized = lower.substring(0, 1).toUpperCase() + lower.substring(1);
|
||||||
|
shortLabel = "Play " + capitalized;
|
||||||
|
longLabel = "Play " + capitalized;
|
||||||
|
iconResId = context.getResources().getIdentifier("ic_shortcut_" + lower, "drawable", context.getPackageName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iconResId == 0) {
|
||||||
|
iconResId = context.getResources().getIdentifier("ic_launcher_foreground", "drawable", context.getPackageName());
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent intent = new Intent(context, GameActivity.class);
|
||||||
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
|
intent.putExtra("game", lower);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
|
|
||||||
|
ShortcutInfo.Builder builder = new ShortcutInfo.Builder(context, "shortcut_" + lower)
|
||||||
|
.setShortLabel(shortLabel)
|
||||||
|
.setLongLabel(longLabel)
|
||||||
|
.setIntent(intent);
|
||||||
|
|
||||||
|
if (iconResId != 0) {
|
||||||
|
builder.setIcon(Icon.createWithResource(context, iconResId));
|
||||||
|
}
|
||||||
|
|
||||||
|
shortcuts.add(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
shortcutManager.setDynamicShortcuts(shortcuts);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.d("GameActivity", "could not update shortcuts: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocking HTTPS GET into destPath, exposed as love.system.httpDownload
|
* Blocking HTTPS GET into destPath, exposed as love.system.httpDownload
|
||||||
* and used by src/core/HostShell.lua. Android ships no curl binary, so
|
* and used by src/core/HostShell.lua. Android ships no curl binary, so
|
||||||
|
|||||||
@@ -2527,11 +2527,7 @@ end
|
|||||||
|
|
||||||
-- the HUD label drawn in place of the level for a statused mon
|
-- the HUD label drawn in place of the level for a statused mon
|
||||||
function BattleState:statusLabel(mon)
|
function BattleState:statusLabel(mon)
|
||||||
local record = Status.recordFor(self.data.statuses, mon.status)
|
return Status.hudLabelFor(self.data.statuses, mon.status)
|
||||||
if record then
|
|
||||||
return record.hudLabel or record.label or mon.status
|
|
||||||
end
|
|
||||||
return mon.status
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- the one accuracy roll (MoveHitTest), hooked as battle.accuracy
|
-- the one accuracy roll (MoveHitTest), hooked as battle.accuracy
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ end
|
|||||||
-- freeze the English. They are already translatable through the
|
-- freeze the English. They are already translatable through the
|
||||||
-- statuses registry (mod.content.statuses:patch(id, { label = ... })).
|
-- statuses registry (mod.content.statuses:patch(id, { label = ... })).
|
||||||
--
|
--
|
||||||
|
-- Do not add a matching hudLabel = "..." below: Status.hudLabelFor reads
|
||||||
|
-- hudLabel before label, and Registry:patch only overrides the fields a
|
||||||
|
-- mod actually passes, so a label-only translation patch would be
|
||||||
|
-- shadowed by this hudLabel forever. Nothing in this codebase gives
|
||||||
|
-- hudLabel a value different from label -- setting it here only recreates
|
||||||
|
-- that trap for no observed benefit.
|
||||||
|
--
|
||||||
-- The five persistent conditions as records: the beforeMove gauntlet, the
|
-- The five persistent conditions as records: the beforeMove gauntlet, the
|
||||||
-- residual sweep, the inflict text/immunities (StatusRegistry.inflict),
|
-- residual sweep, the inflict text/immunities (StatusRegistry.inflict),
|
||||||
-- the catch/wobble bonuses (Catching.attempt), the HUD label, and the
|
-- the catch/wobble bonuses (Catching.attempt), the HUD label, and the
|
||||||
@@ -61,7 +68,7 @@ end
|
|||||||
-- read these fields, so a mod's sixth status plugs into every consumer.
|
-- read these fields, so a mod's sixth status plugs into every consumer.
|
||||||
Status.RECORDS = {
|
Status.RECORDS = {
|
||||||
SLP = {
|
SLP = {
|
||||||
id = "SLP", label = "SLP", hudLabel = "SLP",
|
id = "SLP", label = "SLP",
|
||||||
catchBonus = 25, shakeBonus = 10,
|
catchBonus = 25, shakeBonus = 10,
|
||||||
beforeMovePriority = 40,
|
beforeMovePriority = 40,
|
||||||
beforeMove = function(battler, _, battle)
|
beforeMove = function(battler, _, battle)
|
||||||
@@ -82,7 +89,7 @@ Status.RECORDS = {
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
FRZ = {
|
FRZ = {
|
||||||
id = "FRZ", label = "FRZ", hudLabel = "FRZ",
|
id = "FRZ", label = "FRZ",
|
||||||
catchBonus = 25, shakeBonus = 10,
|
catchBonus = 25, shakeBonus = 10,
|
||||||
beforeMovePriority = 30,
|
beforeMovePriority = 30,
|
||||||
beforeMove = function(battler, _, battle)
|
beforeMove = function(battler, _, battle)
|
||||||
@@ -96,7 +103,7 @@ Status.RECORDS = {
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
PSN = {
|
PSN = {
|
||||||
id = "PSN", label = "PSN", hudLabel = "PSN",
|
id = "PSN", label = "PSN",
|
||||||
catchBonus = 12, shakeBonus = 5,
|
catchBonus = 12, shakeBonus = 5,
|
||||||
residual = damageOverTime("_HurtByPoisonText",
|
residual = damageOverTime("_HurtByPoisonText",
|
||||||
Strings.source("%s's\nhurt by poison!")),
|
Strings.source("%s's\nhurt by poison!")),
|
||||||
@@ -112,7 +119,7 @@ Status.RECORDS = {
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
BRN = {
|
BRN = {
|
||||||
id = "BRN", label = "BRN", hudLabel = "BRN",
|
id = "BRN", label = "BRN",
|
||||||
catchBonus = 12, shakeBonus = 5,
|
catchBonus = 12, shakeBonus = 5,
|
||||||
statPenalty = { stat = "attack", div = 2 },
|
statPenalty = { stat = "attack", div = 2 },
|
||||||
residual = damageOverTime("_HurtByBurnText",
|
residual = damageOverTime("_HurtByBurnText",
|
||||||
@@ -124,7 +131,7 @@ Status.RECORDS = {
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
PAR = {
|
PAR = {
|
||||||
id = "PAR", label = "PAR", hudLabel = "PAR",
|
id = "PAR", label = "PAR",
|
||||||
catchBonus = 12, shakeBonus = 5,
|
catchBonus = 12, shakeBonus = 5,
|
||||||
statPenalty = { stat = "speed", div = 4 },
|
statPenalty = { stat = "speed", div = 4 },
|
||||||
beforeMovePriority = 10,
|
beforeMovePriority = 10,
|
||||||
@@ -160,6 +167,14 @@ function Status.recordFor(statuses, id)
|
|||||||
return (statuses or Status.RECORDS)[id]
|
return (statuses or Status.RECORDS)[id]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- the HUD label for a status id: a mod's patched hudLabel/label if the
|
||||||
|
-- merged registry has one, the raw id otherwise (BattleState.statusLabel,
|
||||||
|
-- SummaryMenu.draw and PartyMenu.draw all read this the same way)
|
||||||
|
function Status.hudLabelFor(statuses, id)
|
||||||
|
local record = Status.recordFor(statuses, id)
|
||||||
|
return record and (record.hudLabel or record.label) or id
|
||||||
|
end
|
||||||
|
|
||||||
local function battleStatuses(battle)
|
local function battleStatuses(battle)
|
||||||
return battle and battle.data and battle.data.statuses
|
return battle and battle.data and battle.data.statuses
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ function Game:makeTitleState()
|
|||||||
self:restoreSave(loaded, recovered, { freshBoot = true })
|
self:restoreSave(loaded, recovered, { freshBoot = true })
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
onExit = self.onExit,
|
||||||
})
|
})
|
||||||
title.screenId = title.screenId or "TitleState"
|
title.screenId = title.screenId or "TitleState"
|
||||||
return title
|
return title
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ function Game2:showMainMenu()
|
|||||||
onNewGame = function() self:newGame() end,
|
onNewGame = function() self:newGame() end,
|
||||||
onContinue = function(save) self:continueGame(save) end,
|
onContinue = function(save) self:continueGame(save) end,
|
||||||
onOption = function() self:showOptions(function() self:showMainMenu() end) end,
|
onOption = function() self:showOptions(function() self:showMainMenu() end) end,
|
||||||
|
onExit = self.onExit,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local Version = require("src.core.Version")
|
||||||
|
|
||||||
|
local IssueReport = {}
|
||||||
|
|
||||||
|
local FORM_URL = "https://github.com/bryanthaboi/gen1recomp/issues/new"
|
||||||
|
local TEMPLATE = "bug_report.yml"
|
||||||
|
|
||||||
|
local function clean(value)
|
||||||
|
if value == nil then return nil end
|
||||||
|
local text = tostring(value):gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
if text == "" or text == "unknown" or text == "Unknown" then return nil end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
|
||||||
|
local function call(fn, ...)
|
||||||
|
if type(fn) ~= "function" then return nil end
|
||||||
|
local ok, a, b, c, d, e = pcall(fn, ...)
|
||||||
|
if not ok then return nil end
|
||||||
|
return a, b, c, d, e
|
||||||
|
end
|
||||||
|
|
||||||
|
local function invoke(fn, ...)
|
||||||
|
if type(fn) ~= "function" then return false end
|
||||||
|
local ok, result = pcall(fn, ...)
|
||||||
|
return ok, result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function commandValue(command)
|
||||||
|
if not io or type(io.popen) ~= "function" then return nil end
|
||||||
|
local ok, pipe = pcall(io.popen, command, "r")
|
||||||
|
if not ok or not pipe then return nil end
|
||||||
|
local readOK, value = pcall(pipe.read, pipe, "*l")
|
||||||
|
pcall(pipe.close, pipe)
|
||||||
|
if not readOK then return nil end
|
||||||
|
return clean(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function percentEncode(value)
|
||||||
|
local text = tostring(value or "")
|
||||||
|
return (text:gsub("([^%w%-_%.~])", function(char)
|
||||||
|
return ("%%%02X"):format(char:byte())
|
||||||
|
end))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function formOS(raw)
|
||||||
|
local values = {
|
||||||
|
["OS X"] = "macOS",
|
||||||
|
macOS = "macOS",
|
||||||
|
Windows = "Windows",
|
||||||
|
Linux = "Linux",
|
||||||
|
Android = "Android",
|
||||||
|
iOS = "iOS",
|
||||||
|
NX = "Nintendo Switch",
|
||||||
|
UWP = "Xbox",
|
||||||
|
Xbox = "Xbox",
|
||||||
|
}
|
||||||
|
return values[raw] or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function loveVersion()
|
||||||
|
local major, minor, revision, codename = call(love and love.getVersion)
|
||||||
|
if not major then return "" end
|
||||||
|
local result = tostring(major) .. "." .. tostring(minor) .. "." .. tostring(revision)
|
||||||
|
if codename and codename ~= "" then result = result .. " (" .. tostring(codename) .. ")" end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function appVersion()
|
||||||
|
local version = clean(Version.engine)
|
||||||
|
if not version or version == "0.0.0" or version == "0.0.0-dev" then return "" end
|
||||||
|
return version
|
||||||
|
end
|
||||||
|
|
||||||
|
local function deviceModel(rawOS, system)
|
||||||
|
local model = clean(call(system.getModel))
|
||||||
|
if model then return model end
|
||||||
|
if rawOS == "OS X" or rawOS == "macOS" then
|
||||||
|
return commandValue("sysctl -n hw.model 2>/dev/null")
|
||||||
|
end
|
||||||
|
if rawOS == "Windows" then
|
||||||
|
return commandValue("powershell.exe -NoProfile -NonInteractive -Command \"(Get-CimInstance Win32_ComputerSystem).Model\" 2>NUL")
|
||||||
|
end
|
||||||
|
if rawOS == "Linux" then
|
||||||
|
return commandValue("cat /sys/devices/virtual/dmi/id/product_name 2>/dev/null")
|
||||||
|
or commandValue("cat /sys/devices/virtual/dmi/id/model 2>/dev/null")
|
||||||
|
end
|
||||||
|
if rawOS == "Android" then
|
||||||
|
return commandValue("getprop ro.product.model 2>/dev/null")
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function modRows(context)
|
||||||
|
if context and type(context.mods) == "table" then return context.mods end
|
||||||
|
local ok, LauncherMods = pcall(require, "src.mods.LauncherMods")
|
||||||
|
if ok and LauncherMods and LauncherMods.list then
|
||||||
|
local listed = call(LauncherMods.list)
|
||||||
|
if type(listed) == "table" then return listed end
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function modNames(rows, safeMode)
|
||||||
|
local enabled = {}
|
||||||
|
for _, mod in ipairs(rows or {}) do
|
||||||
|
if type(mod) == "table" then
|
||||||
|
local name = clean(mod.name or mod.id)
|
||||||
|
if name and not safeMode and mod.enabled == true then
|
||||||
|
enabled[#enabled + 1] = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(enabled)
|
||||||
|
return enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
local function metadata(options, context)
|
||||||
|
local system = love and love.system or {}
|
||||||
|
local graphics = love and love.graphics or {}
|
||||||
|
local window = love and love.window or {}
|
||||||
|
local rawOS = clean(call(system.getOS))
|
||||||
|
local model = deviceModel(rawOS, system)
|
||||||
|
local renderer, rendererVersion, _, rendererDevice = call(graphics.getRendererInfo)
|
||||||
|
local width, height = call(graphics.getDimensions)
|
||||||
|
local pixelWidth, pixelHeight = call(graphics.getPixelDimensions)
|
||||||
|
local modeWidth, modeHeight, flags = call(window.getMode)
|
||||||
|
local safeMode = SaveData.isSafeMode(options)
|
||||||
|
local rows = modRows(context or {})
|
||||||
|
local enabledMods = modNames(rows, safeMode)
|
||||||
|
local lines = { "Diagnostics:" }
|
||||||
|
local function add(label, value)
|
||||||
|
value = clean(value)
|
||||||
|
if value then lines[#lines + 1] = "- " .. label .. ": " .. value end
|
||||||
|
end
|
||||||
|
add("Platform", formOS(rawOS))
|
||||||
|
local hardware = model
|
||||||
|
if rendererDevice and rendererDevice ~= model then
|
||||||
|
hardware = hardware and (hardware .. " (" .. rendererDevice .. ")") or rendererDevice
|
||||||
|
end
|
||||||
|
add("Device", hardware)
|
||||||
|
local rendererDetails = clean(renderer)
|
||||||
|
if rendererDetails and clean(rendererVersion) then
|
||||||
|
rendererDetails = rendererDetails .. " " .. clean(rendererVersion)
|
||||||
|
end
|
||||||
|
add("Renderer", rendererDetails)
|
||||||
|
local displayWidth, displayHeight = width or modeWidth or pixelWidth, height or modeHeight or pixelHeight
|
||||||
|
if displayWidth and displayHeight then
|
||||||
|
add("Display", tostring(displayWidth) .. "x" .. tostring(displayHeight))
|
||||||
|
end
|
||||||
|
if pixelWidth and pixelHeight
|
||||||
|
and (pixelWidth ~= displayWidth or pixelHeight ~= displayHeight) then
|
||||||
|
add("Pixel display", tostring(pixelWidth) .. "x" .. tostring(pixelHeight))
|
||||||
|
end
|
||||||
|
if flags and flags.fullscreen == true then add("Fullscreen", "yes") end
|
||||||
|
local version = appVersion()
|
||||||
|
add("App", version ~= "" and Version.title() or "gen1recomp")
|
||||||
|
add("LÖVE", loveVersion())
|
||||||
|
if safeMode then add("Safe mode", "on") end
|
||||||
|
return {
|
||||||
|
rawOS = rawOS,
|
||||||
|
os = formOS(rawOS),
|
||||||
|
device = model,
|
||||||
|
version = version,
|
||||||
|
safeMode = safeMode,
|
||||||
|
enabledMods = enabledMods,
|
||||||
|
metadata = table.concat(lines, "\n"),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function IssueReport.build(options, context)
|
||||||
|
options = options or SaveData.loadOptions()
|
||||||
|
context = context or {}
|
||||||
|
local info = metadata(options, context)
|
||||||
|
local fields = {
|
||||||
|
summary = "",
|
||||||
|
mods_which = #info.enabledMods > 0 and table.concat(info.enabledMods, ", ") or "",
|
||||||
|
version = info.version or "",
|
||||||
|
location = "",
|
||||||
|
screenshot = "",
|
||||||
|
steps = "",
|
||||||
|
expected = "",
|
||||||
|
extra = info.metadata,
|
||||||
|
}
|
||||||
|
local params = {
|
||||||
|
"template=" .. percentEncode(TEMPLATE),
|
||||||
|
"title=" .. percentEncode("bug: replace this with a meaningful title"),
|
||||||
|
}
|
||||||
|
local order = { "summary", "mods_which",
|
||||||
|
"version", "location", "screenshot", "steps", "expected", "extra" }
|
||||||
|
for _, key in ipairs(order) do
|
||||||
|
params[#params + 1] = key .. "=" .. percentEncode(fields[key])
|
||||||
|
end
|
||||||
|
return FORM_URL .. "?" .. table.concat(params, "&"), fields, info
|
||||||
|
end
|
||||||
|
|
||||||
|
function IssueReport.open(options, context)
|
||||||
|
local url = IssueReport.build(options, context)
|
||||||
|
local system = love and love.system or {}
|
||||||
|
local opened, openResult = invoke(system.openURL, url)
|
||||||
|
if opened and openResult ~= false then
|
||||||
|
return true, url
|
||||||
|
end
|
||||||
|
local copied, copyResult = invoke(system.setClipboardText, url)
|
||||||
|
if copied and copyResult ~= false then
|
||||||
|
return true, url, "Issue URL copied to the clipboard."
|
||||||
|
end
|
||||||
|
local filesystem = love and love.filesystem or {}
|
||||||
|
local written, writeResult = invoke(filesystem.write, "issue-report-url.txt", url)
|
||||||
|
if written and writeResult ~= false then
|
||||||
|
return true, url, "Issue URL saved to issue-report-url.txt."
|
||||||
|
end
|
||||||
|
return false, url, "No browser, clipboard, or writable save directory is available for the issue report."
|
||||||
|
end
|
||||||
|
|
||||||
|
IssueReport.percentEncode = percentEncode
|
||||||
|
IssueReport.metadata = metadata
|
||||||
|
|
||||||
|
return IssueReport
|
||||||
@@ -67,10 +67,23 @@ local function argFlag(argv, name)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local cachedIntentGame = nil
|
||||||
|
|
||||||
-- Returns version, slotId (either may be nil). Command line wins over env,
|
-- Returns version, slotId (either may be nil). Command line wins over env,
|
||||||
-- so a shortcut can override a machine-wide default.
|
-- so a shortcut can override a machine-wide default.
|
||||||
function LaunchOptions.resolve(argv)
|
function LaunchOptions.resolve(argv)
|
||||||
|
if cachedIntentGame == nil then
|
||||||
|
if love.system and love.system.getOS and love.system.getOS() == "Android"
|
||||||
|
and love.system.getLaunchGame then
|
||||||
|
cachedIntentGame = normalizeVersion(love.system.getLaunchGame()) or false
|
||||||
|
else
|
||||||
|
cachedIntentGame = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local intentGame = cachedIntentGame or nil
|
||||||
|
|
||||||
local game = normalizeVersion(argValue(argv, "game"))
|
local game = normalizeVersion(argValue(argv, "game"))
|
||||||
|
or intentGame
|
||||||
or normalizeVersion(os.getenv("POKEPORT_GAME"))
|
or normalizeVersion(os.getenv("POKEPORT_GAME"))
|
||||||
or normalizeVersion(os.getenv("POKEPORT_LAUNCH"))
|
or normalizeVersion(os.getenv("POKEPORT_LAUNCH"))
|
||||||
local slot = argValue(argv, "slot") or os.getenv("POKEPORT_SLOT")
|
local slot = argValue(argv, "slot") or os.getenv("POKEPORT_SLOT")
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ function SaveData.defaultOptions()
|
|||||||
-- Native mod enablement is an installation option, not save-slot data.
|
-- Native mod enablement is an installation option, not save-slot data.
|
||||||
-- Missing entries mean enabled so newly installed mods work by default.
|
-- Missing entries mean enabled so newly installed mods work by default.
|
||||||
mods = {},
|
mods = {},
|
||||||
|
safeMode = false,
|
||||||
-- Mods the player forced past the target gate (Loader:_gateGeneration).
|
-- Mods the player forced past the target gate (Loader:_gateGeneration).
|
||||||
-- modsGen2[id][version] = true, one answer per game; a bare `true` is the
|
-- modsGen2[id][version] = true, one answer per game; a bare `true` is the
|
||||||
-- pre-per-game shape and means the Gen 2 games only (see modForced).
|
-- pre-per-game shape and means the Gen 2 games only (see modForced).
|
||||||
@@ -386,6 +387,16 @@ function SaveData.mergeOptions(loaded)
|
|||||||
return opts
|
return opts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function SaveData.isSafeMode(options)
|
||||||
|
return type(options) == "table" and options.safeMode == true
|
||||||
|
end
|
||||||
|
|
||||||
|
function SaveData.setSafeMode(options, enabled)
|
||||||
|
if type(options) ~= "table" then return false end
|
||||||
|
options.safeMode = enabled == true
|
||||||
|
return options.safeMode
|
||||||
|
end
|
||||||
|
|
||||||
function SaveData.encode(data)
|
function SaveData.encode(data)
|
||||||
return SaveSerializer.encode(data)
|
return SaveSerializer.encode(data)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ local function discoverModSchemas(opts)
|
|||||||
-- except experimental mods, which stay off until opted in.
|
-- except experimental mods, which stay off until opted in.
|
||||||
local flag = require("src.core.SaveData").modEnabled(opts, m.id)
|
local flag = require("src.core.SaveData").modEnabled(opts, m.id)
|
||||||
local enabled = flag == true or (flag == nil and not m.experimental)
|
local enabled = flag == true or (flag == nil and not m.experimental)
|
||||||
if enabled then
|
if enabled and not SaveData.isSafeMode(opts) then
|
||||||
local chunk = fs.load(path .. "/" .. m.options_schema)
|
local chunk = fs.load(path .. "/" .. m.options_schema)
|
||||||
if chunk then
|
if chunk then
|
||||||
local okR, schema = pcall(chunk)
|
local okR, schema = pcall(chunk)
|
||||||
@@ -521,9 +521,49 @@ local function modRows(opts, mod)
|
|||||||
return true
|
return true
|
||||||
end }
|
end }
|
||||||
end
|
end
|
||||||
|
for _, row in ipairs(rows) do
|
||||||
|
row.safeModeBlocked = true
|
||||||
|
if row.step then
|
||||||
|
local step = row.step
|
||||||
|
row.step = function(dir)
|
||||||
|
if SaveData.isSafeMode(opts) then return false end
|
||||||
|
return step(dir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if row.setText then
|
||||||
|
local setText = row.setText
|
||||||
|
row.setText = function(text)
|
||||||
|
if SaveData.isSafeMode(opts) then return false end
|
||||||
|
return setText(text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
return rows
|
return rows
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function troubleshootingRows(opts, hooks)
|
||||||
|
return {
|
||||||
|
{
|
||||||
|
label = Strings("SAFE MODE"),
|
||||||
|
actionLabel = function()
|
||||||
|
return SaveData.isSafeMode(opts) and Strings("Turn off") or Strings("Turn on")
|
||||||
|
end,
|
||||||
|
action = function()
|
||||||
|
SaveData.setSafeMode(opts, not SaveData.isSafeMode(opts))
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label = Strings("REPORT ISSUE"),
|
||||||
|
actionLabel = Strings("Report bug"),
|
||||||
|
action = function()
|
||||||
|
if hooks and hooks.reportIssue then hooks.reportIssue(opts) end
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
-- ------- Gen 2 (Gold)
|
-- ------- Gen 2 (Gold)
|
||||||
--
|
--
|
||||||
-- Gold reads NONE of the rows above. Its OPTION screen writes a different
|
-- Gold reads NONE of the rows above. Its OPTION screen writes a different
|
||||||
@@ -704,6 +744,10 @@ function LauncherSettings.open(hooks, version)
|
|||||||
sections[#sections + 1] = { title = mod.name, rows = rows }
|
sections[#sections + 1] = { title = mod.name, rows = rows }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
sections[#sections + 1] = {
|
||||||
|
title = Strings("TROUBLESHOOTING"),
|
||||||
|
rows = troubleshootingRows(opts, hooks),
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
opts = opts,
|
opts = opts,
|
||||||
version = version,
|
version = version,
|
||||||
|
|||||||
@@ -682,6 +682,7 @@ end
|
|||||||
|
|
||||||
local function modStatusColor(status)
|
local function modStatusColor(status)
|
||||||
if status == "ok" then return Strings("Ready"), PAL.green end
|
if status == "ok" then return Strings("Ready"), PAL.green end
|
||||||
|
if status == "safe_mode" then return Strings("Safe mode"), PAL.yellow end
|
||||||
if status == "needs_import" then return Strings("Import required"), PAL.yellow end
|
if status == "needs_import" then return Strings("Import required"), PAL.yellow end
|
||||||
if status == "conflict" then return Strings("Conflict"), PAL.red end
|
if status == "conflict" then return Strings("Conflict"), PAL.red end
|
||||||
-- not a fault: the mod is intact, this is simply not a game it is for
|
-- not a fault: the mod is intact, this is simply not a game it is for
|
||||||
@@ -1814,10 +1815,11 @@ end
|
|||||||
|
|
||||||
-- One compact coloured checkbox for each game. The cartridge colour carries
|
-- One compact coloured checkbox for each game. The cartridge colour carries
|
||||||
-- the game identity even when the row is narrow.
|
-- the game identity even when the row is narrow.
|
||||||
local function modGameCheckbox(x, y, size, checked, game, id)
|
local function modGameCheckbox(x, y, size, checked, game, id, enabled)
|
||||||
local color = cartColor(game)
|
enabled = enabled ~= false
|
||||||
local focused = Kit.focusable(id, x, y, size, size)
|
local color = enabled and cartColor(game) or PAL.steel
|
||||||
local hot = focused or Kit.hover(x, y, size, size)
|
local focused = enabled and Kit.focusable(id, x, y, size, size)
|
||||||
|
local hot = enabled and (focused or Kit.hover(x, y, size, size))
|
||||||
if love.graphics then
|
if love.graphics then
|
||||||
Theme.fillRounded(x, y, size, size, PAL.bg, 1)
|
Theme.fillRounded(x, y, size, size, PAL.bg, 1)
|
||||||
if checked then
|
if checked then
|
||||||
@@ -1829,12 +1831,13 @@ local function modGameCheckbox(x, y, size, checked, game, id)
|
|||||||
hot and Theme.A.focus or Theme.A.hairline, 1)
|
hot and Theme.A.focus or Theme.A.hairline, 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return Kit.press(x, y, size, size) or Kit._activateId == id
|
return enabled and (Kit.press(x, y, size, size) or Kit._activateId == id)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function buildModsPanel(imp, x, y, w, availH, m)
|
local function buildModsPanel(imp, x, y, w, availH, m)
|
||||||
imp:_ensureMods()
|
imp:_ensureMods()
|
||||||
local ModUpdate = require("src.mods.ModUpdate")
|
local ModUpdate = require("src.mods.ModUpdate")
|
||||||
|
local safeMode = imp.safeMode == true
|
||||||
local mods = imp.mods or {}
|
local mods = imp.mods or {}
|
||||||
local gap = m.gap
|
local gap = m.gap
|
||||||
local cy = y
|
local cy = y
|
||||||
@@ -1866,9 +1869,11 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
action = function() imp:chooseMod() end })
|
action = function() imp:chooseMod() end })
|
||||||
btn(imp, place(disableW), cy, disableW, bh, "mods-disable-all", Strings("Disable all"), {
|
btn(imp, place(disableW), cy, disableW, bh, "mods-disable-all", Strings("Disable all"), {
|
||||||
kind = "warn", font = "small",
|
kind = "warn", font = "small",
|
||||||
|
enabled = not safeMode,
|
||||||
action = function() imp:_setAllMods(false) end })
|
action = function() imp:_setAllMods(false) end })
|
||||||
btn(imp, place(enableW), cy, enableW, bh, "mods-enable-all", Strings("Enable all"), {
|
btn(imp, place(enableW), cy, enableW, bh, "mods-enable-all", Strings("Enable all"), {
|
||||||
kind = "good", font = "small",
|
kind = "good", font = "small",
|
||||||
|
enabled = not safeMode,
|
||||||
action = function() imp:_setAllMods(true) end })
|
action = function() imp:_setAllMods(true) end })
|
||||||
btn(imp, place(checkFullW), cy, checkFullW, bh, "mods-check-updates", Strings("Check for updates"), {
|
btn(imp, place(checkFullW), cy, checkFullW, bh, "mods-check-updates", Strings("Check for updates"), {
|
||||||
font = "small",
|
font = "small",
|
||||||
@@ -1917,7 +1922,9 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
|
|
||||||
-- notice line
|
-- notice line
|
||||||
local noticeText, noticeCol
|
local noticeText, noticeCol
|
||||||
if imp.modNotice then
|
if safeMode then
|
||||||
|
noticeText, noticeCol = "Safe mode is on. All mods are disabled. Turn it off in Settings to change mod toggles.", PAL.yellow
|
||||||
|
elseif imp.modNotice then
|
||||||
noticeText = imp.modNotice.text
|
noticeText = imp.modNotice.text
|
||||||
noticeCol = imp.modNotice.ok and PAL.green or PAL.red
|
noticeCol = imp.modNotice.ok and PAL.green or PAL.red
|
||||||
else
|
else
|
||||||
@@ -2043,7 +2050,7 @@ local function buildModsPanel(imp, x, y, w, availH, m)
|
|||||||
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
|
local togKey = "mod-toggle-" .. mod.id .. "-" .. game
|
||||||
if modGameCheckbox(tx, gamesY, togH,
|
if modGameCheckbox(tx, gamesY, togH,
|
||||||
mod.enabledByVersion and mod.enabledByVersion[game] == true,
|
mod.enabledByVersion and mod.enabledByVersion[game] == true,
|
||||||
game, togKey) then
|
game, togKey, not safeMode) then
|
||||||
local version = game
|
local version = game
|
||||||
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
|
queueAction(imp, togKey, function() imp:_toggleMod(mod.id, nil, version) end)
|
||||||
flipped = true
|
flipped = true
|
||||||
@@ -3079,6 +3086,7 @@ local function buildProfilesModal(imp, m)
|
|||||||
btn(imp, place(swBtnW), ry + math.floor(4 * m.s), swBtnW, rowH - math.floor(8 * m.s), "prof-sw-" .. i,
|
btn(imp, place(swBtnW), ry + math.floor(4 * m.s), swBtnW, rowH - math.floor(8 * m.s), "prof-sw-" .. i,
|
||||||
Strings("Switch"), {
|
Strings("Switch"), {
|
||||||
kind = "good", font = "micro",
|
kind = "good", font = "micro",
|
||||||
|
enabled = not imp.safeMode,
|
||||||
action = function()
|
action = function()
|
||||||
LauncherMods.applyProfile(p.name, options)
|
LauncherMods.applyProfile(p.name, options)
|
||||||
if imp._refreshMods then imp:_refreshMods() end
|
if imp._refreshMods then imp:_refreshMods() end
|
||||||
@@ -3105,8 +3113,10 @@ local function buildModHeaderActionsModal(imp, m)
|
|||||||
local btns = {
|
local btns = {
|
||||||
{ label = Strings("Mod profiles..."), action = function() imp._profilesPopup = true end },
|
{ label = Strings("Mod profiles..."), action = function() imp._profilesPopup = true end },
|
||||||
{ label = Strings("Check for updates"), action = function() imp:_syncModUpdateInfo(true) end },
|
{ label = Strings("Check for updates"), action = function() imp:_syncModUpdateInfo(true) end },
|
||||||
{ label = Strings("Enable all mods"), kind = "good", action = function() imp:_setAllMods(true) end },
|
{ label = Strings("Enable all mods"), kind = "good", enabled = not imp.safeMode,
|
||||||
{ label = Strings("Disable all mods"), kind = "warn", action = function() imp:_setAllMods(false) end },
|
action = function() imp:_setAllMods(true) end },
|
||||||
|
{ label = Strings("Disable all mods"), kind = "warn", enabled = not imp.safeMode,
|
||||||
|
action = function() imp:_setAllMods(false) end },
|
||||||
{ label = Strings("Sort mods..."), action = function() imp._sortPopup = "mods" end },
|
{ label = Strings("Sort mods..."), action = function() imp._sortPopup = "mods" end },
|
||||||
}
|
}
|
||||||
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
|
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
|
||||||
@@ -3119,6 +3129,7 @@ local function buildModHeaderActionsModal(imp, m)
|
|||||||
for i, b in ipairs(btns) do
|
for i, b in ipairs(btns) do
|
||||||
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "modheadact-" .. i, b.label, {
|
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "modheadact-" .. i, b.label, {
|
||||||
kind = b.kind or "ghost", font = "small",
|
kind = b.kind or "ghost", font = "small",
|
||||||
|
enabled = b.enabled,
|
||||||
action = function()
|
action = function()
|
||||||
imp._modHeaderActionsPopup = nil
|
imp._modHeaderActionsPopup = nil
|
||||||
b.action()
|
b.action()
|
||||||
@@ -3737,6 +3748,7 @@ end
|
|||||||
|
|
||||||
local function buildSettingsModal(imp, m)
|
local function buildSettingsModal(imp, m)
|
||||||
local model = imp._settings
|
local model = imp._settings
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
local pad = math.floor(18 * m.s)
|
local pad = math.floor(18 * m.s)
|
||||||
local w = math.floor(640 * m.s)
|
local w = math.floor(640 * m.s)
|
||||||
local h = math.floor(math.min(m.H - 2 * m.pad, m.H * 0.9))
|
local h = math.floor(math.min(m.H - 2 * m.pad, m.H * 0.9))
|
||||||
@@ -3826,6 +3838,8 @@ local function buildSettingsModal(imp, m)
|
|||||||
item.header)
|
item.header)
|
||||||
else
|
else
|
||||||
local row = item.row
|
local row = item.row
|
||||||
|
local rowEnabled = not row.safeModeBlocked
|
||||||
|
or not SaveData.isSafeMode(model.opts)
|
||||||
local key = "set-" .. i
|
local key = "set-" .. i
|
||||||
Kit.card(px + pad, ry, pw - 2 * pad, rowH, "hairline")
|
Kit.card(px + pad, ry, pw - 2 * pad, rowH, "hairline")
|
||||||
local ix = px + pad + math.floor(12 * m.s)
|
local ix = px + pad + math.floor(12 * m.s)
|
||||||
@@ -3855,6 +3869,7 @@ local function buildSettingsModal(imp, m)
|
|||||||
ctlY + (m.btnH - Kit.textHeight("small")) / 2, PAL.detail)
|
ctlY + (m.btnH - Kit.textHeight("small")) / 2, PAL.detail)
|
||||||
btn(imp, rx - ew, ctlY, ew, m.btnH,
|
btn(imp, rx - ew, ctlY, ew, m.btnH,
|
||||||
key .. "-edit", Strings("Edit"), { kind = "accent", font = "small",
|
key .. "-edit", Strings("Edit"), { kind = "accent", font = "small",
|
||||||
|
enabled = rowEnabled,
|
||||||
action = function()
|
action = function()
|
||||||
imp._settingsText = { row = row, text = tostring(row.value() or ""),
|
imp._settingsText = { row = row, text = tostring(row.value() or ""),
|
||||||
maxLen = row.editText.maxLen }
|
maxLen = row.editText.maxLen }
|
||||||
@@ -3863,12 +3878,14 @@ local function buildSettingsModal(imp, m)
|
|||||||
elseif row.action then
|
elseif row.action then
|
||||||
-- A plain action row (Reset rebinds, Touch controls): the whole right
|
-- A plain action row (Reset rebinds, Touch controls): the whole right
|
||||||
-- side is one button rather than a value ladder.
|
-- side is one button rather than a value ladder.
|
||||||
local aw = Kit.textWidth("small", row.actionLabel or Strings("Run"))
|
local actionLabel = type(row.actionLabel) == "function"
|
||||||
|
and row.actionLabel() or row.actionLabel or Strings("Run")
|
||||||
|
local aw = Kit.textWidth("small", actionLabel)
|
||||||
+ math.floor(24 * m.s)
|
+ math.floor(24 * m.s)
|
||||||
Kit.text("small", Kit.ellipsize("small", row.label,
|
Kit.text("small", Kit.ellipsize("small", row.label,
|
||||||
labelW or (inner - aw - math.floor(12 * m.s))), ix, labelY, PAL.text)
|
labelW or (inner - aw - math.floor(12 * m.s))), ix, labelY, PAL.text)
|
||||||
btn(imp, rx - aw, ctlY, aw, m.btnH,
|
btn(imp, rx - aw, ctlY, aw, m.btnH,
|
||||||
key .. "-act", row.actionLabel or Strings("Run"), {
|
key .. "-act", actionLabel, {
|
||||||
kind = row.danger and "danger" or "ghost", font = "small",
|
kind = row.danger and "danger" or "ghost", font = "small",
|
||||||
action = function()
|
action = function()
|
||||||
if row.action() ~= false then model.save() end
|
if row.action() ~= false then model.save() end
|
||||||
@@ -3884,12 +3901,14 @@ local function buildSettingsModal(imp, m)
|
|||||||
or valW
|
or valW
|
||||||
btn(imp, rx - stepW, ctlY, stepW, m.btnH,
|
btn(imp, rx - stepW, ctlY, stepW, m.btnH,
|
||||||
key .. "-next", ">", { font = "small",
|
key .. "-next", ">", { font = "small",
|
||||||
|
enabled = rowEnabled,
|
||||||
action = function() if row.step and row.step(1) then model.save() end end })
|
action = function() if row.step and row.step(1) then model.save() end end })
|
||||||
Kit.textCenter("small", Kit.ellipsize("small", tostring(row.value()), vw),
|
Kit.textCenter("small", Kit.ellipsize("small", tostring(row.value()), vw),
|
||||||
rx - stepW - vw, ctlY + (m.btnH - Kit.textHeight("small")) / 2, vw,
|
rx - stepW - vw, ctlY + (m.btnH - Kit.textHeight("small")) / 2, vw,
|
||||||
PAL.heading)
|
PAL.heading)
|
||||||
btn(imp, rx - stepW - vw - stepW, ctlY, stepW,
|
btn(imp, rx - stepW - vw - stepW, ctlY, stepW,
|
||||||
m.btnH, key .. "-prev", "<", { font = "small",
|
m.btnH, key .. "-prev", "<", { font = "small",
|
||||||
|
enabled = rowEnabled,
|
||||||
action = function() if row.step and row.step(-1) then model.save() end end })
|
action = function() if row.step and row.step(-1) then model.save() end end })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -4046,6 +4065,7 @@ local function buildDepResolverModal(imp, m)
|
|||||||
local bw = Kit.textWidth("small", btnLabel) + math.floor(20 * m.s)
|
local bw = Kit.textWidth("small", btnLabel) + math.floor(20 * m.s)
|
||||||
btn(imp, place(bw), ly, bw, chipH, "dep-dis-" .. i, btnLabel, {
|
btn(imp, place(bw), ly, bw, chipH, "dep-dis-" .. i, btnLabel, {
|
||||||
kind = "warn", font = "small",
|
kind = "warn", font = "small",
|
||||||
|
enabled = not imp.safeMode,
|
||||||
action = function()
|
action = function()
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
LauncherMods.setEnabled(dep.id, false, imp.modScope)
|
LauncherMods.setEnabled(dep.id, false, imp.modScope)
|
||||||
|
|||||||
@@ -136,6 +136,9 @@ local VERSION_REQUIRED_FILES_OVERRIDE = {
|
|||||||
-- costs nothing on a current cache and is the difference between every
|
-- costs nothing on a current cache and is the difference between every
|
||||||
-- trainer battle opening with a picture and opening with none.
|
-- trainer battle opening with a picture and opening with none.
|
||||||
"assets/generated/battle/trainers/falkner.png",
|
"assets/generated/battle/trainers/falkner.png",
|
||||||
|
-- BattleStart_TrainerHuds cannot draw its party rows from a cache made
|
||||||
|
-- before the four ball tiles were extracted (#1502).
|
||||||
|
"assets/generated/battle/hud/balls.png",
|
||||||
"assets/generated/audio/programs.bin",
|
"assets/generated/audio/programs.bin",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -326,6 +329,32 @@ function RomImporter.isReady(version)
|
|||||||
return marker == markerFor(version) and allRequiredFilesExist(version)
|
return marker == markerFor(version) and allRequiredFilesExist(version)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function RomImporter.syncAndroidShortcuts(activeVersion)
|
||||||
|
if not (love.system and love.system.getOS and love.system.getOS() == "Android"
|
||||||
|
and love.system.updateShortcuts) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local allVersions = { "red", "blue", "yellow", "gold" }
|
||||||
|
local ready = {}
|
||||||
|
local seen = {}
|
||||||
|
|
||||||
|
if activeVersion and RomImporter.isReady(activeVersion) then
|
||||||
|
table.insert(ready, activeVersion)
|
||||||
|
seen[activeVersion] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, v in ipairs(allVersions) do
|
||||||
|
if not seen[v] and RomImporter.isReady(v) then
|
||||||
|
table.insert(ready, v)
|
||||||
|
seen[v] = true
|
||||||
|
if #ready >= 4 then break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return love.system.updateShortcuts(ready)
|
||||||
|
end
|
||||||
|
|
||||||
-- Load the import manifest for a version and confirm it matches that ROM.
|
-- Load the import manifest for a version and confirm it matches that ROM.
|
||||||
local function sha1(data)
|
local function sha1(data)
|
||||||
local digest = love.data.hash("sha1", data)
|
local digest = love.data.hash("sha1", data)
|
||||||
@@ -1393,6 +1422,7 @@ function RomImporter.new(onComplete, opts)
|
|||||||
self.romName[version] = "pokemon_" .. info.id
|
self.romName[version] = "pokemon_" .. info.id
|
||||||
.. ((info.id == "yellow" or info.id == "gold") and ".gbc" or ".gb")
|
.. ((info.id == "yellow" or info.id == "gold") and ".gbc" or ".gb")
|
||||||
end
|
end
|
||||||
|
RomImporter.syncAndroidShortcuts()
|
||||||
self:_applyLastVersionTab()
|
self:_applyLastVersionTab()
|
||||||
self:_queueBaseRomScan()
|
self:_queueBaseRomScan()
|
||||||
|
|
||||||
@@ -1787,6 +1817,7 @@ function RomImporter:_completeImport(version, prefix, displayName)
|
|||||||
self.workState = "complete"
|
self.workState = "complete"
|
||||||
self.completeVersion = version
|
self.completeVersion = version
|
||||||
self.status = "Ready"
|
self.status = "Ready"
|
||||||
|
RomImporter.syncAndroidShortcuts(version)
|
||||||
-- NX launcher stays put: keep the imports/ cleanup hint instead of
|
-- NX launcher stays put: keep the imports/ cleanup hint instead of
|
||||||
-- overwriting it with a "Starting…" line that never boots from here.
|
-- overwriting it with a "Starting…" line that never boots from here.
|
||||||
if self.launcher and self.isNX and type(displayName) == "string" then
|
if self.launcher and self.isNX and type(displayName) == "string" then
|
||||||
@@ -3598,6 +3629,10 @@ function RomImporter:_openSettings()
|
|||||||
-- The tab rides along: the editor persists the layout into that game's own
|
-- The tab rides along: the editor persists the layout into that game's own
|
||||||
-- option block, and Gold's is not the flat Gen 1 one (#1100).
|
-- option block, and Gold's is not the flat Gen 1 one (#1100).
|
||||||
local hooks = {}
|
local hooks = {}
|
||||||
|
local version = self.tab
|
||||||
|
hooks.reportIssue = function(opts)
|
||||||
|
return self:_reportIssue(opts, version)
|
||||||
|
end
|
||||||
if self.onEditTouchControls then
|
if self.onEditTouchControls then
|
||||||
local version = self.tab
|
local version = self.tab
|
||||||
hooks.editTouchControls = function()
|
hooks.editTouchControls = function()
|
||||||
@@ -3615,11 +3650,13 @@ function RomImporter:_openSettings()
|
|||||||
-- The tab the gear was opened on decides the row set: Gold reads a
|
-- The tab the gear was opened on decides the row set: Gold reads a
|
||||||
-- different option block entirely, and offering it Gen 1's rows meant a
|
-- different option block entirely, and offering it Gen 1's rows meant a
|
||||||
-- dozen controls that changed nothing (see LauncherSettings.gen2Rows).
|
-- dozen controls that changed nothing (see LauncherSettings.gen2Rows).
|
||||||
local version = self.tab
|
|
||||||
local ok, model = pcall(function()
|
local ok, model = pcall(function()
|
||||||
return require("src.import.LauncherSettings").open(hooks, version)
|
return require("src.import.LauncherSettings").open(hooks, version)
|
||||||
end)
|
end)
|
||||||
if ok and model then self._settings = model end
|
if ok and model then
|
||||||
|
self._settings = model
|
||||||
|
self._settingsSafeModeAtOpen = require("src.core.SaveData").isSafeMode(model.opts)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Quit from the launcher's own X. It goes through love.event.quit so main.lua's
|
-- Quit from the launcher's own X. It goes through love.event.quit so main.lua's
|
||||||
@@ -3630,8 +3667,38 @@ function RomImporter:_quitApp()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:_closeSettings()
|
function RomImporter:_closeSettings()
|
||||||
if self._settings then self._settings.save() end
|
local model = self._settings
|
||||||
|
if model then
|
||||||
|
model.save()
|
||||||
|
local safeMode = require("src.core.SaveData").isSafeMode(model.opts)
|
||||||
|
if safeMode ~= self._settingsSafeModeAtOpen then
|
||||||
|
self.mods = nil
|
||||||
|
self.safeMode = safeMode
|
||||||
|
self._modSortCache = nil
|
||||||
|
self._modInfoFetch = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
self._settings = nil
|
self._settings = nil
|
||||||
|
self._settingsSafeModeAtOpen = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function RomImporter:_reportIssue(options, version)
|
||||||
|
local ok, IssueReport = pcall(require, "src.core.IssueReport")
|
||||||
|
if not ok then
|
||||||
|
self.modNotice = { ok = false, text = "Could not prepare the issue report." }
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local opened, url, reason = IssueReport.open(options, {
|
||||||
|
version = version,
|
||||||
|
mods = self.mods,
|
||||||
|
})
|
||||||
|
if not opened then
|
||||||
|
self.modNotice = { ok = false, text = reason or "Could not open the issue report." }
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self._lastIssueReportURL = url
|
||||||
|
if reason then self.modNotice = { ok = true, text = reason } end
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:_commitSettingsText()
|
function RomImporter:_commitSettingsText()
|
||||||
@@ -3971,6 +4038,8 @@ end
|
|||||||
-- so a still list costs nothing after the first paint.
|
-- so a still list costs nothing after the first paint.
|
||||||
function RomImporter:_refreshMods()
|
function RomImporter:_refreshMods()
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
self.safeMode = SaveData.isSafeMode(SaveData.loadOptions())
|
||||||
-- Once per session, ahead of the first listing: pull in any mod the player
|
-- Once per session, ahead of the first listing: pull in any mod the player
|
||||||
-- unzipped beside the executable, which an ordinary (non-portable) install
|
-- unzipped beside the executable, which an ordinary (non-portable) install
|
||||||
-- has no way to read. It happens here rather than behind a button because
|
-- has no way to read. It happens here rather than behind a button because
|
||||||
@@ -4110,6 +4179,10 @@ end
|
|||||||
-- so that game's checkbox and status chips reflect the new resolution.
|
-- so that game's checkbox and status chips reflect the new resolution.
|
||||||
-- Enabling an experimental mod arms a confirmation for that same game.
|
-- Enabling an experimental mod arms a confirmation for that same game.
|
||||||
function RomImporter:_toggleMod(id, confirmed, version)
|
function RomImporter:_toggleMod(id, confirmed, version)
|
||||||
|
if self.safeMode then
|
||||||
|
self.modNotice = { ok = false, text = "Safe mode is active. Turn it off in Settings to change mods." }
|
||||||
|
return
|
||||||
|
end
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
local cur, experimental = false, false
|
local cur, experimental = false, false
|
||||||
for _, m in ipairs(self.mods or {}) do
|
for _, m in ipairs(self.mods or {}) do
|
||||||
@@ -4150,6 +4223,10 @@ end
|
|||||||
-- must not be the way around it. Disabling needs no confirm -- it is the
|
-- must not be the way around it. Disabling needs no confirm -- it is the
|
||||||
-- recovery action, and Delete is the only destructive one on this panel.
|
-- recovery action, and Delete is the only destructive one on this panel.
|
||||||
function RomImporter:_setAllMods(want, confirmed)
|
function RomImporter:_setAllMods(want, confirmed)
|
||||||
|
if self.safeMode then
|
||||||
|
self.modNotice = { ok = false, text = "Safe mode is active. Turn it off in Settings to change mods." }
|
||||||
|
return
|
||||||
|
end
|
||||||
local LauncherMods = require("src.mods.LauncherMods")
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
local ids, experimental = {}, false
|
local ids, experimental = {}, false
|
||||||
for _, m in ipairs(self.mods or {}) do
|
for _, m in ipairs(self.mods or {}) do
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
local ordered = {}
|
local ordered = {}
|
||||||
for _, m in ipairs(manifests) do ordered[#ordered + 1] = m end
|
for _, m in ipairs(manifests) do ordered[#ordered + 1] = m end
|
||||||
table.sort(ordered, function(a, b) return a.id < b.id end)
|
table.sort(ordered, function(a, b) return a.id < b.id end)
|
||||||
|
local safeMode = SaveData.isSafeMode(options)
|
||||||
|
|
||||||
-- the override is one answer per game (SaveData.modForced), the same scope
|
-- the override is one answer per game (SaveData.modForced), the same scope
|
||||||
-- the loader resolves it under
|
-- the loader resolves it under
|
||||||
@@ -304,17 +305,24 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
-- matching the loader -- except experimental mods, which stay off until
|
-- matching the loader -- except experimental mods, which stay off until
|
||||||
-- the player opts in. Scoped through modScope, so this reads exactly what
|
-- the player opts in. Scoped through modScope, so this reads exactly what
|
||||||
-- setEnabled writes and the loader loads for the selected game.
|
-- setEnabled writes and the loader loads for the selected game.
|
||||||
local decided = SaveData.modEnabled(options, m.id, SaveData.modScope(version))
|
if not safeMode then
|
||||||
if decided == nil then decided = not m.experimental end
|
local decided = SaveData.modEnabled(options, m.id, SaveData.modScope(version))
|
||||||
if decided then enabledSet[m.id] = true end
|
if decided == nil then decided = not m.experimental end
|
||||||
|
if decided then enabledSet[m.id] = true end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local out = {}
|
local out = {}
|
||||||
for _, m in ipairs(ordered) do
|
for _, m in ipairs(ordered) do
|
||||||
local enabled = enabledSet[m.id] == true
|
local enabled = not safeMode and enabledSet[m.id] == true
|
||||||
local forced = forcedFor(m.id)
|
local forced = forcedFor(m.id)
|
||||||
local status, detail =
|
local status, detail
|
||||||
statusFor(byId, m.id, enabledSet, enabled, version, forcedFor)
|
if safeMode then
|
||||||
|
status, detail = "safe_mode", "Disabled by safe mode"
|
||||||
|
else
|
||||||
|
status, detail =
|
||||||
|
statusFor(byId, m.id, enabledSet, enabled, version, forcedFor)
|
||||||
|
end
|
||||||
-- nil, not false, when the panel is showing every game at once
|
-- nil, not false, when the panel is showing every game at once
|
||||||
local here = nil
|
local here = nil
|
||||||
if version then here = ModTargets.runsHere(m, version, nil, forced) end
|
if version then here = ModTargets.runsHere(m, version, nil, forced) end
|
||||||
@@ -332,7 +340,8 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
local answers = {}
|
local answers = {}
|
||||||
for _, game in ipairs(GameVersion.ORDER) do
|
for _, game in ipairs(GameVersion.ORDER) do
|
||||||
local answer = SaveData.modEnabled(options, m.id, game)
|
local answer = SaveData.modEnabled(options, m.id, game)
|
||||||
answers[game] = answer == true or (answer == nil and not m.experimental)
|
answers[game] = not safeMode
|
||||||
|
and (answer == true or (answer == nil and not m.experimental))
|
||||||
end
|
end
|
||||||
return answers
|
return answers
|
||||||
end)(),
|
end)(),
|
||||||
@@ -346,6 +355,7 @@ function LauncherMods.deriveList(manifests, options, version)
|
|||||||
-- panel is showing (src/mods/ModTargets.lua)
|
-- panel is showing (src/mods/ModTargets.lua)
|
||||||
targets = ModTargets.chip(m),
|
targets = ModTargets.chip(m),
|
||||||
targetsHere = here,
|
targetsHere = here,
|
||||||
|
safeMode = safeMode,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
return out
|
return out
|
||||||
@@ -586,6 +596,7 @@ end
|
|||||||
-- answer. The loader and the in-game manager use the same scope on next boot.
|
-- answer. The loader and the in-game manager use the same scope on next boot.
|
||||||
function LauncherMods.setEnabled(id, enabled, version)
|
function LauncherMods.setEnabled(id, enabled, version)
|
||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
|
if SaveData.isSafeMode(options) then return false end
|
||||||
SaveData.setModEnabled(options, id, enabled, SaveData.modScope(version))
|
SaveData.setModEnabled(options, id, enabled, SaveData.modScope(version))
|
||||||
SaveData.saveOptions(options)
|
SaveData.saveOptions(options)
|
||||||
LauncherMods.syncActiveProfile(options)
|
LauncherMods.syncActiveProfile(options)
|
||||||
@@ -599,6 +610,7 @@ end
|
|||||||
-- and leaves a half-applied state behind if one of them fails.
|
-- and leaves a half-applied state behind if one of them fails.
|
||||||
function LauncherMods.setAllEnabled(ids, enabled, version)
|
function LauncherMods.setAllEnabled(ids, enabled, version)
|
||||||
local options = SaveData.loadOptions()
|
local options = SaveData.loadOptions()
|
||||||
|
if SaveData.isSafeMode(options) then return false end
|
||||||
local scope = SaveData.modScope(version)
|
local scope = SaveData.modScope(version)
|
||||||
for _, id in ipairs(ids or {}) do
|
for _, id in ipairs(ids or {}) do
|
||||||
if scope then
|
if scope then
|
||||||
@@ -1184,6 +1196,7 @@ end
|
|||||||
|
|
||||||
function LauncherMods.applyProfile(profileName, options)
|
function LauncherMods.applyProfile(profileName, options)
|
||||||
options = options or SaveData.loadOptions()
|
options = options or SaveData.loadOptions()
|
||||||
|
if SaveData.isSafeMode(options) then return false end
|
||||||
local profiles = options.modProfiles or {}
|
local profiles = options.modProfiles or {}
|
||||||
local targetProfile
|
local targetProfile
|
||||||
for _, p in ipairs(profiles) do
|
for _, p in ipairs(profiles) do
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ function Loader.new(opts)
|
|||||||
modInput = {}, modEnv = {}, stepsQueues = {},
|
modInput = {}, modEnv = {}, stepsQueues = {},
|
||||||
fs = (opts and opts.fs) or (love and love.filesystem),
|
fs = (opts and opts.fs) or (love and love.filesystem),
|
||||||
dev = dev,
|
dev = dev,
|
||||||
|
safeMode = false,
|
||||||
-- Which generation this boot is (1 or 2). Fixed at construction: the
|
-- Which generation this boot is (1 or 2). Fixed at construction: the
|
||||||
-- active version is set once in main.lua's bootGame before anything
|
-- active version is set once in main.lua's bootGame before anything
|
||||||
-- builds a loader, and a run never changes generation underneath one.
|
-- builds a loader, and a run never changes generation underneath one.
|
||||||
@@ -300,6 +301,8 @@ end
|
|||||||
function Loader:_loadState()
|
function Loader:_loadState()
|
||||||
self.disabled = {}
|
self.disabled = {}
|
||||||
local options = SaveData.loadOptions(self.fs)
|
local options = SaveData.loadOptions(self.fs)
|
||||||
|
self.safeMode = SaveData.isSafeMode(options)
|
||||||
|
Runtime.safeMode = self.safeMode
|
||||||
local scope = self:_enableScope()
|
local scope = self:_enableScope()
|
||||||
local ids = {}
|
local ids = {}
|
||||||
for id in pairs(options.mods or {}) do ids[id] = true end
|
for id in pairs(options.mods or {}) do ids[id] = true end
|
||||||
@@ -415,6 +418,7 @@ function Loader:_writeOptionSchemas()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Loader:setEnabled(id, enabled)
|
function Loader:setEnabled(id, enabled)
|
||||||
|
if self.safeMode then return false end
|
||||||
if not self.mods[id] then return false end
|
if not self.mods[id] then return false end
|
||||||
self.disabled[id] = not enabled
|
self.disabled[id] = not enabled
|
||||||
self.mods[id].enabled = enabled
|
self.mods[id].enabled = enabled
|
||||||
@@ -427,6 +431,7 @@ end
|
|||||||
-- choice could not be persisted for a game, so the caller does not promise a
|
-- choice could not be persisted for a game, so the caller does not promise a
|
||||||
-- restart will honour it.
|
-- restart will honour it.
|
||||||
function Loader:setGen2Forced(id, forced)
|
function Loader:setGen2Forced(id, forced)
|
||||||
|
if self.safeMode then return false, false end
|
||||||
if not self.mods[id] then return false, false end
|
if not self.mods[id] then return false, false end
|
||||||
self.gen2Forced[id] = forced or nil
|
self.gen2Forced[id] = forced or nil
|
||||||
self:_saveState()
|
self:_saveState()
|
||||||
@@ -1539,6 +1544,9 @@ function Loader:load(data)
|
|||||||
require("src.mods.Builtins").install(self.content, data, self.generation)
|
require("src.mods.Builtins").install(self.content, data, self.generation)
|
||||||
self:_loadState()
|
self:_loadState()
|
||||||
self:_discover()
|
self:_discover()
|
||||||
|
if self.safeMode then
|
||||||
|
for id in pairs(self.mods) do self.disabled[id] = true end
|
||||||
|
end
|
||||||
-- Existing installs stored one shared answer. Once their manifests are
|
-- Existing installs stored one shared answer. Once their manifests are
|
||||||
-- known, split that answer across every game before the next launcher/game
|
-- known, split that answer across every game before the next launcher/game
|
||||||
-- toggle can change one independently. _loadState already used the same
|
-- toggle can change one independently. _loadState already used the same
|
||||||
@@ -1574,7 +1582,7 @@ function Loader:load(data)
|
|||||||
-- the one build where its env var is set.
|
-- the one build where its env var is set.
|
||||||
for id, mod in pairs(self.mods) do
|
for id, mod in pairs(self.mods) do
|
||||||
local envName = mod.manifest.force_enable_env
|
local envName = mod.manifest.force_enable_env
|
||||||
if envName and os.getenv(envName) == "1" then
|
if not self.safeMode and envName and os.getenv(envName) == "1" then
|
||||||
self.disabled[id] = nil
|
self.disabled[id] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1740,6 +1748,7 @@ function Loader:status()
|
|||||||
local manifest = {}
|
local manifest = {}
|
||||||
for key, value in pairs(mod.manifest) do manifest[key] = value end
|
for key, value in pairs(mod.manifest) do manifest[key] = value end
|
||||||
manifest.enabled = mod.enabled ~= false
|
manifest.enabled = mod.enabled ~= false
|
||||||
|
manifest.safeMode = self.safeMode == true
|
||||||
manifest.state = mod.state or (manifest.enabled and "loaded" or "disabled")
|
manifest.state = mod.state or (manifest.enabled and "loaded" or "disabled")
|
||||||
manifest.error = mod.failure
|
manifest.error = mod.failure
|
||||||
-- set instead of `error` when the mod was left out for a reason that is
|
-- set instead of `error` when the mod was left out for a reason that is
|
||||||
|
|||||||
@@ -365,9 +365,13 @@ end
|
|||||||
|
|
||||||
function ManagerState:detailRows(m)
|
function ManagerState:detailRows(m)
|
||||||
local rows = {}
|
local rows = {}
|
||||||
rows[#rows + 1] = { label = m.enabled and "DISABLE" or "ENABLE",
|
if Runtime.safeMode then
|
||||||
action = function() self:beginToggle(m) end }
|
rows[#rows + 1] = { label = "SAFE MODE ACTIVE", inert = true }
|
||||||
if self:schemaFor(m) then
|
else
|
||||||
|
rows[#rows + 1] = { label = m.enabled and "DISABLE" or "ENABLE",
|
||||||
|
action = function() self:beginToggle(m) end }
|
||||||
|
end
|
||||||
|
if not Runtime.safeMode and self:schemaFor(m) then
|
||||||
rows[#rows + 1] = { label = Strings("OPTIONS.."),
|
rows[#rows + 1] = { label = Strings("OPTIONS.."),
|
||||||
action = function() self:openOptions(m) end }
|
action = function() self:openOptions(m) end }
|
||||||
end
|
end
|
||||||
@@ -383,7 +387,8 @@ function ManagerState:detailRows(m)
|
|||||||
-- what this mod does.
|
-- what this mod does.
|
||||||
local loader = self.game.mods
|
local loader = self.game.mods
|
||||||
local version, gen = self:targetGame()
|
local version, gen = self:targetGame()
|
||||||
if loader and loader.setGen2Forced and not ModTargets.supports(m, version, gen) then
|
if loader and loader.setGen2Forced and not Runtime.safeMode
|
||||||
|
and not ModTargets.supports(m, version, gen) then
|
||||||
rows[#rows + 1] = {
|
rows[#rows + 1] = {
|
||||||
label = m.gen2Forced and Strings("DON'T TRY HERE") or Strings("TRY HERE ANYWAY"),
|
label = m.gen2Forced and Strings("DON'T TRY HERE") or Strings("TRY HERE ANYWAY"),
|
||||||
action = function() self:toggleGen2Force(m) end }
|
action = function() self:toggleGen2Force(m) end }
|
||||||
@@ -674,6 +679,10 @@ end
|
|||||||
-- ------- the enable/disable flow
|
-- ------- the enable/disable flow
|
||||||
|
|
||||||
function ManagerState:beginToggle(m)
|
function ManagerState:beginToggle(m)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
if not m then return end
|
if not m then return end
|
||||||
local want = not m.enabled
|
local want = not m.enabled
|
||||||
local loader = self.game.mods
|
local loader = self.game.mods
|
||||||
@@ -711,6 +720,10 @@ end
|
|||||||
-- override is scoped to THIS game, and a boot that cannot name one keeps it in
|
-- override is scoped to THIS game, and a boot that cannot name one keeps it in
|
||||||
-- memory only, which the notice says rather than promising a restart.
|
-- memory only, which the notice says rather than promising a restart.
|
||||||
function ManagerState:toggleGen2Force(m)
|
function ManagerState:toggleGen2Force(m)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
local loader = self.game.mods
|
local loader = self.game.mods
|
||||||
if not (loader and loader.setGen2Forced) then return end
|
if not (loader and loader.setGen2Forced) then return end
|
||||||
local want = not m.gen2Forced
|
local want = not m.gen2Forced
|
||||||
@@ -743,6 +756,10 @@ function ManagerState:enableScope()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ManagerState:commitToggle(apply)
|
function ManagerState:commitToggle(apply)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
local loader = self.game.mods
|
local loader = self.game.mods
|
||||||
local opts = self:optionsTable()
|
local opts = self:optionsTable()
|
||||||
local scope = self:enableScope()
|
local scope = self:enableScope()
|
||||||
@@ -757,6 +774,10 @@ function ManagerState:commitToggle(apply)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ManagerState:discardChanges()
|
function ManagerState:discardChanges()
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
local loader = self.game.mods
|
local loader = self.game.mods
|
||||||
local opts = self:optionsTable()
|
local opts = self:optionsTable()
|
||||||
local scope = self:enableScope()
|
local scope = self:enableScope()
|
||||||
@@ -802,6 +823,10 @@ function ManagerState:persistOptions()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ManagerState:applyProfile(p)
|
function ManagerState:applyProfile(p)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
local mods = self:manifestMap()
|
local mods = self:manifestMap()
|
||||||
local set = self:enabledSet()
|
local set = self:enabledSet()
|
||||||
local combined = {}
|
local combined = {}
|
||||||
@@ -963,6 +988,10 @@ function ManagerState:optionValue(modId, row)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ManagerState:setOption(modId, key, value)
|
function ManagerState:setOption(modId, key, value)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return false
|
||||||
|
end
|
||||||
local save = self.game.save
|
local save = self.game.save
|
||||||
if save and save.options then
|
if save and save.options then
|
||||||
save.options.modOptions = save.options.modOptions or {}
|
save.options.modOptions = save.options.modOptions or {}
|
||||||
@@ -1123,6 +1152,10 @@ function ManagerState:buildOptionRows(m, schema)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ManagerState:openOptions(m)
|
function ManagerState:openOptions(m)
|
||||||
|
if Runtime.safeMode then
|
||||||
|
self:notify("SAFE MODE ACTIVE")
|
||||||
|
return
|
||||||
|
end
|
||||||
local schema = self:schemaFor(m)
|
local schema = self:schemaFor(m)
|
||||||
if not schema then
|
if not schema then
|
||||||
self:notify("NO OPTIONS")
|
self:notify("NO OPTIONS")
|
||||||
|
|||||||
@@ -33,11 +33,21 @@ Runtime.currentMod = nil
|
|||||||
-- currentMod went back to nil (src/mods/Sandbox.lua)
|
-- currentMod went back to nil (src/mods/Sandbox.lua)
|
||||||
Runtime.modRequire = nil
|
Runtime.modRequire = nil
|
||||||
|
|
||||||
|
Runtime.safeMode = false
|
||||||
|
|
||||||
function Runtime.install(events, hooks, errors)
|
function Runtime.install(events, hooks, errors)
|
||||||
Runtime.events, Runtime.hooks = events, hooks
|
Runtime.events, Runtime.hooks = events, hooks
|
||||||
Runtime.errors = errors
|
Runtime.errors = errors
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Runtime.reset()
|
||||||
|
Runtime.events = NullEvents
|
||||||
|
Runtime.hooks = NullHooks
|
||||||
|
Runtime.errors = nil
|
||||||
|
Runtime.currentMod = nil
|
||||||
|
Runtime.modRequire = nil
|
||||||
|
end
|
||||||
|
|
||||||
-- attribute a runtime failure to the mod that owns the offending record.
|
-- attribute a runtime failure to the mod that owns the offending record.
|
||||||
-- "base" is the engine's own owner id: a vanilla record that fails is a
|
-- "base" is the engine's own owner id: a vanilla record that fails is a
|
||||||
-- console line, not something the manager can ask the player to disable.
|
-- console line, not something the manager can ask the player to disable.
|
||||||
|
|||||||
@@ -180,8 +180,8 @@ local function release(game)
|
|||||||
and mon.ot == game.save.player.name then
|
and mon.ot == game.save.player.name then
|
||||||
require("src.core.Sound").playCry(game.data, mon.species)
|
require("src.core.Sound").playCry(game.data, mon.species)
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
(t._PikachuUnhappyText or Strings("%s looks\nunhappy about it!", name))
|
((t._PikachuUnhappyText or Strings("%s looks\nunhappy about it!", name))
|
||||||
:gsub("{RAM:wNameBuffer}", name)))
|
:gsub("{RAM:wNameBuffer}", name))))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ local Theme = require("src.ui.Theme")
|
|||||||
local FieldDefaults = require("src.world.FieldDefaults")
|
local FieldDefaults = require("src.world.FieldDefaults")
|
||||||
local Map = require("src.world.Map")
|
local Map = require("src.world.Map")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
|
local Status = require("src.battle.Status")
|
||||||
|
|
||||||
local PartyMenu = {}
|
local PartyMenu = {}
|
||||||
PartyMenu.__index = PartyMenu
|
PartyMenu.__index = PartyMenu
|
||||||
@@ -821,7 +822,7 @@ function PartyMenu:draw()
|
|||||||
if mon.hp <= 0 then
|
if mon.hp <= 0 then
|
||||||
Font.draw(Strings("FNT"), 136, y)
|
Font.draw(Strings("FNT"), 136, y)
|
||||||
elseif mon.status then
|
elseif mon.status then
|
||||||
Font.draw(mon.status, 136, y)
|
Font.draw(Status.hudLabelFor(self.game.data.statuses, mon.status), 136, y)
|
||||||
end
|
end
|
||||||
-- the tile HP bar (DrawHP2 + SetPartyMenuHPBarColor). grayFill:
|
-- the tile HP bar (DrawHP2 + SetPartyMenuHPBarColor). grayFill:
|
||||||
-- tinting the fill AND running it through the row's zone
|
-- tinting the fill AND running it through the row's zone
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ local Font = require("src.render.Font")
|
|||||||
local TypeChart = require("src.battle.TypeChart")
|
local TypeChart = require("src.battle.TypeChart")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
local Stats = require("src.pokemon.Stats")
|
local Stats = require("src.pokemon.Stats")
|
||||||
|
local Status = require("src.battle.Status")
|
||||||
|
|
||||||
local SummaryMenu = {}
|
local SummaryMenu = {}
|
||||||
SummaryMenu.__index = SummaryMenu
|
SummaryMenu.__index = SummaryMenu
|
||||||
@@ -145,7 +146,7 @@ function SummaryMenu:draw()
|
|||||||
HudTiles.drawHPBar(data, 11, 3, mon, 1, barZoned) -- wHPBarType 1
|
HudTiles.drawHPBar(data, 11, 3, mon, 1, barZoned) -- wHPBarType 1
|
||||||
Font.draw(("%3d/%3d"):format(mon.hp, mon.stats.hp), 96, 32)
|
Font.draw(("%3d/%3d"):format(mon.hp, mon.stats.hp), 96, 32)
|
||||||
Font.draw(Strings("STATUS/"), 72, 48)
|
Font.draw(Strings("STATUS/"), 72, 48)
|
||||||
Font.draw(mon.status or "OK", 128, 48)
|
Font.draw(Status.hudLabelFor(data.statuses, mon.status) or "OK", 128, 48)
|
||||||
|
|
||||||
-- stats box (0,8) 10x10: names rows 9/11/13/15, values indented
|
-- stats box (0,8) 10x10: names rows 9/11/13/15, values indented
|
||||||
Font.drawBox(0, 8, 10, 10)
|
Font.drawBox(0, 8, 10, 10)
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ function TitleState.new(game, opts)
|
|||||||
self.game = game
|
self.game = game
|
||||||
self.onNewGame = opts.onNewGame
|
self.onNewGame = opts.onNewGame
|
||||||
self.onContinue = opts.onContinue
|
self.onContinue = opts.onContinue
|
||||||
|
self.onExit = opts.onExit
|
||||||
-- branding comes from field.title with the shipped art as fallback, so
|
-- branding comes from field.title with the shipped art as fallback, so
|
||||||
-- a total conversion rebrands the title without replacing the screen
|
-- a total conversion rebrands the title without replacing the screen
|
||||||
local title = (game.data.field and game.data.field.title) or {}
|
local title = (game.data.field and game.data.field.title) or {}
|
||||||
@@ -514,7 +515,9 @@ function TitleState:openMenu()
|
|||||||
require("src.ui.Screens").push(game, "OptionsMenu")
|
require("src.ui.Screens").push(game, "OptionsMenu")
|
||||||
end })
|
end })
|
||||||
table.insert(items, { label = Strings("EXIT GAME"), onSelect = function()
|
table.insert(items, { label = Strings("EXIT GAME"), onSelect = function()
|
||||||
if love.event and love.event.quit then
|
if self.onExit then
|
||||||
|
self.onExit()
|
||||||
|
elseif love.event and love.event.quit then
|
||||||
love.event.quit()
|
love.event.quit()
|
||||||
end
|
end
|
||||||
end })
|
end })
|
||||||
|
|||||||
@@ -2290,6 +2290,7 @@ function BattleState:openParty(forced)
|
|||||||
-- :2702; engine/pokemon/party_menu.asm:660-679). Only the voluntary list
|
-- :2702; engine/pokemon/party_menu.asm:660-679). Only the voluntary list
|
||||||
-- carries BattleMonMenu; PickPartyMonInBattle has no submenu.
|
-- carries BattleMonMenu; PickPartyMonInBattle has no submenu.
|
||||||
prompt = forced and "which" or "choose",
|
prompt = forced and "which" or "choose",
|
||||||
|
battle = true,
|
||||||
battleSubmenu = not forced,
|
battleSubmenu = not forced,
|
||||||
onCancel = function()
|
onCancel = function()
|
||||||
stack:pop()
|
stack:pop()
|
||||||
@@ -2762,6 +2763,7 @@ function BattleState:openShiftParty()
|
|||||||
self.phase = "submenu"
|
self.phase = "submenu"
|
||||||
Screens.push(self.game, "Gen2PartyMenu", {
|
Screens.push(self.game, "Gen2PartyMenu", {
|
||||||
prompt = "which",
|
prompt = "which",
|
||||||
|
battle = true,
|
||||||
onCancel = function()
|
onCancel = function()
|
||||||
stack:pop()
|
stack:pop()
|
||||||
self.phase = "resolving"
|
self.phase = "resolving"
|
||||||
@@ -3135,6 +3137,7 @@ function BattleState:useOnPartyMon(itemId, action)
|
|||||||
self.phase = "submenu"
|
self.phase = "submenu"
|
||||||
Screens.push(self.game, "Gen2PartyMenu", {
|
Screens.push(self.game, "Gen2PartyMenu", {
|
||||||
prompt = "useItem",
|
prompt = "useItem",
|
||||||
|
battle = true,
|
||||||
party = self.battle.party or (self.save and self.save.party),
|
party = self.battle.party or (self.save and self.save.party),
|
||||||
onCancel = function()
|
onCancel = function()
|
||||||
stack:pop()
|
stack:pop()
|
||||||
|
|||||||
@@ -76,6 +76,23 @@ local BATTLE_SUBMENU_LEFT, BATTLE_SUBMENU_TOP = 11, 11
|
|||||||
|
|
||||||
-- HP bar is 6 tiles wide (48px) in the party list.
|
-- HP bar is 6 tiles wide (48px) in the party list.
|
||||||
|
|
||||||
|
local function gridIndex(index, count, direction)
|
||||||
|
if count < 1 then return nil end
|
||||||
|
local row, col = math.floor((index - 1) / 2), (index - 1) % 2
|
||||||
|
if direction == "left" or direction == "right" then
|
||||||
|
local other = row * 2 + (1 - col) + 1
|
||||||
|
return other <= count and other or index
|
||||||
|
end
|
||||||
|
local step = direction == "up" and -1 or direction == "down" and 1
|
||||||
|
if not step then return nil end
|
||||||
|
local rows = math.ceil(count / 2)
|
||||||
|
for offset = 1, rows do
|
||||||
|
local other = ((row + step * offset) % rows) * 2 + col + 1
|
||||||
|
if other <= count then return other end
|
||||||
|
end
|
||||||
|
return index
|
||||||
|
end
|
||||||
|
|
||||||
function PartyMenu:wantsFillScale() return true end
|
function PartyMenu:wantsFillScale() return true end
|
||||||
function PartyMenu:drawsWidescreen() return true end
|
function PartyMenu:drawsWidescreen() return true end
|
||||||
|
|
||||||
@@ -114,6 +131,7 @@ function PartyMenu.new(game, opts)
|
|||||||
self.wantsSubmenu = opts.submenu == true
|
self.wantsSubmenu = opts.submenu == true
|
||||||
-- BattleMenu_PKMN's `callfar BattleMonMenu` (engine/battle/core.asm:4810).
|
-- BattleMenu_PKMN's `callfar BattleMonMenu` (engine/battle/core.asm:4810).
|
||||||
self.wantsBattleSubmenu = opts.battleSubmenu == true
|
self.wantsBattleSubmenu = opts.battleSubmenu == true
|
||||||
|
self.battle = opts.battle == true
|
||||||
self.submenu = nil
|
self.submenu = nil
|
||||||
-- The held slot while SwitchPartyMons' second pick is open; nil otherwise.
|
-- The held slot while SwitchPartyMons' second pick is open; nil otherwise.
|
||||||
self.switchFrom = nil
|
self.switchFrom = nil
|
||||||
@@ -146,6 +164,13 @@ function PartyMenu:isCancel()
|
|||||||
return self.index > #self.party
|
return self.index > #self.party
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function PartyMenu:gridNavigation()
|
||||||
|
if not self.battle
|
||||||
|
or not Runtime.wantsHook("ui.party.grid_navigation") then return false end
|
||||||
|
return Runtime.call("ui.party.grid_navigation", function() return false end,
|
||||||
|
self) == true
|
||||||
|
end
|
||||||
|
|
||||||
-- ------------------------------------------------------------- mon submenu
|
-- ------------------------------------------------------------- mon submenu
|
||||||
|
|
||||||
-- GetMonSubmenuItems, in its own order: every field move the mon knows first,
|
-- GetMonSubmenuItems, in its own order: every field move the mon knows first,
|
||||||
@@ -477,7 +502,18 @@ function PartyMenu:update(_dt)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
local total = self:count()
|
local total = self:count()
|
||||||
if input:wasPressed("up") then
|
local grid
|
||||||
|
if self:gridNavigation() then
|
||||||
|
local direction = input:wasPressed("left") and "left"
|
||||||
|
or input:wasPressed("right") and "right"
|
||||||
|
or input:wasPressed("up") and "up"
|
||||||
|
or input:wasPressed("down") and "down"
|
||||||
|
grid = gridIndex(self.index, #self.party, direction)
|
||||||
|
end
|
||||||
|
if grid then
|
||||||
|
self.index = grid
|
||||||
|
self:storeCursor()
|
||||||
|
elseif input:wasPressed("up") then
|
||||||
self.index = self.index > 1 and self.index - 1 or total
|
self.index = self.index > 1 and self.index - 1 or total
|
||||||
elseif input:wasPressed("down") then
|
elseif input:wasPressed("down") then
|
||||||
self.index = self.index < total and self.index + 1 or 1
|
self.index = self.index < total and self.index + 1 or 1
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
-- Test returning to launcher from Gen 1 and Gen 2 on Android without closing the process
|
||||||
|
-- luajit tests/engine/android_exit_to_launcher_test.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 TitleState = require("src.ui.TitleState")
|
||||||
|
local Gen2MainMenu = require("src.ui.gen2.MainMenu")
|
||||||
|
local Runtime = require("src.mods.Runtime")
|
||||||
|
|
||||||
|
-- 1. Test Gen 1 TitleState onExit callback support
|
||||||
|
do
|
||||||
|
local exitCalled = false
|
||||||
|
local dummyGame = {
|
||||||
|
data = { field = {} },
|
||||||
|
stack = {
|
||||||
|
states = {},
|
||||||
|
push = function(self, state) table.insert(self.states, state) end,
|
||||||
|
top = function(self) return self.states[#self.states] end,
|
||||||
|
pop = function(self) return table.remove(self.states) end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
local state = TitleState.new(dummyGame, {
|
||||||
|
onExit = function()
|
||||||
|
exitCalled = true
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
state:openMenu()
|
||||||
|
local menu = dummyGame.stack:top()
|
||||||
|
check(menu ~= nil, "TitleState:openMenu opens a menu")
|
||||||
|
local exitItem = nil
|
||||||
|
for _, item in ipairs(menu.items or {}) do
|
||||||
|
if tostring(item.label):find("EXIT", 1, true) then
|
||||||
|
exitItem = item
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
check(exitItem ~= nil, "Gen 1 TitleState menu contains an EXIT GAME item")
|
||||||
|
if exitItem and exitItem.onSelect then
|
||||||
|
exitItem.onSelect()
|
||||||
|
end
|
||||||
|
check(exitCalled, "Selecting EXIT GAME in Gen 1 TitleState invokes onExit callback")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 2. Test Gen 2 MainMenu onExit callback support
|
||||||
|
do
|
||||||
|
local exitCalled = false
|
||||||
|
local dummyGame2 = {
|
||||||
|
data = {},
|
||||||
|
stack = {
|
||||||
|
states = {},
|
||||||
|
push = function(self, state) table.insert(self.states, state) end,
|
||||||
|
top = function(self) return self.states[#self.states] end,
|
||||||
|
pop = function(self) return table.remove(self.states) end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
local menu = Gen2MainMenu.new(dummyGame2, {
|
||||||
|
hasSave = false,
|
||||||
|
onExit = function()
|
||||||
|
exitCalled = true
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
menu:choose("exit")
|
||||||
|
check(exitCalled, "Selecting EXIT GAME in Gen 2 MainMenu invokes onExit callback")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 3. Test Runtime.reset restores NullEvents and NullHooks
|
||||||
|
do
|
||||||
|
Runtime.install({ emit = function() end }, { call = function() end }, { "err" })
|
||||||
|
check(Runtime.errors ~= nil, "Runtime has errors list after install")
|
||||||
|
Runtime.reset()
|
||||||
|
check(Runtime.errors == nil, "Runtime.reset clears errors")
|
||||||
|
check(Runtime.currentMod == nil, "Runtime.reset clears currentMod")
|
||||||
|
check(Runtime.modRequire == nil, "Runtime.reset clears modRequire")
|
||||||
|
end
|
||||||
|
|
||||||
|
T.finish("android_exit_to_launcher_test")
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
-- tests/engine/android_shortcuts_payload_test.lua
|
||||||
|
-- Tests Android dynamic shortcuts synchronization, launch options intent resolution,
|
||||||
|
-- and love.handlers.intent_game in-process game hot-swapping.
|
||||||
|
-- luajit tests/engine/android_shortcuts_payload_test.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")
|
||||||
|
|
||||||
|
require("main")
|
||||||
|
|
||||||
|
-- 1. Verify LaunchOptions handles getLaunchGame on Android
|
||||||
|
local LaunchOptions = require("src.core.LaunchOptions")
|
||||||
|
|
||||||
|
local savedOS = love.system and love.system.getOS
|
||||||
|
local savedGetLaunchGame = love.system and love.system.getLaunchGame
|
||||||
|
|
||||||
|
love.system = love.system or {}
|
||||||
|
love.system.getOS = function() return "Android" end
|
||||||
|
love.system.getLaunchGame = function() return "gold" end
|
||||||
|
|
||||||
|
local game, slot = LaunchOptions.resolve({})
|
||||||
|
check(game == "gold", "LaunchOptions resolves intent game from love.system.getLaunchGame on Android")
|
||||||
|
|
||||||
|
local gameCli, slotCli = LaunchOptions.resolve({ "--game=red" })
|
||||||
|
check(gameCli == "red", "CLI --game flag overrides intent game")
|
||||||
|
|
||||||
|
-- 2. Verify RomImporter.syncAndroidShortcuts ranking and 4-item cap
|
||||||
|
local RomImporter = require("src.import.RomImporter")
|
||||||
|
|
||||||
|
local originalIsReady = RomImporter.isReady
|
||||||
|
local capturedShortcuts = nil
|
||||||
|
|
||||||
|
love.system.updateShortcuts = function(versions)
|
||||||
|
capturedShortcuts = versions
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mock isReady
|
||||||
|
RomImporter.isReady = function(v)
|
||||||
|
return v == "red" or v == "gold" or v == "blue" or v == "yellow"
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok = RomImporter.syncAndroidShortcuts("gold")
|
||||||
|
check(ok == true, "syncAndroidShortcuts returns true on Android")
|
||||||
|
check(#capturedShortcuts == 4, "syncAndroidShortcuts caps at 4 items")
|
||||||
|
check(capturedShortcuts[1] == "gold", "activeVersion 'gold' is placed first")
|
||||||
|
|
||||||
|
-- Test with subset of ready games (e.g. only Red and Gold)
|
||||||
|
RomImporter.isReady = function(v)
|
||||||
|
return v == "red" or v == "gold"
|
||||||
|
end
|
||||||
|
|
||||||
|
capturedShortcuts = nil
|
||||||
|
RomImporter.syncAndroidShortcuts("red")
|
||||||
|
check(#capturedShortcuts == 2, "syncAndroidShortcuts only includes ready ROMs")
|
||||||
|
check(capturedShortcuts[1] == "red" and capturedShortcuts[2] == "gold", "ready ROMs correctly passed")
|
||||||
|
|
||||||
|
-- Test on non-Android platform (safe no-op)
|
||||||
|
love.system.getOS = function() return "Linux" end
|
||||||
|
capturedShortcuts = nil
|
||||||
|
local nonAndroidOk = RomImporter.syncAndroidShortcuts("red")
|
||||||
|
check(nonAndroidOk == false, "syncAndroidShortcuts safely no-ops on non-Android")
|
||||||
|
check(capturedShortcuts == nil, "no shortcuts updated on non-Android")
|
||||||
|
|
||||||
|
-- 3. Verify love.handlers.intent_game definition
|
||||||
|
check(type(love.handlers.intent_game) == "function", "main.lua defines love.handlers.intent_game")
|
||||||
|
|
||||||
|
-- Restore
|
||||||
|
RomImporter.isReady = originalIsReady
|
||||||
|
if savedOS then
|
||||||
|
love.system.getOS = savedOS
|
||||||
|
else
|
||||||
|
love.system.getOS = nil
|
||||||
|
end
|
||||||
|
love.system.getLaunchGame = savedGetLaunchGame
|
||||||
|
love.system.updateShortcuts = nil
|
||||||
|
|
||||||
|
print("8/8 checks passed (android_shortcuts_payload_test)")
|
||||||
@@ -397,7 +397,8 @@ local GEN2_HOOKS = {
|
|||||||
"world.tod", "map.palette", "fieldmove.eligibility",
|
"world.tod", "map.palette", "fieldmove.eligibility",
|
||||||
-- menus and the battle intro
|
-- menus and the battle intro
|
||||||
"ui.start_menu.items", "ui.title_menu.items", "ui.options.rows",
|
"ui.start_menu.items", "ui.title_menu.items", "ui.options.rows",
|
||||||
"ui.party.submenu", "ui.naming.grid", "ui.pc.items", "ui.list_menu",
|
"ui.party.submenu", "ui.party.grid_navigation", "ui.naming.grid",
|
||||||
|
"ui.pc.items", "ui.list_menu",
|
||||||
"transition.style",
|
"transition.style",
|
||||||
-- battle
|
-- battle
|
||||||
"battle.damage", "battle.crit", "battle.accuracy", "battle.turn_order",
|
"battle.damage", "battle.crit", "battle.accuracy", "battle.turn_order",
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
-- BoxMenu's Yellow-only "Pikachu looks unhappy" release path
|
||||||
|
-- (release()'s isYellow()/species=="PIKACHU"/otId/ot branch) pushes its
|
||||||
|
-- TextBox with `TextBox.new(game, (...):gsub(...))` -- the gsub call is
|
||||||
|
-- the last argument, unparenthesized, so Lua expands its second return
|
||||||
|
-- value (the substitution count) into TextBox.new's third parameter,
|
||||||
|
-- onDone. TextBox.lua later calls onDone() unconditionally once the box
|
||||||
|
-- is dismissed, and a number is not callable: every release of your own
|
||||||
|
-- caught Pikachu in Yellow crashed, regardless of its nickname (unlike
|
||||||
|
-- the separate %-escape gsub bug, this needs no special save content --
|
||||||
|
-- ordinary play reaches it every time). ROM-free: registers a fake
|
||||||
|
-- Data.pokemon.PIKACHU cloned from the fixture species so the species ==
|
||||||
|
-- "PIKACHU" check can be exercised without a real ROM import.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.modkit")
|
||||||
|
local Data = T.fixtures.fresh()
|
||||||
|
local ids = T.fixtures.ids
|
||||||
|
require("src.render.Font").load(Data)
|
||||||
|
|
||||||
|
-- clone a real fixture species under the literal id release() checks for
|
||||||
|
Data.pokemon.PIKACHU = Data.pokemon[ids.species[1]]
|
||||||
|
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local Boxes = require("src.pokemon.Boxes")
|
||||||
|
local TextBox = require("src.render.TextBox")
|
||||||
|
local BoxMenu = require("src.ui.BoxMenu")
|
||||||
|
local ListMenu = require("src.ui.ListMenu")
|
||||||
|
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
local Sound = require("src.core.Sound")
|
||||||
|
|
||||||
|
local realCry, realPlay = Sound.playCry, Sound.play
|
||||||
|
Sound.playCry = function() end
|
||||||
|
Sound.play = function() end
|
||||||
|
|
||||||
|
local stack = { states = {} }
|
||||||
|
function stack:push(s) self.states[#self.states + 1] = s end
|
||||||
|
function stack:pop()
|
||||||
|
local t = self.states[#self.states]
|
||||||
|
self.states[#self.states] = nil
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
function stack:top() return self.states[#self.states] end
|
||||||
|
function stack:update(dt)
|
||||||
|
local t = self:top()
|
||||||
|
if t and t.update then t:update(dt) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local pressed = {}
|
||||||
|
local function press(btn)
|
||||||
|
pressed = { [btn] = true }
|
||||||
|
stack:update(1 / 60)
|
||||||
|
pressed = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function topMt() return getmetatable(stack:top()) end
|
||||||
|
local function mash(btn, cond, n)
|
||||||
|
for _ = 1, (n or 400) do
|
||||||
|
if cond() then return true end
|
||||||
|
press(btn)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
GameVersion.set("yellow")
|
||||||
|
|
||||||
|
local save = SaveData.newGame()
|
||||||
|
local game = {
|
||||||
|
data = Data,
|
||||||
|
save = save,
|
||||||
|
stack = stack,
|
||||||
|
input = {
|
||||||
|
wasPressed = function(_, key) return pressed[key] or false end,
|
||||||
|
isDown = function() return false end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
game.save.options = game.save.options or {}
|
||||||
|
game.save.options.textSpeed = 1
|
||||||
|
|
||||||
|
local box = Boxes.active(save)
|
||||||
|
local mon = Pokemon.new(Data, "PIKACHU", 5)
|
||||||
|
mon.otId = save.player.id
|
||||||
|
mon.ot = save.player.name
|
||||||
|
box[1] = mon
|
||||||
|
|
||||||
|
stack:push(BoxMenu.new(game))
|
||||||
|
press("down"); press("down"); press("a") -- open RELEASE list
|
||||||
|
T.check(topMt() == ListMenu, "RELEASE opens the box list")
|
||||||
|
|
||||||
|
-- release() calls Sound.playCry (stubbed) then pushes the "unhappy"
|
||||||
|
-- TextBox before any confirmation prompt -- pre-fix this line itself
|
||||||
|
-- raises "attempt to call field 'onDone' (a number value)" the moment
|
||||||
|
-- TextBox.new stores the leaked count and something dismisses the box.
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
press("a") -- choose the Pikachu; release() runs synchronously here
|
||||||
|
T.check(topMt() == TextBox, "the unhappy-Pikachu TextBox opens directly, no confirm prompt")
|
||||||
|
-- dismiss it: this is what calls onDone, which is where the pre-fix
|
||||||
|
-- leaked count used to crash
|
||||||
|
mash("a", function() return topMt() ~= TextBox end)
|
||||||
|
end)
|
||||||
|
T.check(ok, "releasing your own caught Pikachu in Yellow does not crash: " .. tostring(err))
|
||||||
|
|
||||||
|
GameVersion.set("red")
|
||||||
|
Sound.playCry, Sound.play = realCry, realPlay
|
||||||
|
T.finish("pikachu_unhappy_release_crash")
|
||||||
@@ -26,5 +26,7 @@ check(helperStart ~= nil, "requiredFilesFor helper exists")
|
|||||||
local helper = src:sub(helperStart, start)
|
local helper = src:sub(helperStart, start)
|
||||||
check(helper:find("VERSION_REQUIRED_FILES_OVERRIDE", 1, true) ~= nil,
|
check(helper:find("VERSION_REQUIRED_FILES_OVERRIDE", 1, true) ~= nil,
|
||||||
"requiredFilesFor consults VERSION_REQUIRED_FILES_OVERRIDE")
|
"requiredFilesFor consults VERSION_REQUIRED_FILES_OVERRIDE")
|
||||||
|
check(src:find('"assets/generated/battle/hud/balls.png"', 1, true) ~= nil,
|
||||||
|
"Gold caches require the trainer HUD ball sheet")
|
||||||
|
|
||||||
T.finish()
|
T.finish()
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local S = require("tests.harness").suite("safe mode and issue report")
|
||||||
|
local check = S.check
|
||||||
|
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local LauncherMods = require("src.mods.LauncherMods")
|
||||||
|
local IssueReport = require("src.core.IssueReport")
|
||||||
|
local Version = require("src.core.Version")
|
||||||
|
|
||||||
|
local options = SaveData.defaultOptions()
|
||||||
|
check(not SaveData.isSafeMode(options), "safe mode defaults off")
|
||||||
|
SaveData.setSafeMode(options, true)
|
||||||
|
check(SaveData.isSafeMode(options), "safe mode can be enabled")
|
||||||
|
|
||||||
|
local manifests = {
|
||||||
|
{ id = "alpha", name = "Alpha", version = "1.0.0", experimental = false,
|
||||||
|
raw = {}, dependencySpecs = {}, conflictSpecs = {} },
|
||||||
|
{ id = "beta", name = "Beta", version = "1.0.0", experimental = false,
|
||||||
|
raw = {}, dependencySpecs = {}, conflictSpecs = {} },
|
||||||
|
}
|
||||||
|
options.mods.alpha = false
|
||||||
|
options.mods.beta = true
|
||||||
|
local rows = LauncherMods.deriveList(manifests, options, "red")
|
||||||
|
check(#rows == 2, "safe mode keeps installed mods visible")
|
||||||
|
check(not rows[1].enabled and not rows[2].enabled,
|
||||||
|
"safe mode disables every launcher mod row")
|
||||||
|
check(rows[1].status == "safe_mode" and rows[2].status == "safe_mode",
|
||||||
|
"safe mode explains every disabled launcher row")
|
||||||
|
|
||||||
|
SaveData.setSafeMode(options, false)
|
||||||
|
rows = LauncherMods.deriveList(manifests, options, "red")
|
||||||
|
local byId = {}
|
||||||
|
for _, row in ipairs(rows) do byId[row.id] = row end
|
||||||
|
check(not byId.alpha.enabled and byId.beta.enabled,
|
||||||
|
"turning safe mode off restores saved mod choices")
|
||||||
|
|
||||||
|
local previousLove = _G.love
|
||||||
|
local openedURL
|
||||||
|
_G.love = {
|
||||||
|
getVersion = function() return 12, 0, 0, "Mysterious Mysteries" end,
|
||||||
|
system = {
|
||||||
|
getOS = function() return "iOS" end,
|
||||||
|
getModel = function() return "iPad Test" end,
|
||||||
|
openURL = function(url) openedURL = url end,
|
||||||
|
},
|
||||||
|
graphics = {
|
||||||
|
getRendererInfo = function()
|
||||||
|
return "Metal", "3.0", "Apple", "Simulator GPU"
|
||||||
|
end,
|
||||||
|
getDimensions = function() return 1024, 768 end,
|
||||||
|
getPixelDimensions = function() return 2048, 1536 end,
|
||||||
|
},
|
||||||
|
window = {
|
||||||
|
getMode = function() return 1024, 768, { fullscreen = false } end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local url, fields, info = IssueReport.build({
|
||||||
|
safeMode = true,
|
||||||
|
lastVersion = "gold",
|
||||||
|
}, {
|
||||||
|
version = "gold",
|
||||||
|
mods = { { id = "alpha", name = "Alpha", enabled = true } },
|
||||||
|
})
|
||||||
|
check(url:find("template=bug_report.yml", 1, true) ~= nil,
|
||||||
|
"report URL selects the bug form")
|
||||||
|
check(url:find("title=bug%3A%20replace%20this%20with%20a%20meaningful%20title", 1, true) ~= nil,
|
||||||
|
"report URL uses the requested bug title")
|
||||||
|
check(info.os == "iOS",
|
||||||
|
"report metadata maps the platform")
|
||||||
|
check(fields.mods_which == "",
|
||||||
|
"safe mode leaves the optional mod list blank")
|
||||||
|
check(not url:find("game=", 1, true)
|
||||||
|
and not url:find("os=", 1, true)
|
||||||
|
and not url:find("mods_enabled=", 1, true),
|
||||||
|
"report URL omits unsupported dropdown and checkbox prefills")
|
||||||
|
check(fields.summary == "" and fields.location == "" and fields.screenshot == ""
|
||||||
|
and fields.steps == "" and fields.expected == "",
|
||||||
|
"report leaves user-entered fields blank")
|
||||||
|
check(info.metadata:find("Device: iPad Test", 1, true) ~= nil
|
||||||
|
and info.metadata:find("LÖVE: 12.0.0", 1, true) ~= nil
|
||||||
|
and info.metadata:find("Safe mode: on", 1, true) ~= nil,
|
||||||
|
"report metadata includes device and app details")
|
||||||
|
check(not info.metadata:find("unknown", 1, true),
|
||||||
|
"report metadata omits unknown values")
|
||||||
|
check(not info.metadata:find("Game id", 1, true)
|
||||||
|
and not info.metadata:find("Game:", 1, true)
|
||||||
|
and not info.metadata:find("Mods:", 1, true)
|
||||||
|
and not info.metadata:find("Processors", 1, true)
|
||||||
|
and not info.metadata:find("Power", 1, true),
|
||||||
|
"report metadata omits redundant system fields")
|
||||||
|
|
||||||
|
local previousEngine = Version.engine
|
||||||
|
Version.engine = "0.1.50"
|
||||||
|
local _, versionFields, versionInfo = IssueReport.build({}, { mods = {} })
|
||||||
|
check(versionFields.version == "0.1.50"
|
||||||
|
and versionInfo.metadata:find("App: gen1recomp v0.1.50", 1, true) ~= nil,
|
||||||
|
"report uses the stamped app version")
|
||||||
|
Version.engine = previousEngine
|
||||||
|
local _, _, developmentInfo = IssueReport.build({}, { mods = {} })
|
||||||
|
check(not developmentInfo.metadata:find("0.0.0-dev", 1, true),
|
||||||
|
"report omits an unstamped development version")
|
||||||
|
|
||||||
|
local previousOS = love.system.getOS
|
||||||
|
local previousModel = love.system.getModel
|
||||||
|
local previousIO = _G.io
|
||||||
|
love.system.getOS = function() return "OS X" end
|
||||||
|
love.system.getModel = nil
|
||||||
|
_G.io = {
|
||||||
|
popen = function()
|
||||||
|
return {
|
||||||
|
read = function() return "MacBookPro18,3" end,
|
||||||
|
close = function() end,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
local desktopInfo = IssueReport.metadata({}, { mods = {} })
|
||||||
|
check(desktopInfo.device == "MacBookPro18,3",
|
||||||
|
"report finds desktop device model when LOVE has no model")
|
||||||
|
love.system.getOS = function() return "UWP" end
|
||||||
|
local xboxInfo = IssueReport.metadata({}, { mods = {} })
|
||||||
|
check(xboxInfo.os == "Xbox", "report maps the Xbox runtime platform")
|
||||||
|
love.system.getOS = previousOS
|
||||||
|
love.system.getModel = previousModel
|
||||||
|
_G.io = previousIO
|
||||||
|
|
||||||
|
local opened = IssueReport.open({ safeMode = false }, {
|
||||||
|
version = "red",
|
||||||
|
mods = {},
|
||||||
|
})
|
||||||
|
check(opened and openedURL and openedURL:find("title=bug%3A%20replace%20this%20with%20a%20meaningful%20title", 1, true) ~= nil,
|
||||||
|
"report action opens the generated URL")
|
||||||
|
|
||||||
|
_G.love = previousLove
|
||||||
|
|
||||||
|
S.finish()
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
-- SummaryMenu.lua:148 and PartyMenu.lua:824 used to draw mon.status as a
|
||||||
|
-- bare literal ("PSN", "PAR", "BRN", "FRZ", "SLP"), invisible to any
|
||||||
|
-- translation a mod supplies. Unlike the strings catalog, a mod translates
|
||||||
|
-- status abbreviations through the statuses content registry
|
||||||
|
-- (mod.content.statuses:patch(id, { label = value }), label only), the
|
||||||
|
-- same registry src/battle/BattleState.lua:statusLabel already reads in
|
||||||
|
-- battle. This test drives both screens' status draw with a mod-patched
|
||||||
|
-- registry and checks the patched label reaches Font.draw, not the raw
|
||||||
|
-- status id.
|
||||||
|
--
|
||||||
|
-- It also guards a second bug found alongside the first: Status.RECORDS'
|
||||||
|
-- five vanilla entries used to set hudLabel to the same literal as label
|
||||||
|
-- ("FRZ", hudLabel = "FRZ", ...). Since Status.hudLabelFor (and
|
||||||
|
-- BattleState:statusLabel before it) reads "hudLabel or label", and
|
||||||
|
-- Registry:patch only overrides the fields a mod actually passes, a
|
||||||
|
-- real label-only patch left the untouched vanilla hudLabel shadowing it
|
||||||
|
-- forever -- the translation was stored but never displayed anywhere,
|
||||||
|
-- in or out of battle.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
|
||||||
|
love = love or {}
|
||||||
|
love.graphics = {
|
||||||
|
setColor = function() end,
|
||||||
|
rectangle = function() end,
|
||||||
|
draw = function() end,
|
||||||
|
push = function() end, pop = function() end,
|
||||||
|
translate = function() end, scale = function() end,
|
||||||
|
}
|
||||||
|
|
||||||
|
package.loaded["src.render.Font"] = {
|
||||||
|
draw = function() end,
|
||||||
|
drawCode = function() end,
|
||||||
|
drawBox = function() end,
|
||||||
|
}
|
||||||
|
package.loaded["src.render.HudTiles"] = {
|
||||||
|
statusTile = function() end,
|
||||||
|
tile = function() end,
|
||||||
|
drawHPBar = function() end,
|
||||||
|
}
|
||||||
|
package.loaded["src.render.PaletteFX"] = {
|
||||||
|
shader = function() return nil end,
|
||||||
|
pal = function() return nil end,
|
||||||
|
markTrueColor = function() end,
|
||||||
|
}
|
||||||
|
package.loaded["src.ui.Theme"] = { cursor = 0, cursorHollow = 0 }
|
||||||
|
package.loaded["src.render.Assets"] = {}
|
||||||
|
package.loaded["src.world.FieldDefaults"] = {}
|
||||||
|
package.loaded["src.world.Map"] = {}
|
||||||
|
package.loaded["src.mods.Runtime"] = { wantsHook = function() return false end }
|
||||||
|
package.loaded["src.ui.Screens"] = {}
|
||||||
|
package.loaded["src.core.Logger"] = { warn = function() end }
|
||||||
|
|
||||||
|
local Status = require("src.battle.Status")
|
||||||
|
|
||||||
|
-- a mod's registered status translation, same shape mod.content.statuses:
|
||||||
|
-- patch(id, { label = ..., hudLabel = ... }) merges into Data.statuses
|
||||||
|
local function moddedStatuses()
|
||||||
|
local statuses = {}
|
||||||
|
for id, record in pairs(Status.RECORDS) do statuses[id] = record end
|
||||||
|
statuses.PSN = { id = "PSN", label = "PSN", hudLabel = "TOX" }
|
||||||
|
return statuses
|
||||||
|
end
|
||||||
|
|
||||||
|
local Font = package.loaded["src.render.Font"]
|
||||||
|
local drawn
|
||||||
|
local origDraw = Font.draw
|
||||||
|
Font.draw = function(text, x, y)
|
||||||
|
drawn[#drawn + 1] = { text = text, x = x, y = y }
|
||||||
|
return origDraw(text, x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mkDef()
|
||||||
|
return { name = "BULBASAUR", dex = 1, types = { "GRASS" } }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mkMon(status)
|
||||||
|
return {
|
||||||
|
nickname = "SAUR", species = "BULBASAUR", level = 5,
|
||||||
|
hp = 10, stats = { hp = 10, attack = 5, defense = 5, speed = 5, special = 5 },
|
||||||
|
status = status,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- SummaryMenu: page 1's STATUS/ line (~line 148-151) ----
|
||||||
|
do
|
||||||
|
local SummaryMenu = assert(loadfile("src/ui/SummaryMenu.lua"))()
|
||||||
|
local game = {
|
||||||
|
data = { pokemon = { BULBASAUR = mkDef() }, statuses = moddedStatuses() },
|
||||||
|
save = { player = { id = 1, name = "RED" } },
|
||||||
|
}
|
||||||
|
local menu = setmetatable(
|
||||||
|
{ game = game, mon = mkMon("PSN"), page = 1 }, SummaryMenu)
|
||||||
|
drawn = {}
|
||||||
|
menu:draw()
|
||||||
|
local statusDraw
|
||||||
|
for _, d in ipairs(drawn) do
|
||||||
|
if d.x == 128 and d.y == 48 then statusDraw = d end
|
||||||
|
end
|
||||||
|
T.check(statusDraw ~= nil, "SummaryMenu draws a status label at (128,48)")
|
||||||
|
T.eq(statusDraw.text, "TOX",
|
||||||
|
"SummaryMenu draws the mod-patched hudLabel, not the raw status id")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- vanilla (no mod): falls back to the plain id, same as before the fix
|
||||||
|
do
|
||||||
|
local SummaryMenu = assert(loadfile("src/ui/SummaryMenu.lua"))()
|
||||||
|
local game = {
|
||||||
|
data = { pokemon = { BULBASAUR = mkDef() }, statuses = nil },
|
||||||
|
save = { player = { id = 1, name = "RED" } },
|
||||||
|
}
|
||||||
|
local menu = setmetatable(
|
||||||
|
{ game = game, mon = mkMon("PSN"), page = 1 }, SummaryMenu)
|
||||||
|
drawn = {}
|
||||||
|
menu:draw()
|
||||||
|
local statusDraw
|
||||||
|
for _, d in ipairs(drawn) do
|
||||||
|
if d.x == 128 and d.y == 48 then statusDraw = d end
|
||||||
|
end
|
||||||
|
T.eq(statusDraw.text, "PSN",
|
||||||
|
"SummaryMenu still shows the vanilla PSN label with no mod loaded")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- PartyMenu: the roster row's status column (~line 824-827) ----
|
||||||
|
do
|
||||||
|
local PartyMenu = assert(loadfile("src/ui/PartyMenu.lua"))()
|
||||||
|
PartyMenu.drawIcon = function() end
|
||||||
|
local game = {
|
||||||
|
data = { pokemon = { BULBASAUR = mkDef() }, statuses = moddedStatuses(),
|
||||||
|
text = {} },
|
||||||
|
save = { party = { mkMon("PSN") } },
|
||||||
|
}
|
||||||
|
local list = setmetatable({ game = game, index = 1 }, PartyMenu)
|
||||||
|
drawn = {}
|
||||||
|
list:draw()
|
||||||
|
local statusDraw
|
||||||
|
for _, d in ipairs(drawn) do
|
||||||
|
if d.x == 136 then statusDraw = d end
|
||||||
|
end
|
||||||
|
T.check(statusDraw ~= nil, "PartyMenu draws a status label at x=136")
|
||||||
|
T.eq(statusDraw.text, "TOX",
|
||||||
|
"PartyMenu draws the mod-patched hudLabel, not the raw status id")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- real Registry:patch, not a hand-built table: a label-only patch (the
|
||||||
|
-- shape a translation mod would send for every one of the 5 vanilla
|
||||||
|
-- statuses) must reach the HUD despite the vanilla record already
|
||||||
|
-- defining hudLabel ----
|
||||||
|
do
|
||||||
|
local Registry = require("src.mods.Registry")
|
||||||
|
local reg = Registry.new("statuses", { semantics = "record", target = "statuses" })
|
||||||
|
reg.base = function() return Status.RECORDS end
|
||||||
|
local LABEL_ONLY_PATCH = { SLP = "SOM", FRZ = "GEL", PSN = "PSN", BRN = "BRU", PAR = "PAR" }
|
||||||
|
for id, translated in pairs(LABEL_ONLY_PATCH) do
|
||||||
|
reg:patch(id, { label = translated }, "mod")
|
||||||
|
end
|
||||||
|
local merged = {}
|
||||||
|
for id in pairs(Status.RECORDS) do merged[id] = reg:get(id) end
|
||||||
|
for id, translated in pairs(LABEL_ONLY_PATCH) do
|
||||||
|
T.eq(Status.hudLabelFor(merged, id), translated,
|
||||||
|
"a label-only mod patch on " .. id .. " reaches the HUD label")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
T.finish("status_abbreviation_translation_test")
|
||||||
@@ -14,6 +14,7 @@ local NamingScreen = require("src.ui.NamingScreen")
|
|||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||||
local PartyMenu = require("src.ui.PartyMenu")
|
local PartyMenu = require("src.ui.PartyMenu")
|
||||||
|
local Gen2PartyMenu = require("src.ui.gen2.PartyMenu")
|
||||||
local Player = require("src.world.Player")
|
local Player = require("src.world.Player")
|
||||||
local Music = require("src.core.Music")
|
local Music = require("src.core.Music")
|
||||||
|
|
||||||
@@ -295,6 +296,16 @@ do
|
|||||||
menu:update(0)
|
menu:update(0)
|
||||||
check(menu.index == 4,
|
check(menu.index == 4,
|
||||||
"removing the hook restores native list navigation immediately")
|
"removing the hook restores native list navigation immediately")
|
||||||
|
|
||||||
|
local gold = Gen2PartyMenu.new(game, { battle = true })
|
||||||
|
unsub = wrap("ui.party.grid_navigation", function() return true end)
|
||||||
|
gold:update(0)
|
||||||
|
check(gold.index == 3,
|
||||||
|
"a Gold battle party can follow the same companion grid")
|
||||||
|
unsub()
|
||||||
|
gold:update(0)
|
||||||
|
check(gold.index == 4,
|
||||||
|
"Gold restores native party list navigation without the hook")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ------- music.volume (distance / indoor muffling)
|
-- ------- music.volume (distance / indoor muffling)
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generates Android Adaptive Icon (based on cleaned love.png Pokéball + Gen1Recomp emblem)
|
||||||
|
and 3D Cartridge Shortcut assets for all density buckets (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from collections import deque
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageDraw, ImageFilter
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
RES_DIR = os.path.join(ROOT, "mobile", "android", "app", "src", "main", "res")
|
||||||
|
|
||||||
|
DENSITIES = {
|
||||||
|
"mdpi": {"shortcut": 48, "adaptive": 108},
|
||||||
|
"hdpi": {"shortcut": 72, "adaptive": 162},
|
||||||
|
"xhdpi": {"shortcut": 96, "adaptive": 216},
|
||||||
|
"xxhdpi": {"shortcut": 144, "adaptive": 324},
|
||||||
|
"xxxhdpi": {"shortcut": 192, "adaptive": 432},
|
||||||
|
}
|
||||||
|
|
||||||
|
SHELL_COLORS = {
|
||||||
|
"red": {"main": (230, 45, 55), "dark": (175, 25, 35), "light": (255, 90, 100)},
|
||||||
|
"blue": {"main": (35, 125, 235), "dark": (20, 85, 175), "light": (80, 165, 255)},
|
||||||
|
"yellow": {"main": (255, 205, 10), "dark": (210, 160, 0), "light": (255, 230, 80)},
|
||||||
|
"gold": {"main": (225, 170, 40), "dark": (170, 120, 20), "light": (245, 200, 80)},
|
||||||
|
}
|
||||||
|
|
||||||
|
def extract_cleaned_love_emblem():
|
||||||
|
"""Extracts the Pokéball/Gen1Recomp emblem from love.png with transparent bg and deepened blacks."""
|
||||||
|
src_path = os.path.join(RES_DIR, "drawable-xxxhdpi", "love.png")
|
||||||
|
if not os.path.exists(src_path):
|
||||||
|
src_path = os.path.join(RES_DIR, "drawable-xxhdpi", "love.png")
|
||||||
|
src = Image.open(src_path).convert("RGBA")
|
||||||
|
arr = np.array(src, dtype=np.uint8)
|
||||||
|
h, w = arr.shape[:2]
|
||||||
|
|
||||||
|
visited = np.zeros((h, w), dtype=bool)
|
||||||
|
bg_mask = np.zeros((h, w), dtype=bool)
|
||||||
|
|
||||||
|
queue = deque()
|
||||||
|
for x in range(w):
|
||||||
|
queue.append((0, x)); queue.append((h-1, x))
|
||||||
|
for y in range(h):
|
||||||
|
queue.append((y, 0)); queue.append((y, w-1))
|
||||||
|
|
||||||
|
bg_ref = np.array([255, 237, 254], dtype=float)
|
||||||
|
while queue:
|
||||||
|
y, x = queue.popleft()
|
||||||
|
if visited[y, x]: continue
|
||||||
|
visited[y, x] = True
|
||||||
|
color = arr[y, x, :3].astype(float)
|
||||||
|
if np.max(np.abs(color - bg_ref)) < 28:
|
||||||
|
bg_mask[y, x] = True
|
||||||
|
for dy, dx in [(-1,0), (1,0), (0,-1), (0,1)]:
|
||||||
|
ny, nx = y + dy, x + dx
|
||||||
|
if 0 <= ny < h and 0 <= nx < w and not visited[ny, nx]:
|
||||||
|
queue.append((ny, nx))
|
||||||
|
|
||||||
|
out_arr = arr.copy().astype(float)
|
||||||
|
out_arr[bg_mask, 3] = 0
|
||||||
|
|
||||||
|
# Deepen the soft blacks/outlines for crispness:
|
||||||
|
fg_mask = ~bg_mask
|
||||||
|
rgb = out_arr[fg_mask, :3]
|
||||||
|
lum = 0.299 * rgb[:, 0] + 0.587 * rgb[:, 1] + 0.114 * rgb[:, 2]
|
||||||
|
|
||||||
|
for i in range(len(rgb)):
|
||||||
|
l = lum[i]
|
||||||
|
if l < 110:
|
||||||
|
factor = (l / 110.0) ** 1.8
|
||||||
|
rgb[i, 0] = max(0, rgb[i, 0] * factor * 0.7)
|
||||||
|
rgb[i, 1] = max(0, rgb[i, 1] * factor * 0.7)
|
||||||
|
rgb[i, 2] = max(0, rgb[i, 2] * factor * 0.8)
|
||||||
|
|
||||||
|
out_arr[fg_mask, :3] = np.clip(rgb, 0, 255)
|
||||||
|
return Image.fromarray(out_arr.astype(np.uint8))
|
||||||
|
|
||||||
|
CLEANED_EMBLEM = extract_cleaned_love_emblem()
|
||||||
|
|
||||||
|
def create_adaptive_foreground(size):
|
||||||
|
emblem = CLEANED_EMBLEM.copy()
|
||||||
|
target_size = int(size * 0.78)
|
||||||
|
emblem.thumbnail((target_size, target_size), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||||
|
x = (size - emblem.width) // 2
|
||||||
|
y = (size - emblem.height) // 2
|
||||||
|
canvas.paste(emblem, (x, y), emblem)
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
def create_adaptive_monochrome(size):
|
||||||
|
fg = create_adaptive_foreground(size)
|
||||||
|
arr = np.array(fg, dtype=float)
|
||||||
|
lum = 0.299 * arr[:, :, 0] + 0.587 * arr[:, :, 1] + 0.114 * arr[:, :, 2]
|
||||||
|
alpha = arr[:, :, 3]
|
||||||
|
|
||||||
|
mono_alpha = np.zeros_like(alpha)
|
||||||
|
valid = alpha > 20
|
||||||
|
mono_alpha[valid & (lum > 70)] = 255
|
||||||
|
mono_alpha[valid & (lum <= 70)] = 0
|
||||||
|
|
||||||
|
mono_img = np.zeros((size, size, 4), dtype=np.uint8)
|
||||||
|
mono_img[:, :, 0] = 255
|
||||||
|
mono_img[:, :, 1] = 255
|
||||||
|
mono_img[:, :, 2] = 255
|
||||||
|
mono_img[:, :, 3] = mono_alpha.astype(np.uint8)
|
||||||
|
|
||||||
|
return Image.fromarray(mono_img)
|
||||||
|
|
||||||
|
def render_3d_cartridge(version, size):
|
||||||
|
colors = SHELL_COLORS[version]
|
||||||
|
S = size * 4
|
||||||
|
|
||||||
|
# Transparent canvas so the cartridge sits directly on launcher's white circle plate
|
||||||
|
canvas = Image.new("RGBA", (S, S), (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(canvas)
|
||||||
|
|
||||||
|
# Tight padding to maximize cartridge size
|
||||||
|
pad_x = int(S * 0.05)
|
||||||
|
pad_y = int(S * 0.03)
|
||||||
|
cw = S - pad_x * 2
|
||||||
|
ch = S - pad_y * 2
|
||||||
|
|
||||||
|
# Soft drop shadow
|
||||||
|
shadow = Image.new("RGBA", (S, S), (0, 0, 0, 0))
|
||||||
|
sdraw = ImageDraw.Draw(shadow)
|
||||||
|
sdraw.rounded_rectangle([pad_x + 8, pad_y + 14, pad_x + cw + 8, pad_y + ch + 14],
|
||||||
|
radius=int(S * 0.05), fill=(0, 0, 0, 90))
|
||||||
|
shadow = shadow.filter(ImageFilter.GaussianBlur(int(S * 0.03)))
|
||||||
|
canvas.paste(shadow, (0, 0), shadow)
|
||||||
|
|
||||||
|
radius = int(S * 0.045)
|
||||||
|
depth = int(S * 0.03)
|
||||||
|
draw.rounded_rectangle([pad_x, pad_y + depth, pad_x + cw, pad_y + ch + depth],
|
||||||
|
radius=radius, fill=colors["dark"])
|
||||||
|
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
|
||||||
|
radius=radius, fill=colors["main"])
|
||||||
|
|
||||||
|
notch_w = int(cw * 0.65)
|
||||||
|
notch_h = int(ch * 0.07)
|
||||||
|
notch_x = pad_x + (cw - notch_w) // 2
|
||||||
|
notch_y = pad_y + int(ch * 0.035)
|
||||||
|
draw.rounded_rectangle([notch_x, notch_y, notch_x + notch_w, notch_y + notch_h],
|
||||||
|
radius=int(notch_h * 0.4), fill=colors["dark"])
|
||||||
|
draw.rounded_rectangle([notch_x, notch_y - 2, notch_x + notch_w, notch_y + notch_h - 2],
|
||||||
|
radius=int(notch_h * 0.4), fill=colors["light"])
|
||||||
|
|
||||||
|
label_margin_x = int(cw * 0.09)
|
||||||
|
label_top_y = pad_y + int(ch * 0.20)
|
||||||
|
label_w = cw - label_margin_x * 2
|
||||||
|
label_h = int(ch * 0.70)
|
||||||
|
label_x = pad_x + label_margin_x
|
||||||
|
|
||||||
|
draw.rounded_rectangle([label_x - 3, label_top_y - 3, label_x + label_w + 3, label_top_y + label_h + 3],
|
||||||
|
radius=int(radius * 0.7), fill=colors["dark"])
|
||||||
|
|
||||||
|
label_path = os.path.join(ROOT, "assets", "labels", f"{version}.png")
|
||||||
|
if os.path.exists(label_path):
|
||||||
|
label_img = Image.open(label_path).convert("RGBA")
|
||||||
|
label_img = label_img.resize((label_w, label_h), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
mask = Image.new("L", (label_w, label_h), 0)
|
||||||
|
mdraw = ImageDraw.Draw(mask)
|
||||||
|
mdraw.rounded_rectangle([0, 0, label_w, label_h], radius=int(radius * 0.5), fill=255)
|
||||||
|
|
||||||
|
canvas.paste(label_img, (label_x, label_top_y), mask)
|
||||||
|
else:
|
||||||
|
draw.rounded_rectangle([label_x, label_top_y, label_x + label_w, label_top_y + label_h],
|
||||||
|
radius=int(radius * 0.5), fill=(240, 240, 240, 255))
|
||||||
|
|
||||||
|
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
|
||||||
|
radius=radius, outline=colors["light"], width=max(2, int(S * 0.008)))
|
||||||
|
|
||||||
|
return canvas.resize((size, size), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.makedirs(os.path.join(RES_DIR, "values"), exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(RES_DIR, "mipmap-anydpi-v26"), exist_ok=True)
|
||||||
|
|
||||||
|
for density, sizes in DENSITIES.items():
|
||||||
|
drawable_dir = os.path.join(RES_DIR, f"drawable-{density}")
|
||||||
|
os.makedirs(drawable_dir, exist_ok=True)
|
||||||
|
|
||||||
|
fg = create_adaptive_foreground(sizes["adaptive"])
|
||||||
|
fg.save(os.path.join(drawable_dir, "ic_launcher_foreground.png"), "PNG")
|
||||||
|
|
||||||
|
for ver in ("red", "blue", "yellow", "gold"):
|
||||||
|
cart = render_3d_cartridge(ver, sizes["shortcut"])
|
||||||
|
cart.save(os.path.join(drawable_dir, f"ic_shortcut_{ver}.png"), "PNG")
|
||||||
|
|
||||||
|
print(f"Generated assets for drawable-{density}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||