From c7af4069c9baeddfafdc33905462a250220a584f Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Tue, 13 Oct 2026 18:31:37 +0700 Subject: [PATCH] fix(server): resolve KeyError avatar (error 1001_1 / 1001_3) with SafeDict & persistent auto-healing --- app/build.gradle.kts | 4 +- app/src/main/assets/chaquopy/app.imy | Bin 4943 -> 6355 bytes .../data/PersistentRepository.kt | 154 ++++++++++++++- .../escapepenacony/data/UpdateRepository.kt | 24 ++- .../escapepenacony/service/ServerService.kt | 7 + .../catlas/escapepenacony/ui/MainScreen.kt | 1 + app/src/main/python/run_ppsr.py | 178 ++++++++++++++++++ 7 files changed, 355 insertions(+), 13 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0ccb7ba..1b65ed6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.catlas.escapepenacony" minSdk = 26 targetSdk = 35 - versionCode = 6 - versionName = "v1.1.3" + versionCode = 7 + versionName = "v1.1.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/assets/chaquopy/app.imy b/app/src/main/assets/chaquopy/app.imy index 0c3b9e9ef4209f3ec34889eb5fbfff52705e6738..0c675c4da4e4b153237765dde617137028449bbe 100644 GIT binary patch delta 4153 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 DOPJ@z delta 2725 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 diff --git a/app/src/main/java/com/catlas/escapepenacony/data/PersistentRepository.kt b/app/src/main/java/com/catlas/escapepenacony/data/PersistentRepository.kt index d008d2e..a69de97 100644 --- a/app/src/main/java/com/catlas/escapepenacony/data/PersistentRepository.kt +++ b/app/src/main/java/com/catlas/escapepenacony/data/PersistentRepository.kt @@ -44,13 +44,128 @@ class PersistentRepository(private val context: Context) { return File(serverBuildDir, "ppsr.pyc").exists() } + companion object { + val DEFAULT_PERSISTENT_JSON_STRING = """ + { + "send_hotfix": true, + "scene": { + "plane_id": 20313, + "x": 40748, + "y": 192819, + "z": 439218, + "calyx_x": 40748, + "calyx_y": 192810, + "calyx_z": 441000, + "map_layer": 2, + "calyx_group_id": 186, + "calyx_inst_id": 300001, + "calyx_entity_id": 1337, + "calyx_prop_id": 808 + }, + "avatar": { + "tb_gender": "Female", + "tb_path": "Elation", + "march_path": "Preservation", + "skin": { + "1001": 1100101, + "1310": 1131001, + "1415": 1141501, + "1407": 1140701, + "1501": 1150101 + }, + "lineup": [ + 1511, + 1502, + 1506, + 1503 + ], + "global_passive": [ + { + "name": "Castorice Global Passive", + "id": 140703, + "active": true + }, + { + "name": "SW999 Global Passsive", + "id": 150602, + "active": true + }, + { + "name": "Aha Global Passive", + "id": 151104, + "active": true + } + ], + "buff": [ + 1000121 + ], + "leader": 0 + }, + "key": "" + } + """.trimIndent() + } + + fun createDefaultPersistentJson(): JSONObject { + return JSONObject(DEFAULT_PERSISTENT_JSON_STRING) + } + + fun ensureValidPersistentJsonSync(): JSONObject { + try { + miscDir.mkdirs() + val defaultTemplate = createDefaultPersistentJson() + if (!persistentFile.exists()) { + persistentFile.writeText(defaultTemplate.toString(4), StandardCharsets.UTF_8) + return defaultTemplate + } + val content = persistentFile.readText(StandardCharsets.UTF_8) + val json = JSONObject(content) + var modified = false + + if (!json.has("send_hotfix")) { + json.put("send_hotfix", true) + modified = true + } + if (!json.has("key")) { + json.put("key", "") + modified = true + } + if (!json.has("scene") || json.optJSONObject("scene") == null) { + json.put("scene", defaultTemplate.getJSONObject("scene")) + modified = true + } + val avatar = json.optJSONObject("avatar") + if (avatar == null) { + json.put("avatar", defaultTemplate.getJSONObject("avatar")) + modified = true + } else { + val defaultAvatar = defaultTemplate.getJSONObject("avatar") + val keys = listOf("tb_gender", "tb_path", "march_path", "skin", "lineup", "global_passive", "buff", "leader") + for (k in keys) { + if (!avatar.has(k) || avatar.isNull(k)) { + avatar.put(k, defaultAvatar.get(k)) + modified = true + } + } + } + + if (modified) { + persistentFile.writeText(json.toString(4), StandardCharsets.UTF_8) + } + return json + } catch (e: Exception) { + val fallback = createDefaultPersistentJson() + try { + persistentFile.writeText(fallback.toString(4), StandardCharsets.UTF_8) + } catch (_: Exception) {} + return fallback + } + } + suspend fun readPersistentJson(): Result = withContext(Dispatchers.IO) { try { - if (!persistentFile.exists()) { - return@withContext Result.failure(FileNotFoundException("persistent.json not found")) - } - val text = persistentFile.readText(StandardCharsets.UTF_8) - Result.success(JSONObject(text)) + val json = ensureValidPersistentJsonSync() + Result.success(json) } catch (e: Exception) { Result.failure(e) } @@ -59,6 +174,25 @@ class PersistentRepository(private val context: Context) { suspend fun writePersistentJson(json: JSONObject): Result = withContext(Dispatchers.IO) { try { miscDir.mkdirs() + val defaultTemplate = createDefaultPersistentJson() + if (!json.has("send_hotfix")) json.put("send_hotfix", true) + if (!json.has("key")) json.put("key", "") + if (!json.has("scene") || json.optJSONObject("scene") == null) { + json.put("scene", defaultTemplate.getJSONObject("scene")) + } + val avatar = json.optJSONObject("avatar") + if (avatar == null) { + json.put("avatar", defaultTemplate.getJSONObject("avatar")) + } else { + val defaultAvatar = defaultTemplate.getJSONObject("avatar") + val keys = listOf("tb_gender", "tb_path", "march_path", "skin", "lineup", "global_passive", "buff", "leader") + for (k in keys) { + if (!avatar.has(k) || avatar.isNull(k)) { + avatar.put(k, defaultAvatar.get(k)) + } + } + } + val backupFile = File(miscDir, "persistent.json.bak") if (persistentFile.exists()) { persistentFile.copyTo(backupFile, overwrite = true) @@ -85,7 +219,7 @@ class PersistentRepository(private val context: Context) { } suspend fun setSendHotfix(enabled: Boolean): Result { - val json = readPersistentJson().getOrElse { JSONObject() } + val json = readPersistentJson().getOrElse { createDefaultPersistentJson() } json.put("send_hotfix", enabled) return writePersistentJson(json) } @@ -95,7 +229,7 @@ class PersistentRepository(private val context: Context) { } suspend fun setServerKey(key: String): Result { - val json = readPersistentJson().getOrElse { JSONObject() } + val json = readPersistentJson().getOrElse { createDefaultPersistentJson() } json.put("key", key) return writePersistentJson(json) } @@ -105,13 +239,13 @@ class PersistentRepository(private val context: Context) { val avatar = json?.optJSONObject("avatar") return TrailblazerConfig( gender = avatar?.optString("tb_gender", "Female") ?: "Female", - tbPath = avatar?.optString("tb_path", "Destruction") ?: "Destruction", + tbPath = avatar?.optString("tb_path", "Elation") ?: "Elation", marchPath = avatar?.optString("march_path", "Preservation") ?: "Preservation" ) } suspend fun setTrailblazerConfig(config: TrailblazerConfig): Result { - val json = readPersistentJson().getOrElse { JSONObject() } + val json = readPersistentJson().getOrElse { createDefaultPersistentJson() } val avatar = json.optJSONObject("avatar") ?: JSONObject().also { json.put("avatar", it) } avatar.put("tb_gender", config.gender) avatar.put("tb_path", config.tbPath) @@ -120,7 +254,7 @@ class PersistentRepository(private val context: Context) { } suspend fun applySceneTeleport(payload: SceneTeleportPayload): Result { - val json = readPersistentJson().getOrElse { JSONObject() } + val json = readPersistentJson().getOrElse { createDefaultPersistentJson() } val scene = json.optJSONObject("scene") ?: JSONObject().also { json.put("scene", it) } scene.put("plane_id", payload.planeId) diff --git a/app/src/main/java/com/catlas/escapepenacony/data/UpdateRepository.kt b/app/src/main/java/com/catlas/escapepenacony/data/UpdateRepository.kt index 5b8f4c3..178dff8 100644 --- a/app/src/main/java/com/catlas/escapepenacony/data/UpdateRepository.kt +++ b/app/src/main/java/com/catlas/escapepenacony/data/UpdateRepository.kt @@ -11,6 +11,7 @@ import java.net.HttpURLConnection import java.net.URL import java.util.zip.ZipEntry import java.util.zip.ZipInputStream +import org.json.JSONObject data class UpdateInfo( val currentVersion: String, @@ -187,7 +188,28 @@ class UpdateRepository(private val context: Context) { val stagedMiscDir = File(effectiveStagedDir, "misc").apply { mkdirs() } if (miscBackupFile.exists()) { - miscBackupFile.copyTo(File(stagedMiscDir, "persistent.json"), overwrite = true) + try { + val defaultTemplate = JSONObject(PersistentRepository.DEFAULT_PERSISTENT_JSON_STRING) + val backedUpText = miscBackupFile.readText() + val backedUpJson = JSONObject(backedUpText) + if (backedUpJson.has("send_hotfix")) defaultTemplate.put("send_hotfix", backedUpJson.getBoolean("send_hotfix")) + if (backedUpJson.has("key")) defaultTemplate.put("key", backedUpJson.getString("key")) + if (backedUpJson.has("scene") && backedUpJson.optJSONObject("scene") != null) { + defaultTemplate.put("scene", backedUpJson.getJSONObject("scene")) + } + val backedUpAvatar = backedUpJson.optJSONObject("avatar") + if (backedUpAvatar != null) { + val avatar = defaultTemplate.getJSONObject("avatar") + val keys = backedUpAvatar.keys() + while (keys.hasNext()) { + val k = keys.next() + avatar.put(k, backedUpAvatar.get(k)) + } + } + File(stagedMiscDir, "persistent.json").writeText(defaultTemplate.toString(4)) + } catch (e: Exception) { + miscBackupFile.copyTo(File(stagedMiscDir, "persistent.json"), overwrite = true) + } } if (configBackupFile.exists()) { diff --git a/app/src/main/java/com/catlas/escapepenacony/service/ServerService.kt b/app/src/main/java/com/catlas/escapepenacony/service/ServerService.kt index 0f15f97..934532b 100644 --- a/app/src/main/java/com/catlas/escapepenacony/service/ServerService.kt +++ b/app/src/main/java/com/catlas/escapepenacony/service/ServerService.kt @@ -161,6 +161,13 @@ class ServerService : Service() { } } + try { + val persistentRepo = com.catlas.escapepenacony.data.PersistentRepository(this) + persistentRepo.ensureValidPersistentJsonSync() + } catch (e: Exception) { + appendLog("[Warning] Persistent validation notice: ${e.message}") + } + appendLog("[System] Launching ppSR Python 3.14 Server...") // Start readiness check in background diff --git a/app/src/main/java/com/catlas/escapepenacony/ui/MainScreen.kt b/app/src/main/java/com/catlas/escapepenacony/ui/MainScreen.kt index 81ea54f..7918ece 100644 --- a/app/src/main/java/com/catlas/escapepenacony/ui/MainScreen.kt +++ b/app/src/main/java/com/catlas/escapepenacony/ui/MainScreen.kt @@ -129,6 +129,7 @@ fun MainScreen() { } if (isServerInstalled) { + persistentRepo.ensureValidPersistentJsonSync() sendHotfix = persistentRepo.getSendHotfix() serverKey = persistentRepo.getServerKey() trailblazerConfig = persistentRepo.getTrailblazerConfig() diff --git a/app/src/main/python/run_ppsr.py b/app/src/main/python/run_ppsr.py index 1cf8f3f..03294bd 100644 --- a/app/src/main/python/run_ppsr.py +++ b/app/src/main/python/run_ppsr.py @@ -136,6 +136,145 @@ try: except Exception: print("[Shim] marshmallow not available") +# --- 3.1 Persistent Template & SafeDict Protection --- +DEFAULT_PERSISTENT = { + "send_hotfix": True, + "scene": { + "plane_id": 20313, + "x": 40748, + "y": 192819, + "z": 439218, + "calyx_x": 40748, + "calyx_y": 192810, + "calyx_z": 441000, + "map_layer": 2, + "calyx_group_id": 186, + "calyx_inst_id": 300001, + "calyx_entity_id": 1337, + "calyx_prop_id": 808 + }, + "avatar": { + "tb_gender": "Female", + "tb_path": "Elation", + "march_path": "Preservation", + "skin": { + "1001": 1100101, + "1310": 1131001, + "1415": 1141501, + "1407": 1140701, + "1501": 1150101 + }, + "lineup": [ + 1511, + 1502, + 1506, + 1503 + ], + "global_passive": [ + { + "name": "Castorice Global Passive", + "id": 140703, + "active": True + }, + { + "name": "SW999 Global Passsive", + "id": 150602, + "active": True + }, + { + "name": "Aha Global Passive", + "id": 151104, + "active": True + } + ], + "buff": [ + 1000121 + ], + "leader": 0 + }, + "key": "" +} + +class SafeDict(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for k, v in list(self.items()): + if isinstance(v, dict) and not isinstance(v, SafeDict): + self[k] = SafeDict(v) + + def __getitem__(self, key): + import copy + if key not in self: + if key == 'avatar': + self['avatar'] = SafeDict(copy.deepcopy(DEFAULT_PERSISTENT['avatar'])) + return self['avatar'] + elif key == 'scene': + self['scene'] = SafeDict(copy.deepcopy(DEFAULT_PERSISTENT['scene'])) + return self['scene'] + elif key == 'skin': + self['skin'] = SafeDict(copy.deepcopy(DEFAULT_PERSISTENT['avatar']['skin'])) + return self['skin'] + elif key == 'lineup': + self['lineup'] = copy.deepcopy(DEFAULT_PERSISTENT['avatar']['lineup']) + return self['lineup'] + elif key == 'buff': + self['buff'] = copy.deepcopy(DEFAULT_PERSISTENT['avatar']['buff']) + return self['buff'] + elif key == 'global_passive': + self['global_passive'] = copy.deepcopy(DEFAULT_PERSISTENT['avatar']['global_passive']) + return self['global_passive'] + elif key == 'tb_gender': + return 'Female' + elif key == 'tb_path': + return 'Elation' + elif key == 'march_path': + return 'Preservation' + elif key == 'leader': + return 0 + elif key == 'send_hotfix': + return True + elif key == 'key': + return '' + val = super().__getitem__(key) + if isinstance(val, dict) and not isinstance(val, SafeDict): + val = SafeDict(val) + self[key] = val + return val + +def _sanitize_persistent_file(persistent_file): + try: + import json, os, copy + os.makedirs(os.path.dirname(os.path.abspath(persistent_file)), exist_ok=True) + data = {} + if os.path.exists(persistent_file): + try: + with open(persistent_file, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception: + data = {} + + modified = False + if not isinstance(data, dict): + data = {} + modified = True + + for k, v in DEFAULT_PERSISTENT.items(): + if k not in data: + data[k] = copy.deepcopy(v) + modified = True + elif isinstance(v, dict) and isinstance(data[k], dict): + for sub_k, sub_v in v.items(): + if sub_k not in data[k]: + data[k][sub_k] = copy.deepcopy(sub_v) + modified = True + + if modified or not os.path.exists(persistent_file): + with open(persistent_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) + print("[Patch] persistent.json validated and auto-healed with complete avatar/scene defaults") + except Exception as e: + print(f"[Patch Warning] sanitize persistent: {e}") + # --- 4. Shutdown Logic --- def kill_server_process(): global _active_server_instance @@ -264,6 +403,45 @@ def run_server(ppsr_path, activity_context=None): except Exception as e: print(f"[Patch Warning] Werkzeug patch: {e}") + # --- Persistent & Avatar Auto-healing Patch --- + _persistent_file = os.path.join(misc_dir, "persistent.json") + _sanitize_persistent_file(_persistent_file) + + try: + import game.services.manager as _mgr_mod + _orig_load_persistent = _mgr_mod.Manager.load_persistent + + @classmethod + def _patched_load_persistent(cls): + _orig_load_persistent() + if hasattr(cls, 'persistent') and isinstance(cls.persistent, dict): + cls.persistent = SafeDict(cls.persistent) + + _mgr_mod.Manager.load_persistent = _patched_load_persistent + if hasattr(_mgr_mod.Manager, 'persistent') and isinstance(_mgr_mod.Manager.persistent, dict): + _mgr_mod.Manager.persistent = SafeDict(_mgr_mod.Manager.persistent) + print("[Patch] Manager.load_persistent patched with SafeDict avatar protection") + except Exception as e: + print(f"[Patch Warning] Manager persistent patch: {e}") + + try: + import game.router as _router_mod + _orig_router_handle = _router_mod.handle + + def _safe_router_handle(data): + try: + return _orig_router_handle(data) + except Exception as e: + import traceback + print(f"[Router Warning] Handled exception in router.handle: {e}") + traceback.print_exc() + return b'' + + _router_mod.handle = _safe_router_handle + print("[Patch] game.router.handle wrapped with crash-guard") + except Exception as e: + print(f"[Patch Warning] Router crash-guard: {e}") + print(f"Loading bytecode target: {ppsr_path}...") with open(ppsr_path, "rb") as f: