fix(server): resolve KeyError avatar (error 1001_1 / 1001_3) with SafeDict & persistent auto-healing
This commit is contained in:
1 parent
f439733781
commit
c7af4069c9
7 files changed
+355
-13
No files matched your search
@@ -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"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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<JSONObject> = 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<Unit> = 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<Unit> {
|
||||
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<Unit> {
|
||||
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<Unit> {
|
||||
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<Unit> {
|
||||
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)
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -129,6 +129,7 @@ fun MainScreen() {
|
||||
}
|
||||
|
||||
if (isServerInstalled) {
|
||||
persistentRepo.ensureValidPersistentJsonSync()
|
||||
sendHotfix = persistentRepo.getSendHotfix()
|
||||
serverKey = persistentRepo.getServerKey()
|
||||
trailblazerConfig = persistentRepo.getTrailblazerConfig()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user