Skip to content

Commit 218acdf

Browse files
authored
Merge pull request #38 from Ark-Repoleved/dev
Dev
2 parents 265b927 + a803d87 commit 218acdf

File tree

13 files changed

+1579
-513
lines changed

13 files changed

+1579
-513
lines changed

app/src/main/java/com/example/bd2modmanager/data/model/ModModels.kt

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,34 @@ package com.example.bd2modmanager.data.model
22

33
import android.net.Uri
44

5+
enum class ResolutionState {
6+
KNOWN,
7+
MISC,
8+
UNKNOWN,
9+
INVALID
10+
}
11+
12+
enum class MatchStrategy {
13+
EXACT,
14+
NORMALIZED,
15+
EXTENSION_MAPPING,
16+
FALLBACK,
17+
NONE
18+
}
19+
20+
data class ResolvedTarget(
21+
val originalFileName: String,
22+
val normalizedCandidates: List<String> = emptyList(),
23+
val resolvedAssetKey: String? = null,
24+
val resolvedBundleName: String? = null,
25+
val resolvedBundlePath: String? = null,
26+
val assetType: String? = null,
27+
val targetHash: String? = null,
28+
val familyKey: String? = null,
29+
val matchStrategy: MatchStrategy = MatchStrategy.NONE,
30+
val confidence: Float = 0f
31+
)
32+
533
data class ModInfo(
634
val name: String,
735
val character: String,
@@ -10,7 +38,13 @@ data class ModInfo(
1038
val isEnabled: Boolean,
1139
val uri: Uri,
1240
val targetHashedName: String?,
13-
val isDirectory: Boolean
41+
val isDirectory: Boolean,
42+
val resolutionState: ResolutionState = ResolutionState.UNKNOWN,
43+
val targetHash: String? = targetHashedName,
44+
val resolvedFamilyKey: String? = null,
45+
val resolvedTargets: List<ResolvedTarget> = emptyList(),
46+
val unresolvedFiles: List<String> = emptyList(),
47+
val errorReason: String? = null
1448
)
1549

1650
data class ModCacheInfo(
@@ -21,7 +55,12 @@ data class ModCacheInfo(
2155
val costume: String,
2256
val type: String,
2357
val targetHashedName: String?,
24-
val isDirectory: Boolean
58+
val isDirectory: Boolean,
59+
val resolutionState: ResolutionState = ResolutionState.UNKNOWN,
60+
val targetHash: String? = targetHashedName,
61+
val resolvedFamilyKey: String? = null,
62+
val unresolvedFiles: List<String> = emptyList(),
63+
val errorReason: String? = null
2564
)
2665

2766
data class CharacterInfo(val character: String, val costume: String, val type: String, val hashedName: String)
Lines changed: 126 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,126 @@
1-
package com.example.bd2modmanager.data.repository
2-
3-
import android.content.Context
4-
import com.chaquo.python.Python
5-
import com.example.bd2modmanager.data.model.CharacterInfo
6-
import kotlinx.coroutines.Dispatchers
7-
import kotlinx.coroutines.withContext
8-
import org.json.JSONArray
9-
import java.io.File
10-
11-
class CharacterRepository(private val context: Context) {
12-
13-
companion object {
14-
private const val CHARACTERS_JSON_FILENAME = "characters.json"
15-
private const val MOD_CACHE_FILENAME = "mod_cache.json"
16-
}
17-
18-
private var characterLut: Map<String, List<CharacterInfo>> = emptyMap()
19-
20-
suspend fun updateCharacterData(): Boolean {
21-
return withContext(Dispatchers.IO) {
22-
var success = false
23-
try {
24-
if (!Python.isStarted()) {
25-
Python.start(com.chaquo.python.android.AndroidPlatform(context))
26-
}
27-
val py = Python.getInstance()
28-
val mainScript = py.getModule("main_script")
29-
val result = mainScript.callAttr("update_character_data", context.filesDir.absolutePath).asList()
30-
val status = result[0].toString() // SUCCESS, SKIPPED, FAILED
31-
val message = result[1].toString()
32-
33-
when (status) {
34-
"SUCCESS" -> {
35-
println("Successfully ran scraper and saved characters.json: $message")
36-
// When a new characters.json is generated, the mod cache becomes invalid.
37-
val cacheFile = File(context.cacheDir, MOD_CACHE_FILENAME)
38-
if (cacheFile.exists()) {
39-
cacheFile.delete()
40-
println("Deleted mod cache to force re-scan.")
41-
}
42-
success = true
43-
}
44-
"SKIPPED" -> {
45-
println("Scraper skipped: $message")
46-
success = true
47-
}
48-
"FAILED" -> {
49-
println("Scraper script failed: $message. Will use local version if available.")
50-
success = false
51-
}
52-
}
53-
} catch (e: Exception) {
54-
e.printStackTrace()
55-
println("Failed to execute scraper python script, will use local version. Error: ${e.message}")
56-
success = false
57-
}
58-
characterLut = parseCharacterJson()
59-
success
60-
}
61-
}
62-
63-
private suspend fun parseCharacterJson(): Map<String, List<CharacterInfo>> {
64-
return withContext(Dispatchers.IO) {
65-
val lut = mutableMapOf<String, MutableList<CharacterInfo>>()
66-
val internalFile = File(context.filesDir, CHARACTERS_JSON_FILENAME)
67-
if (!internalFile.exists()) return@withContext emptyMap()
68-
val jsonString = try { internalFile.readText() } catch (e: Exception) { e.printStackTrace(); "" }
69-
if (jsonString.isNotEmpty()) {
70-
try {
71-
// Try parsing new format first
72-
val rootObject = org.json.JSONObject(jsonString)
73-
val jsonArray = rootObject.getJSONArray("characters")
74-
for (i in 0 until jsonArray.length()) {
75-
val obj = jsonArray.getJSONObject(i)
76-
val fileId = obj.getString("file_id").lowercase()
77-
val charInfo = CharacterInfo(obj.getString("character"), obj.getString("costume"), obj.getString("type"), obj.getString("hashed_name"))
78-
lut.getOrPut(fileId) { mutableListOf() }.add(charInfo)
79-
}
80-
} catch (e: org.json.JSONException) {
81-
// Fallback to old format
82-
try {
83-
val jsonArray = JSONArray(jsonString)
84-
for (i in 0 until jsonArray.length()) {
85-
val obj = jsonArray.getJSONObject(i)
86-
val fileId = obj.getString("file_id").lowercase()
87-
val charInfo = CharacterInfo(obj.getString("character"), obj.getString("costume"), obj.getString("type"), obj.getString("hashed_name"))
88-
lut.getOrPut(fileId) { mutableListOf() }.add(charInfo)
89-
}
90-
} catch (e2: Exception) {
91-
e2.printStackTrace()
92-
}
93-
} catch (e: Exception) {
94-
e.printStackTrace()
95-
}
96-
}
97-
lut
98-
}
99-
}
100-
101-
fun findBestMatch(fileId: String?, fileNames: List<String>): CharacterInfo? {
102-
if (fileId == null) return null
103-
104-
val candidates = characterLut[fileId] ?: return null
105-
if (candidates.size == 1) return candidates.first()
106-
if (candidates.isEmpty()) return null
107-
108-
val hasCutsceneKeyword = fileNames.any { it.contains("cutscene", ignoreCase = true) }
109-
if (hasCutsceneKeyword) {
110-
candidates.find { it.type == "cutscene" }?.let { return it }
111-
}
112-
113-
val validHashCandidates = candidates.filter { !it.hashedName.isNullOrBlank() }
114-
if (validHashCandidates.size == 1) {
115-
return validHashCandidates.first()
116-
}
117-
118-
candidates.find { it.type == "idle" }?.let { return it }
119-
120-
return candidates.first()
121-
}
122-
123-
fun extractFileId(entryName: String): String? {
124-
return "(char\\d{6}|illust_dating\\d+|illust_special\\d+|illust_talk\\d+|npc\\d+|specialillust\\w+|storypack\\w+|\\bRhythmHitAnim\\b)".toRegex(RegexOption.IGNORE_CASE).find(entryName)?.value?.lowercase()
125-
}
126-
}
1+
package com.example.bd2modmanager.data.repository
2+
3+
import android.content.Context
4+
import com.chaquo.python.Python
5+
import com.example.bd2modmanager.data.model.CharacterInfo
6+
import kotlinx.coroutines.Dispatchers
7+
import kotlinx.coroutines.withContext
8+
import org.json.JSONArray
9+
import java.io.File
10+
11+
class CharacterRepository(private val context: Context) {
12+
13+
companion object {
14+
private const val CHARACTERS_JSON_FILENAME = "characters.json"
15+
private const val MOD_CACHE_FILENAME = "mod_cache.json"
16+
}
17+
18+
private var characterLut: Map<String, List<CharacterInfo>> = emptyMap()
19+
20+
suspend fun updateCharacterData(): Boolean {
21+
return withContext(Dispatchers.IO) {
22+
var success = false
23+
try {
24+
if (!Python.isStarted()) {
25+
Python.start(com.chaquo.python.android.AndroidPlatform(context))
26+
}
27+
val py = Python.getInstance()
28+
val mainScript = py.getModule("main_script")
29+
val result = mainScript.callAttr("update_character_data", context.filesDir.absolutePath).asList()
30+
val status = result[0].toString() // SUCCESS, SKIPPED, FAILED
31+
val message = result[1].toString()
32+
33+
when (status) {
34+
"SUCCESS" -> {
35+
println("Successfully ran scraper and saved characters.json: $message")
36+
// When a new characters.json is generated, the mod cache becomes invalid.
37+
val cacheFile = File(context.filesDir, MOD_CACHE_FILENAME)
38+
if (cacheFile.exists()) {
39+
cacheFile.delete()
40+
println("Deleted mod cache to force re-scan.")
41+
}
42+
success = true
43+
}
44+
"SKIPPED" -> {
45+
println("Scraper skipped: $message")
46+
success = true
47+
}
48+
"FAILED" -> {
49+
println("Scraper script failed: $message. Will use local version if available.")
50+
success = false
51+
}
52+
}
53+
} catch (e: Exception) {
54+
e.printStackTrace()
55+
println("Failed to execute scraper python script, will use local version. Error: ${e.message}")
56+
success = false
57+
}
58+
characterLut = parseCharacterJson()
59+
success
60+
}
61+
}
62+
63+
private suspend fun parseCharacterJson(): Map<String, List<CharacterInfo>> {
64+
return withContext(Dispatchers.IO) {
65+
val lut = mutableMapOf<String, MutableList<CharacterInfo>>()
66+
val internalFile = File(context.filesDir, CHARACTERS_JSON_FILENAME)
67+
if (!internalFile.exists()) return@withContext emptyMap()
68+
val jsonString = try { internalFile.readText() } catch (e: Exception) { e.printStackTrace(); "" }
69+
if (jsonString.isNotEmpty()) {
70+
try {
71+
// Try parsing new format first
72+
val rootObject = org.json.JSONObject(jsonString)
73+
val jsonArray = rootObject.getJSONArray("characters")
74+
for (i in 0 until jsonArray.length()) {
75+
val obj = jsonArray.getJSONObject(i)
76+
val fileId = obj.getString("file_id").lowercase()
77+
val charInfo = CharacterInfo(obj.getString("character"), obj.getString("costume"), obj.getString("type"), obj.getString("hashed_name"))
78+
lut.getOrPut(fileId) { mutableListOf() }.add(charInfo)
79+
}
80+
} catch (e: org.json.JSONException) {
81+
// Fallback to old format
82+
try {
83+
val jsonArray = JSONArray(jsonString)
84+
for (i in 0 until jsonArray.length()) {
85+
val obj = jsonArray.getJSONObject(i)
86+
val fileId = obj.getString("file_id").lowercase()
87+
val charInfo = CharacterInfo(obj.getString("character"), obj.getString("costume"), obj.getString("type"), obj.getString("hashed_name"))
88+
lut.getOrPut(fileId) { mutableListOf() }.add(charInfo)
89+
}
90+
} catch (e2: Exception) {
91+
e2.printStackTrace()
92+
}
93+
} catch (e: Exception) {
94+
e.printStackTrace()
95+
}
96+
}
97+
lut
98+
}
99+
}
100+
101+
fun findBestMatch(fileId: String?, fileNames: List<String>): CharacterInfo? {
102+
if (fileId == null) return null
103+
104+
val candidates = characterLut[fileId] ?: return null
105+
if (candidates.size == 1) return candidates.first()
106+
if (candidates.isEmpty()) return null
107+
108+
val hasCutsceneKeyword = fileNames.any { it.contains("cutscene", ignoreCase = true) }
109+
if (hasCutsceneKeyword) {
110+
candidates.find { it.type == "cutscene" }?.let { return it }
111+
}
112+
113+
val validHashCandidates = candidates.filter { !it.hashedName.isNullOrBlank() }
114+
if (validHashCandidates.size == 1) {
115+
return validHashCandidates.first()
116+
}
117+
118+
candidates.find { it.type == "idle" }?.let { return it }
119+
120+
return candidates.first()
121+
}
122+
123+
fun extractFileId(entryName: String): String? {
124+
return "(char\\d{6}|illust_dating\\d+|illust_special\\d+|illust_talk\\d+|npc\\d+|specialillust\\w+|storypack\\w+|\\bRhythmHitAnim\\b)".toRegex(RegexOption.IGNORE_CASE).find(entryName)?.value?.lowercase()
125+
}
126+
}

0 commit comments

Comments
 (0)