3 Commits
41 changed files with 6133 additions and 57 deletions

No files matched your search

+3
View File
@@ -16,3 +16,6 @@ local.properties
app/build/ app/build/
*.apk *.apk
*.aab *.aab
__pycache__/
*.pyc
+1 -1
View File
@@ -1,4 +1,4 @@
# 📱 GaCy Android # 📱 GaCy Android (if you have client and this ps is working pls tell me)
<div align="center"> <div align="center">
+9
View File
@@ -22,6 +22,13 @@ android {
} }
} }
sourceSets {
getByName("main") {
jniLibs.srcDirs("src/main/jniLibs")
assets.srcDirs("src/main/assets")
}
}
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = false
@@ -46,6 +53,8 @@ android {
} }
dependencies { dependencies {
implementation(files("libs/chaquopy_java.jar"))
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation("androidx.lifecycle:lifecycle-service:2.8.6") implementation("androidx.lifecycle:lifecycle-service:2.8.6")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+23
View File
@@ -0,0 +1,23 @@
{
"assets": {
"bootstrap-native/arm64-v8a/_struct.cpython-314-aarch64-linux-android.so": "9b083e7b97e92291f73d80d7495c9cee8351596b",
"requirements-arm64-v8a.imy": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33",
"bootstrap-native/arm64-v8a/java/chaquopy.so": "1284a42a1b74c8fc5c8705a4f6f309134ecef4a8",
"bootstrap.imy": "6c6d7c85e4764e96924001f9d6290501bfbfc704",
"bootstrap-native/arm64-v8a/math.cpython-314-aarch64-linux-android.so": "dc859c93d3fc01f2d087471052895f230f732436",
"cacert.pem": "fec7b8c0a17c1c555fd33baee53678f31b7645ee",
"bootstrap-native/arm64-v8a/_bz2.cpython-314-aarch64-linux-android.so": "b48b9abf1f87d587a5f5beb4b38bb17acf29c4d3",
"bootstrap-native/arm64-v8a/_ctypes.cpython-314-aarch64-linux-android.so": "50a6a36dab6a9ebe4fca5797872e8d2c3538db56",
"bootstrap-native/arm64-v8a/mmap.cpython-314-aarch64-linux-android.so": "b571492fdceac1279bedc8163e27ac205e104b14",
"app.imy": "8e998f6e20dc7152ab075f8edf0644d411cd7f76",
"stdlib-common.imy": "8715635411fe145000fbf3b7ce4c3a085646d300",
"bootstrap-native/arm64-v8a/_random.cpython-314-aarch64-linux-android.so": "1c6c12f9f43546da0e108031f824b7fd6e79a36a",
"bootstrap-native/arm64-v8a/_lzma.cpython-314-aarch64-linux-android.so": "6f3f86f607b4d775452c128858fa26d19c7da56a",
"bootstrap-native/arm64-v8a/zlib.cpython-314-aarch64-linux-android.so": "2d164f648a0f97c1d8710d41cb66754e9daff44e",
"bootstrap-native/arm64-v8a/binascii.cpython-314-aarch64-linux-android.so": "ff4d2c47c081af6518c8a6a6fa07edcc077b1b6b",
"stdlib-arm64-v8a.imy": "ebebf5dc919d7e686936904604fe9a29fede81e9",
"requirements-common.imy": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33"
},
"extract_packages": [],
"python_version": "3.14"
}
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,6 +4,8 @@ import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.os.Build import android.os.Build
import com.chaquo.python.Python
import com.chaquo.python.android.AndroidPlatform
class GaCyApplication : Application() { class GaCyApplication : Application() {
@@ -18,6 +20,15 @@ class GaCyApplication : Application() {
super.onCreate() super.onCreate()
instance = this instance = this
createNotificationChannel() createNotificationChannel()
// Initialize embedded Python runtime
try {
if (!Python.isStarted()) {
Python.start(AndroidPlatform(this))
}
} catch (t: Throwable) {
t.printStackTrace()
}
} }
private fun createNotificationChannel() { private fun createNotificationChannel() {
@@ -16,7 +16,7 @@ import java.nio.charset.StandardCharsets
class PersistentRepository(private val context: Context) { class PersistentRepository(private val context: Context) {
val serverDataDir: File val serverDataDir: File
get() = File(context.filesDir, "gacy_server") get() = File(context.filesDir, "server/build/build")
val miscDir: File val miscDir: File
get() = File(serverDataDir, "misc") get() = File(serverDataDir, "misc")
@@ -29,7 +29,41 @@ class PersistentRepository(private val context: Context) {
} }
fun isServerInstalled(): Boolean { fun isServerInstalled(): Boolean {
return File(serverDataDir, "gameserver").exists() || File(serverDataDir, "ppsr.pyc").exists() return File(serverDataDir, "ppsr.pyc").exists() && File(serverDataDir, "ppsr.pyc").length() > 0
}
fun ensureValidPersistentJsonSync() {
try {
ensureDataStructure()
if (persistentFile.exists()) {
val text = persistentFile.readText(StandardCharsets.UTF_8)
val json = JSONObject(text)
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) {
val defaultObj = JSONObject(DEFAULT_PERSISTENT_JSON_STRING)
json.put("scene", defaultObj.getJSONObject("scene"))
modified = true
}
if (!json.has("avatar") || json.optJSONObject("avatar") == null) {
val defaultObj = JSONObject(DEFAULT_PERSISTENT_JSON_STRING)
json.put("avatar", defaultObj.getJSONObject("avatar"))
modified = true
}
if (modified) {
persistentFile.writeText(json.toString(2), StandardCharsets.UTF_8)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
} }
private fun ensureDataStructure() { private fun ensureDataStructure() {
@@ -0,0 +1,361 @@
package com.gacy.android.data
import android.content.Context
import android.content.SharedPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.BufferedInputStream
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
data class UpdateInfo(
val currentVersion: String,
val latestVersion: String,
val availableVersions: List<String>,
val hasUpdate: Boolean
)
class UpdateRepository(private val context: Context) {
private val prefs: SharedPreferences =
context.getSharedPreferences("gacy_updater_prefs", Context.MODE_PRIVATE)
companion object {
const val BASE_URL = "https://file.garden/aewBviVxx2XDWgM-/"
const val VERSIONS_URL = "https://file.garden/aewBviVxx2XDWgM-/VERSIONS"
const val PREF_INSTALLED_VERSION = "installed_server_version"
const val CONNECT_TIMEOUT_MS = 15000
const val READ_TIMEOUT_MS = 15000
}
fun getInstalledVersion(): String {
return prefs.getString(PREF_INSTALLED_VERSION, "4.6.53-embedded") ?: "4.6.53-embedded"
}
fun setInstalledVersion(version: String) {
prefs.edit().putString(PREF_INSTALLED_VERSION, version).apply()
}
fun isServerInstalled(): Boolean {
val ppsrFile = File(context.filesDir, "server/build/build/ppsr.pyc")
return ppsrFile.exists() && ppsrFile.length() > 0
}
suspend fun ensureServerInstalled(): Boolean = withContext(Dispatchers.IO) {
val ppsrFile = File(context.filesDir, "server/build/build/ppsr.pyc")
if (ppsrFile.exists() && ppsrFile.length() > 0) {
return@withContext true
}
try {
val stagingDir = File(context.filesDir, "server/staging")
val targetServerDir = File(context.filesDir, "server/build/build")
if (stagingDir.exists()) stagingDir.deleteRecursively()
stagingDir.mkdirs()
// 1. Extract build.zip from bundled assets
extractAssetZip("build.zip", stagingDir)
val stagedBuildDir = File(stagingDir, "build")
val effectiveStagedDir = if (stagedBuildDir.exists()) stagedBuildDir else stagingDir
// 2. Extract runtime packages into effective directory
try {
extractAssetZip("packages.zip", effectiveStagedDir)
} catch (e: Exception) {
e.printStackTrace()
}
// 3. Move/Rename into place
if (targetServerDir.exists()) {
targetServerDir.deleteRecursively()
}
targetServerDir.parentFile?.mkdirs()
val success = effectiveStagedDir.renameTo(targetServerDir)
if (!success) {
copyDirectory(effectiveStagedDir, targetServerDir)
effectiveStagedDir.deleteRecursively()
}
if (stagingDir.exists()) stagingDir.deleteRecursively()
setInstalledVersion("4.6.53-embedded")
return@withContext ppsrFile.exists()
} catch (e: Exception) {
e.printStackTrace()
return@withContext false
}
}
suspend fun checkUpdate(): Result<UpdateInfo> = withContext(Dispatchers.IO) {
try {
val versions = fetchAvailableVersions().getOrThrow()
if (versions.isEmpty()) {
return@withContext Result.failure(IllegalStateException("No versions found from server"))
}
val latestVersion = versions.last()
val installed = getInstalledVersion()
val hasUpdate = installed.isBlank() || installed != latestVersion
Result.success(
UpdateInfo(
currentVersion = installed,
latestVersion = latestVersion,
availableVersions = versions,
hasUpdate = hasUpdate
)
)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun fetchAvailableVersions(): Result<List<String>> = withContext(Dispatchers.IO) {
try {
val url = URL(VERSIONS_URL)
val conn = (url.openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
connectTimeout = CONNECT_TIMEOUT_MS
readTimeout = READ_TIMEOUT_MS
setRequestProperty("User-Agent", "GaCy-Updater/1.0")
}
if (conn.responseCode != HttpURLConnection.HTTP_OK) {
return@withContext Result.failure(
IllegalStateException("HTTP error ${conn.responseCode}: ${conn.responseMessage}")
)
}
val rawText = conn.inputStream.bufferedReader().use { it.readText() }
val list = rawText.split(Regex("[\\r\\n,]+"))
.map { it.trim() }
.filter { it.isNotBlank() }
.distinct()
Result.success(list)
} catch (e: Exception) {
Result.failure(e)
}
}
fun buildDownloadUrl(version: String, filename: String = "build.zip"): String {
val sanitizedVersion = version.trim()
if (sanitizedVersion.contains("/") || sanitizedVersion.contains("\\") || sanitizedVersion.contains("..")) {
throw IllegalArgumentException("Invalid version format")
}
return "$BASE_URL$sanitizedVersion/$filename"
}
suspend fun downloadAndInstall(
version: String,
onProgress: (Float, String) -> Unit
): Result<Unit> = withContext(Dispatchers.IO) {
val tempZip = File(context.cacheDir, "_temp_build_${System.currentTimeMillis()}.zip")
val stagingDir = File(context.filesDir, "server/staging")
val targetServerDir = File(context.filesDir, "server/build/build")
val miscBackupFile = File(context.filesDir, "server/persistent_backup.json")
val configBackupFile = File(context.filesDir, "server/config_backup.json")
try {
onProgress(0.05f, "Connecting to download server...")
val downloadUrl = buildDownloadUrl(version, "build.zip")
val url = URL(downloadUrl)
val conn = (url.openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
connectTimeout = CONNECT_TIMEOUT_MS
readTimeout = READ_TIMEOUT_MS
setRequestProperty("User-Agent", "GaCy-Updater/1.0")
}
if (conn.responseCode != HttpURLConnection.HTTP_OK) {
return@withContext Result.failure(
IllegalStateException("Failed to download package: HTTP ${conn.responseCode}")
)
}
val contentLength = conn.contentLengthLong
val inputStream = BufferedInputStream(conn.inputStream)
val outputStream = FileOutputStream(tempZip)
val buffer = ByteArray(8192)
var totalBytesRead = 0L
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
totalBytesRead += bytesRead
if (contentLength > 0) {
val progress = 0.1f + (totalBytesRead.toFloat() / contentLength.toFloat()) * 0.5f
val mbRead = totalBytesRead / (1024 * 1024f)
val mbTotal = contentLength / (1024 * 1024f)
onProgress(progress, String.format("Downloading: %.1f MB / %.1f MB", mbRead, mbTotal))
} else {
onProgress(0.3f, "Downloading package...")
}
}
outputStream.flush()
outputStream.close()
inputStream.close()
onProgress(0.65f, "Backing up user configuration...")
val existingPersistent = File(targetServerDir, "misc/persistent.json")
if (existingPersistent.exists()) {
existingPersistent.copyTo(miscBackupFile, overwrite = true)
}
val existingConfig = File(targetServerDir, "misc/config.json")
if (existingConfig.exists()) {
existingConfig.copyTo(configBackupFile, overwrite = true)
}
onProgress(0.70f, "Preparing staging directory...")
if (stagingDir.exists()) {
stagingDir.deleteRecursively()
}
stagingDir.mkdirs()
onProgress(0.75f, "Extracting server archive...")
extractZipSafely(tempZip, stagingDir)
val stagedBuildDir = File(stagingDir, "build")
val effectiveStagedDir = if (stagedBuildDir.exists()) stagedBuildDir else stagingDir
onProgress(0.80f, "Installing server runtime dependencies...")
try {
extractAssetZip("packages.zip", effectiveStagedDir)
} catch (e: Exception) {
// Non-fatal if asset missing
}
onProgress(0.85f, "Restoring user data...")
val stagedMiscDir = File(effectiveStagedDir, "misc").apply { mkdirs() }
if (miscBackupFile.exists()) {
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()) {
configBackupFile.copyTo(File(stagedMiscDir, "config.json"), overwrite = true)
}
onProgress(0.92f, "Finalizing installation...")
if (targetServerDir.exists()) {
targetServerDir.deleteRecursively()
}
targetServerDir.parentFile?.mkdirs()
val success = effectiveStagedDir.renameTo(targetServerDir)
if (!success) {
copyDirectory(effectiveStagedDir, targetServerDir)
effectiveStagedDir.deleteRecursively()
}
setInstalledVersion(version)
onProgress(1.0f, "Installation complete!")
Result.success(Unit)
} catch (e: Exception) {
onProgress(0f, "Installation error: ${e.message}")
Result.failure(e)
} finally {
if (tempZip.exists()) tempZip.delete()
if (stagingDir.exists()) stagingDir.deleteRecursively()
}
}
fun extractAssetZip(assetName: String, outputDir: File) {
context.assets.open(assetName).use { inputStream ->
ZipInputStream(BufferedInputStream(inputStream)).use { zis ->
var entry: ZipEntry? = zis.nextEntry
while (entry != null) {
val newFile = File(outputDir, entry.name)
val canonicalDestPath = outputDir.canonicalPath
val canonicalNewFilePath = newFile.canonicalPath
if (!canonicalNewFilePath.startsWith(canonicalDestPath + File.separator) &&
canonicalNewFilePath != canonicalDestPath
) {
throw SecurityException("Zip Slip detected: ${entry.name}")
}
if (entry.isDirectory) {
newFile.mkdirs()
} else {
newFile.parentFile?.mkdirs()
FileOutputStream(newFile).use { fos ->
zis.copyTo(fos)
}
}
zis.closeEntry()
entry = zis.nextEntry
}
}
}
}
private fun extractZipSafely(zipFile: File, outputDir: File) {
ZipInputStream(zipFile.inputStream().buffered()).use { zis ->
var entry: ZipEntry? = zis.nextEntry
while (entry != null) {
val newFile = File(outputDir, entry.name)
val canonicalDestPath = outputDir.canonicalPath
val canonicalNewFilePath = newFile.canonicalPath
if (!canonicalNewFilePath.startsWith(canonicalDestPath + File.separator) &&
canonicalNewFilePath != canonicalDestPath
) {
throw SecurityException("Zip Slip detected: ${entry.name}")
}
if (entry.isDirectory) {
newFile.mkdirs()
} else {
newFile.parentFile?.mkdirs()
FileOutputStream(newFile).use { fos ->
zis.copyTo(fos)
}
}
zis.closeEntry()
entry = zis.nextEntry
}
}
}
private fun copyDirectory(source: File, target: File) {
if (source.isDirectory) {
if (!target.exists()) target.mkdirs()
source.list()?.forEach { file ->
copyDirectory(File(source, file), File(target, file))
}
} else {
source.copyTo(target, overwrite = true)
}
}
}
@@ -9,6 +9,7 @@ enum class ServerStatus {
STOPPED, STOPPED,
STARTING, STARTING,
RUNNING, RUNNING,
STOPPING,
ERROR ERROR
} }
@@ -1,37 +1,62 @@
package com.gacy.android.service package com.gacy.android.service
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.lifecycle.LifecycleService import com.chaquo.python.Python
import androidx.lifecycle.lifecycleScope import com.chaquo.python.android.AndroidPlatform
import com.gacy.android.GaCyApplication import com.gacy.android.GaCyApplication
import com.gacy.android.MainActivity import com.gacy.android.MainActivity
import com.gacy.android.R import com.gacy.android.R
import com.gacy.android.data.PersistentRepository
import com.gacy.android.data.PreferencesRepository import com.gacy.android.data.PreferencesRepository
import com.gacy.android.data.UpdateRepository
import com.gacy.android.model.ServerExecutionMode import com.gacy.android.model.ServerExecutionMode
import com.gacy.android.model.ServerStatus import com.gacy.android.model.ServerStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
import java.net.InetSocketAddress
import java.net.Socket
class ServerService : LifecycleService() { class ServerLogReceiver {
fun onServerLog(msg: String) {
LogBuffer.append(msg)
}
}
private var serverJob: Job? = null class ServerService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var serverThread: Thread? = null
private var probeJob: Job? = null
private var executionJob: Job? = null
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
createNotificationChannel()
_serverStatus.value = ServerStatus.STOPPED _serverStatus.value = ServerStatus.STOPPED
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val action = intent?.action ?: ACTION_START val action = intent?.action ?: ACTION_START
when (action) { when (action) {
@@ -48,61 +73,194 @@ class ServerService : LifecycleService() {
return return
} }
val prefs = PreferencesRepository(this) try {
val config = prefs.getServerConfig() val prefs = PreferencesRepository(this)
val config = prefs.getServerConfig()
_serverStatus.value = ServerStatus.STARTING _serverStatus.value = ServerStatus.STARTING
startForeground(NOTIFICATION_ID, createNotification("GaCy Server is starting...")) startForegroundCompat("GaCy Server is starting...")
LogBuffer.append("--- GaCy Server Starting ---") LogBuffer.append("--- GaCy Server Starting ---")
LogBuffer.append("Mode: ${config.executionMode.displayNameEn}") LogBuffer.append("Mode: ${config.executionMode.displayNameEn}")
LogBuffer.append("Dispatch Port: ${config.dispatchPort} | Gameserver Port: ${config.gameserverPort}") LogBuffer.append("Dispatch Port: ${config.dispatchPort} | Gameserver Port: ${config.gameserverPort}")
serverJob = lifecycleScope.launch { if (config.executionMode == ServerExecutionMode.LOCAL_EMBEDDED) {
try { startLocalServerExecution(config.dispatchPort)
if (config.executionMode == ServerExecutionMode.LOCAL_EMBEDDED) { } else {
LogBuffer.append("Initializing local server runtime...") startRemoteMonitor(config.remoteHost, config.dispatchPort)
delay(500)
LogBuffer.append("Loading resources and protocol definitions...")
delay(500)
LogBuffer.append("Dispatch service listening on 0.0.0.0:${config.dispatchPort}")
LogBuffer.append("Gateway KCP socket bound on 0.0.0.0:${config.gameserverPort}")
_serverStatus.value = ServerStatus.RUNNING
updateNotification("GaCy Server is RUNNING on :${config.dispatchPort}")
LogBuffer.append("GaCy Server is ready! Connect client via dispatch redirect.")
} else {
LogBuffer.append("Connecting to remote GaCy host at ${config.remoteHost}...")
delay(500)
_serverStatus.value = ServerStatus.RUNNING
updateNotification("Monitoring Remote GaCy Host at ${config.remoteHost}")
LogBuffer.append("Connected to remote GaCy server monitor.")
}
} catch (e: Exception) {
LogBuffer.append("Error starting server: ${e.message}")
_serverStatus.value = ServerStatus.ERROR
updateNotification("GaCy Server encountered an error")
} }
} catch (t: Throwable) {
LogBuffer.append("[Start Error] ${t.localizedMessage ?: t.message}")
t.printStackTrace()
_serverStatus.value = ServerStatus.ERROR
stopForegroundCompat()
stopSelf()
}
}
private fun startLocalServerExecution(dispatchPort: Int) {
executionJob?.cancel()
executionJob = serviceScope.launch {
try {
val updateRepo = UpdateRepository(this@ServerService)
if (!updateRepo.isServerInstalled()) {
LogBuffer.append("[Setup] Installing GaCy embedded server binaries from assets...")
val success = updateRepo.ensureServerInstalled()
if (!success) {
LogBuffer.append("[Error] Failed to install server binaries from assets.")
_serverStatus.value = ServerStatus.ERROR
stopForegroundCompat()
stopSelf()
return@launch
}
LogBuffer.append("[Setup] Server binaries installed successfully.")
}
val ppsrFile = File(filesDir, "server/build/build/ppsr.pyc")
if (!ppsrFile.exists()) {
LogBuffer.append("[Error] Server bytecode missing at: ${ppsrFile.absolutePath}")
_serverStatus.value = ServerStatus.ERROR
stopForegroundCompat()
stopSelf()
return@launch
}
try {
val persistentRepo = PersistentRepository(this@ServerService)
persistentRepo.ensureValidPersistentJsonSync()
} catch (e: Throwable) {
LogBuffer.append("[Warning] Persistent validation: ${e.message}")
}
LogBuffer.append("[System] Launching embedded GaCy Python server...")
// Background TCP port readiness probe
probeJob?.cancel()
probeJob = serviceScope.launch {
var attempts = 0
while (isActive && _serverStatus.value == ServerStatus.STARTING && attempts < 35) {
delay(1000)
attempts++
if (isPortOpen("127.0.0.1", dispatchPort)) {
_serverStatus.value = ServerStatus.RUNNING
updateNotification("GaCy Server is RUNNING on :$dispatchPort")
LogBuffer.append("[System] GaCy Dispatch server is ready on 127.0.0.1:$dispatchPort")
break
}
}
}
serverThread = Thread({
try {
if (!Python.isStarted()) {
Python.start(AndroidPlatform(applicationContext))
}
val py = Python.getInstance()
val runPpsr = py.getModule("run_ppsr")
val logReceiver = ServerLogReceiver()
runPpsr.callAttr("run_server", ppsrFile.absolutePath, logReceiver)
} catch (t: Throwable) {
LogBuffer.append("[Python Error] ${t.localizedMessage ?: t.message}")
t.printStackTrace()
} finally {
probeJob?.cancel()
_serverStatus.value = ServerStatus.STOPPED
LogBuffer.append("[System] Server stopped.")
stopForegroundCompat()
stopSelf()
}
}, "GaCy-PythonServer").apply {
start()
}
} catch (t: Throwable) {
LogBuffer.append("[Execution Error] ${t.localizedMessage ?: t.message}")
t.printStackTrace()
_serverStatus.value = ServerStatus.ERROR
stopForegroundCompat()
stopSelf()
}
}
}
private fun startRemoteMonitor(remoteHost: String, dispatchPort: Int) {
executionJob?.cancel()
executionJob = serviceScope.launch {
LogBuffer.append("[Remote] Connecting to remote GaCy host at $remoteHost:$dispatchPort...")
delay(600)
_serverStatus.value = ServerStatus.RUNNING
updateNotification("Monitoring Remote GaCy Host at $remoteHost")
LogBuffer.append("[Remote] Connected to remote GaCy server monitor.")
} }
} }
private fun stopServer() { private fun stopServer() {
LogBuffer.append("--- GaCy Server Stopping ---") LogBuffer.append("--- GaCy Server Stopping ---")
serverJob?.cancel() _serverStatus.value = ServerStatus.STOPPING
serverJob = null updateNotification("Stopping GaCy Server...")
_serverStatus.value = ServerStatus.STOPPED
stopForeground(STOP_FOREGROUND_REMOVE) probeJob?.cancel()
stopSelf() probeJob = null
executionJob?.cancel()
executionJob = null
serviceScope.launch {
try {
if (Python.isStarted()) {
val py = Python.getInstance()
val runPpsr = py.getModule("run_ppsr")
runPpsr.callAttr("kill_server_process")
}
} catch (e: Throwable) {
LogBuffer.append("[Stop Error] ${e.localizedMessage ?: e.message}")
}
delay(1200)
serverThread?.interrupt()
serverThread = null
_serverStatus.value = ServerStatus.STOPPED
stopForegroundCompat()
stopSelf()
}
} }
private fun restartServer() { private fun restartServer() {
LogBuffer.append("Restarting GaCy Server...") LogBuffer.append("Restarting GaCy Server...")
stopServer() stopServer()
lifecycleScope.launch { serviceScope.launch {
delay(800) delay(1500)
startServer() startServer()
} }
} }
private fun isPortOpen(host: String, port: Int): Boolean {
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress(host, port), 400)
true
}
} catch (_: Throwable) {
false
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
GaCyApplication.CHANNEL_ID,
GaCyApplication.CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Background service for GaCy Game Server & Proxy"
setShowBadge(false)
}
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(channel)
}
}
private fun createNotification(contentText: String): Notification { private fun createNotification(contentText: String): Notification {
createNotificationChannel()
val launchIntent = Intent(this, MainActivity::class.java).apply { val launchIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
} }
@@ -126,29 +284,63 @@ class ServerService : LifecycleService() {
return NotificationCompat.Builder(this, GaCyApplication.CHANNEL_ID) return NotificationCompat.Builder(this, GaCyApplication.CHANNEL_ID)
.setContentTitle("GaCy Android") .setContentTitle("GaCy Android")
.setContentText(contentText) .setContentText(contentText)
.setSmallIcon(R.mipmap.ic_launcher) .setSmallIcon(R.drawable.app_icon)
.setOngoing(true) .setOngoing(true)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopPendingIntent) .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopPendingIntent)
.build() .build()
} }
private fun startForegroundCompat(text: String) {
try {
createNotificationChannel()
val notification = createNotification(text)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 34+
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
)
} else {
// API 26..33
startForeground(NOTIFICATION_ID, notification)
}
} catch (e: Throwable) {
try {
val notification = createNotification(text)
startForeground(NOTIFICATION_ID, notification)
} catch (e2: Throwable) {
LogBuffer.append("[Foreground Warning] ${e2.message}")
}
}
}
private fun updateNotification(contentText: String) { private fun updateNotification(contentText: String) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager try {
manager.notify(NOTIFICATION_ID, createNotification(contentText)) val manager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
manager?.notify(NOTIFICATION_ID, createNotification(contentText))
} catch (_: Throwable) {}
}
private fun stopForegroundCompat() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION")
stopForeground(true)
}
} catch (_: Throwable) {}
} }
override fun onDestroy() { override fun onDestroy() {
probeJob?.cancel()
serverThread?.interrupt()
executionJob?.cancel()
super.onDestroy() super.onDestroy()
serverJob?.cancel()
_serverStatus.value = ServerStatus.STOPPED _serverStatus.value = ServerStatus.STOPPED
} }
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
companion object { companion object {
const val ACTION_START = "com.gacy.android.ACTION_START" const val ACTION_START = "com.gacy.android.ACTION_START"
const val ACTION_STOP = "com.gacy.android.ACTION_STOP" const val ACTION_STOP = "com.gacy.android.ACTION_STOP"
@@ -160,17 +352,45 @@ class ServerService : LifecycleService() {
fun start(context: Context) { fun start(context: Context) {
val intent = Intent(context, ServerService::class.java).apply { action = ACTION_START } val intent = Intent(context, ServerService::class.java).apply { action = ACTION_START }
context.startForegroundService(intent) try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
} catch (t: Throwable) {
try {
context.startService(intent)
} catch (t2: Throwable) {
LogBuffer.append("[Service Start Error] ${t2.message}")
}
}
} }
fun stop(context: Context) { fun stop(context: Context) {
val intent = Intent(context, ServerService::class.java).apply { action = ACTION_STOP } val intent = Intent(context, ServerService::class.java).apply { action = ACTION_STOP }
context.startService(intent) try {
context.startService(intent)
} catch (t: Throwable) {
LogBuffer.append("[Service Stop Error] ${t.message}")
}
} }
fun restart(context: Context) { fun restart(context: Context) {
val intent = Intent(context, ServerService::class.java).apply { action = ACTION_RESTART } val intent = Intent(context, ServerService::class.java).apply { action = ACTION_RESTART }
context.startForegroundService(intent) try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
} catch (t: Throwable) {
try {
context.startService(intent)
} catch (t2: Throwable) {
LogBuffer.append("[Service Restart Error] ${t2.message}")
}
}
} }
} }
} }
@@ -109,6 +109,7 @@ fun StatusBadge(status: ServerStatus, lang: AppLanguage) {
val (color, text) = when (status) { val (color, text) = when (status) {
ServerStatus.RUNNING -> Pair(GaCyGreen, tr(lang, "RUNNING", "กำลังทำงาน", "运行中")) ServerStatus.RUNNING -> Pair(GaCyGreen, tr(lang, "RUNNING", "กำลังทำงาน", "运行中"))
ServerStatus.STARTING -> Pair(GaCyAmber, tr(lang, "STARTING...", "กำลังเริ่ม...", "启动中...")) ServerStatus.STARTING -> Pair(GaCyAmber, tr(lang, "STARTING...", "กำลังเริ่ม...", "启动中..."))
ServerStatus.STOPPING -> Pair(GaCyAmber, tr(lang, "STOPPING...", "กำลังหยุด...", "停止中..."))
ServerStatus.STOPPED -> Pair(TextMuted, tr(lang, "STOPPED", "หยุดทำงาน", "已停止")) ServerStatus.STOPPED -> Pair(TextMuted, tr(lang, "STOPPED", "หยุดทำงาน", "已停止"))
ServerStatus.ERROR -> Pair(GaCyRed, tr(lang, "ERROR", "เกิดข้อผิดพลาด", "错误")) ServerStatus.ERROR -> Pair(GaCyRed, tr(lang, "ERROR", "เกิดข้อผิดพลาด", "错误"))
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
import importlib.util
import sys
import os
def load_pyc(path, module_name):
if not os.path.exists(path):
raise FileNotFoundError(f"PYC file not found at absolute path: {path}")
spec = importlib.util.spec_from_file_location(module_name, path)
if spec is None or spec.loader is None:
raise ImportError(f"Could not load spec for module {module_name} from {path}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
+155
View File
@@ -0,0 +1,155 @@
import os
import sys
import zipfile
import tarfile
import urllib.request
import json
import shutil
import traceback
import re
# Primary PyPI base URL and mirrors for China fallback
PYPI_BASES = [
"https://pypi.org/pypi", # Official PyPI
"https://pypi.tuna.tsinghua.edu.cn/pypi", # Tsinghua (TUNA)
"https://mirrors.aliyun.com/pypi", # Aliyun
]
def _parse_package_spec(package_spec):
match = re.match(r"^([a-zA-Z0-9_\-\.]+)(.*)$", package_spec.strip())
if match:
return match.group(1), match.group(2).strip()
return package_spec, ""
def _fetch_pypi_data(clean_name):
"""Attempt to fetch metadata JSON from PyPI and mirrors."""
for base_url in PYPI_BASES:
url = f"{base_url}/{clean_name}/json"
try:
print(f"Fetching metadata from: {base_url}…")
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(req, timeout=10) as response:
return json.loads(response.read().decode("utf-8"))
except Exception as e:
print(f"Failed to fetch metadata from {base_url}: {e}")
return None
def _download_file(file_info_urls, dest_path):
"""Download package file with mirror URL rewriting if official PyPI fails."""
# List of candidate download URLs (original URL + mirror URL replacements)
for file_info in file_info_urls:
original_url = file_info.get("url")
if not original_url:
continue
# Generate candidate download URLs for this file
candidate_urls = [original_url]
# Rewrite official PyPI file URLs to mirror locations if needed
if "files.pythonhosted.org" in original_url:
candidate_urls.append(
original_url.replace("https://files.pythonhosted.org", "https://pypi.tuna.tsinghua.edu.cn")
)
candidate_urls.append(
original_url.replace("https://files.pythonhosted.org", "https://mirrors.aliyun.com/pypi")
)
for dl_url in candidate_urls:
try:
print(f"Downloading from {dl_url}…")
dl_req = urllib.request.Request(dl_url, headers={'User-Agent': 'Mozilla/5.0'})
with urllib.request.urlopen(dl_req, timeout=30) as dl_resp, open(dest_path, "wb") as out_file:
out_file.write(dl_resp.read())
return True
except Exception as e:
print(f"Download failed from {dl_url}: {e}")
return False
def install_package(package_name, target_dir):
try:
clean_name, version_spec = _parse_package_spec(package_name)
print(f"Resolving PyPI metadata for {clean_name} (spec: '{version_spec}')…")
data = _fetch_pypi_data(clean_name)
if not data:
print(f"Could not retrieve PyPI metadata for {clean_name} from any mirror.")
return False
urls = data.get("urls", [])
if version_spec.startswith("=="):
req_version = version_spec.lstrip("=").strip()
releases = data.get("releases", {})
if req_version in releases:
urls = releases[req_version]
else:
print(f"Version {req_version} not found on PyPI for {clean_name}")
return False
target_file_infos = []
# 1. Prefer pure-Python wheels
for file_info in urls:
filename = file_info.get("filename", "")
if filename.endswith("py3-none-any.whl") or filename.endswith("py2.py3-none-any.whl"):
target_file_infos.append(file_info)
break
# 2. Fallback to source distribution
if not target_file_infos:
for file_info in urls:
if file_info.get("packagetype") == "sdist":
target_file_infos.append(file_info)
break
if not target_file_infos:
print(f"No compatible pure-Python release found for {clean_name}")
return False
if not os.path.exists(target_dir):
os.makedirs(target_dir, exist_ok=True)
safe_filename = clean_name.replace("/", "_")
dest_path = os.path.join(target_dir, f"_dl_{safe_filename}")
if not _download_file(target_file_infos, dest_path):
print(f"Failed to download payload for {clean_name} from all sources.")
return False
# Extract package files
if dest_path.endswith(".whl") or dest_path.endswith(".zip") or target_file_infos[0].get("filename", "").endswith((".whl", ".zip")):
with zipfile.ZipFile(dest_path, "r") as zip_ref:
zip_ref.extractall(target_dir)
else:
with tarfile.open(dest_path, "r:gz") as tar_ref:
tar_ref.extractall(target_dir)
if os.path.exists(dest_path):
os.remove(dest_path)
# Handle 'src' layout if present
for root, dirs, files in os.walk(target_dir):
if os.path.basename(root) == "src":
for d in dirs:
src_pkg = os.path.join(root, d)
dest_pkg = os.path.join(target_dir, d)
if not os.path.exists(dest_pkg):
shutil.move(src_pkg, dest_pkg)
# Cleanup __pycache__
for root, dirs, files in os.walk(target_dir):
for d in list(dirs):
if d == "__pycache__":
shutil.rmtree(os.path.join(root, d), ignore_errors=True)
if target_dir not in sys.path:
sys.path.insert(0, target_dir)
print(f"Successfully installed {clean_name}")
return True
except Exception as e:
print(f"Failed to install {package_name}: {e}")
traceback.print_exc()
return False
+504
View File
@@ -0,0 +1,504 @@
import sys
import os
import builtins
import time
import _thread
import gc
import socket
import struct
import warnings
# === GLOBAL WARNING SUPPRESSION & METADATA SHIM (very early) ===
try:
import importlib.metadata
_orig_metadata_version = importlib.metadata.version
_orig_metadata_distribution = importlib.metadata.distribution
_KNOWN_VERSIONS = {
"werkzeug": "3.1.9",
"flask": "3.1.3",
"marshmallow": "3.20.0",
"requests": "2.34.2",
"urllib3": "2.8.0",
"protobuf": "7.36.2",
"click": "8.5.0",
"jinja2": "3.1.6",
"itsdangerous": "2.2.0",
"blinker": "1.9.0",
"markupsafe": "3.0.4",
"dataclasses-json": "0.6.7",
"dataclasses_json": "0.6.7",
"packaging": "26.3"
}
class _ShimedDistribution:
def __init__(self, name, version):
self.name = name
self.version = version
self.metadata = {"Name": name, "Version": version}
def _safe_metadata_version(name):
try:
return _orig_metadata_version(name)
except Exception:
clean = name.lower().replace("-", "_")
return _KNOWN_VERSIONS.get(name.lower(), _KNOWN_VERSIONS.get(clean, "1.0.0"))
def _safe_metadata_distribution(name):
try:
return _orig_metadata_distribution(name)
except Exception:
ver = _safe_metadata_version(name)
return _ShimedDistribution(name, ver)
importlib.metadata.version = _safe_metadata_version
importlib.metadata.distribution = _safe_metadata_distribution
except Exception:
pass
warnings.filterwarnings("ignore", message=".*default.*deprecated.*")
warnings.filterwarnings("ignore", message=".*metadata.*deprecated.*")
warnings.filterwarnings("ignore", module="marshmallow.*")
warnings.filterwarnings("ignore", module="dataclasses_json.*")
try:
from marshmallow.warnings import RemovedInMarshmallow4Warning
warnings.filterwarnings("ignore", category=RemovedInMarshmallow4Warning)
warnings.simplefilter("ignore", category=RemovedInMarshmallow4Warning)
print("[Shim] Global warning suppression activated")
except Exception:
pass
_active_server_instance = None
# --- 1. Stream Redirector for Android UI ---
class AndroidLogStream:
def __init__(self, activity_context, original_stream):
self.activity_context = activity_context
self.original_stream = original_stream
self.buffer = ""
self._in_write = False
def write(self, message):
if self._in_write:
if self.original_stream:
try:
self.original_stream.write(message)
except Exception:
pass
return
self._in_write = True
try:
if self.original_stream:
try:
self.original_stream.write(message)
except Exception:
pass
self.buffer += message
while "\n" in self.buffer:
line, self.buffer = self.buffer.split("\n", 1)
clean_line = line.strip()
if clean_line and self.activity_context:
try:
self.activity_context.onServerLog(clean_line)
except Exception:
pass
finally:
self._in_write = False
def flush(self):
if self.original_stream:
try:
self.original_stream.flush()
except Exception:
pass
if self.buffer.strip() and self.activity_context:
try:
self.activity_context.onServerLog(self.buffer.strip())
self.buffer = ""
except Exception:
pass
# --- 2. Headless Input Mock ---
def _mock_input(prompt=""):
if prompt:
print(f"[CLI]: {prompt}")
time.sleep(2)
return ""
builtins.input = _mock_input
# --- 3. Minimal Marshmallow Info ---
try:
import marshmallow
print(f"[Shim] marshmallow {marshmallow.__version__}")
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
}
],
"ex_rank": [
{
"name": "JingYuan",
"avatar_id": 1204,
"active": True
},
{
"name": "DanHeng IL",
"avatar_id": 1213,
"active": True
},
{
"name": "Acheron",
"avatar_id": 1308,
"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 == 'ex_rank':
self['ex_rank'] = copy.deepcopy(DEFAULT_PERSISTENT['avatar']['ex_rank'])
return self['ex_rank']
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
print("[Shutdown]: Force-stopping server execution...")
try:
import dispatch.dispatchserver as _ds
if hasattr(_ds, 'DispatchServer') and hasattr(_ds.DispatchServer, 'stop'):
_ds.DispatchServer.stop()
except Exception:
pass
try:
import game.gameserver as _gs
if hasattr(_gs, 'GameServer') and hasattr(_gs.GameServer, 'stop'):
_gs.GameServer.stop()
except Exception:
pass
if _active_server_instance is not None:
try:
if hasattr(_active_server_instance, 'shutdown'):
_active_server_instance.shutdown()
if hasattr(_active_server_instance, 'server_close'):
_active_server_instance.server_close()
except Exception:
pass
try:
sock = getattr(_active_server_instance, 'socket', None)
if sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
sock.close()
except Exception:
pass
_active_server_instance = None
try:
for obj in gc.get_objects():
if isinstance(obj, socket.socket):
try:
obj.shutdown(socket.SHUT_RDWR)
obj.close()
except Exception:
pass
except Exception:
pass
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.close()
except Exception:
pass
time.sleep(1.2)
for _ in range(5):
try:
_thread.interrupt_main()
time.sleep(0.2)
except Exception:
pass
# --- 5. Main Execution Entrypoint ---
def run_server(ppsr_path, activity_context=None):
if activity_context is not None:
sys.stdout = AndroidLogStream(activity_context, sys.__stdout__)
sys.stderr = AndroidLogStream(activity_context, sys.__stderr__)
# Extra safety - re-apply on every start
try:
warnings.filterwarnings("ignore", category=RemovedInMarshmallow4Warning)
except Exception:
pass
ppsr_dir = os.path.dirname(os.path.abspath(ppsr_path))
misc_dir = os.path.join(ppsr_dir, "misc")
pkg_zip = os.path.join(ppsr_dir, "packages.zip")
if os.path.exists(ppsr_dir):
os.chdir(ppsr_dir)
print(f"Working directory locked: {os.getcwd()}")
for path in [ppsr_dir, misc_dir, pkg_zip]:
if os.path.exists(path) and path not in sys.path:
sys.path.insert(0, path)
warnings.filterwarnings("ignore", message=".*default.*deprecated.*")
warnings.filterwarnings("ignore", message=".*metadata.*deprecated.*")
warnings.filterwarnings("ignore", module="marshmallow.*")
warnings.filterwarnings("ignore", module="dataclasses_json.*")
# Verify and auto-recover critical packages if needed
for pkg_name in ["requests", "werkzeug", "flask"]:
try:
__import__(pkg_name)
except ImportError:
print(f"[Shim] Package '{pkg_name}' missing, attempting auto-install...")
try:
import package_fetcher
package_fetcher.install_package(pkg_name, ppsr_dir)
except Exception as ex:
print(f"[Shim Warning] Fallback failed for {pkg_name}: {ex}")
# --- Werkzeug Patch ---
try:
import werkzeug.serving
if hasattr(werkzeug.serving, "ThreadedWSGIServer"):
try:
werkzeug.serving.ThreadedWSGIServer._server_version = "Werkzeug/3.1.9"
except Exception:
pass
_orig_make_server = werkzeug.serving.make_server
def _patched_make_server(*args, **kwargs):
global _active_server_instance
kwargs.pop('options', None)
server = _orig_make_server(*args, **kwargs)
try:
server._server_version = "Werkzeug/3.1.9"
except Exception:
pass
try:
if hasattr(server, 'socket') and server.socket:
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
except Exception:
pass
_active_server_instance = server
print("[Server] Server instance patched and ready")
return server
werkzeug.serving.make_server = _patched_make_server
print("[Patch] Werkzeug make_server patched successfully")
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:
code_bytes = f.read()
try:
import marshal
code_obj = marshal.loads(code_bytes[16:])
except Exception:
code_obj = compile(code_bytes, ppsr_path, "exec")
main_globals = {
"__name__": "__main__",
"__file__": ppsr_path,
"__doc__": None,
"sys": sys,
"os": os,
"input": _mock_input
}
try:
exec(code_obj, main_globals)
except KeyboardInterrupt:
print("[System]: Server loop interrupted cleanly.")
except Exception as e:
print(f"[System]: Server terminated: {e}")