Skip to content

Commit d7c52c5

Browse files
committed
Fixed edgecase bug, Updated CrimeBoss to match.
1 parent fc1deac commit d7c52c5

17 files changed

Lines changed: 573 additions & 451 deletions

games/game_batmanlegacy.py

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -105,21 +105,16 @@ def sanitizeFolderName(self, name: str) -> str:
105105
name = "Mod"
106106
return name
107107

108-
def groupRelatedFiles(
109-
self,
110-
entries: list[mobase.FileTreeEntry],
111-
) -> list[list[mobase.FileTreeEntry]]:
112-
"""Group files that belong together (e.g., .pak, .utoc, .ucas with same base name)."""
113-
grouped: dict[str, list[mobase.FileTreeEntry]] = {}
114-
115-
for entry in entries:
116-
# Get base name without extension
117-
name_without_ext = os.path.splitext(entry.name())[0]
118-
if name_without_ext not in grouped:
119-
grouped[name_without_ext] = []
120-
grouped[name_without_ext].append(entry)
121-
122-
return list(grouped.values())
108+
def hasLooseInstallableFiles(self, filetree: mobase.IFileTree) -> bool:
109+
for entry in filetree:
110+
if entry.isFile() and entry.suffix().casefold() in {
111+
"pak",
112+
"utoc",
113+
"ucas",
114+
"bk2",
115+
}:
116+
return True
117+
return False
123118

124119
def dataLooksValid(
125120
self, filetree: mobase.IFileTree
@@ -131,6 +126,10 @@ def dataLooksValid(
131126
GameDataMovieMods = getattr(
132127
self.organizer.managedGame(), "GameDataMovieMods", ""
133128
)
129+
130+
if self.hasLooseInstallableFiles(filetree):
131+
return mobase.ModDataChecker.FIXABLE
132+
134133
if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY):
135134
return mobase.ModDataChecker.VALID
136135
if filetree.exists(GameDataMovieMods, mobase.IFileTree.DIRECTORY):
@@ -152,18 +151,24 @@ def moveTreeContent(
152151
elif installtype == "os":
153152
entry = entries[0]
154153
if isinstance(entry, mobase.IFileTree):
154+
mod_name_val = entry.name()
155+
mod_path = os.path.join(self.organizer.modsPath(), mod_name_val)
156+
insideMods = os.path.join(mod_path, destination)
157+
os.makedirs(insideMods, exist_ok=True)
158+
159+
destination_root = (
160+
destination.replace("\\", "/").split("/", 1)[0].casefold()
161+
)
162+
155163
for subentry in entry:
156164
mod_file = subentry.name()
157-
mod_name_val = entry.name()
158-
mod_path = os.path.join(self.organizer.modsPath(), mod_name_val)
159-
insideMods = os.path.join(mod_path, destination)
160-
os.makedirs(insideMods, exist_ok=True)
165+
166+
if subentry.isDir() and mod_file.casefold() == destination_root:
167+
continue
168+
161169
src = os.path.join(mod_path, mod_file)
162170
dst = os.path.join(mod_path, destination, mod_file)
163-
shutil.move(
164-
src,
165-
dst,
166-
)
171+
shutil.move(src, dst)
167172
return None
168173

169174
def addModDetectionCandidate(
@@ -209,10 +214,10 @@ def showModDetectionDialog(self) -> set[int] | None:
209214
selectButtons = QHBoxLayout()
210215
selectAllButton = QPushButton("Select All")
211216
selectNoneButton = QPushButton("Select None")
212-
selectAllButton.clicked.connect( # type: ignore # type: ignore
217+
selectAllButton.clicked.connect( # type: ignore
213218
lambda: self.setDialogSelection(listWidget, True)
214219
)
215-
selectNoneButton.clicked.connect( # type: ignore # type: ignore
220+
selectNoneButton.clicked.connect( # type: ignore
216221
lambda: self.setDialogSelection(listWidget, False)
217222
)
218223
selectButtons.addWidget(selectAllButton)
@@ -493,7 +498,7 @@ def writeDefaultMods(self, profile: QDir):
493498
mods_json.write(json.dumps(mods_data, indent=4))
494499

495500
def iniFiles(self):
496-
return ["GameUserSettings.ini", "Engine.ini"]
501+
return ["GameUserSettings.ini", "Engine.ini", "Input.ini"]
497502

498503
def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting):
499504
self.writeDefaultMods(directory)

games/game_crimeboss.py

Lines changed: 87 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -141,21 +141,21 @@ def fixInstalledMod(self, mod: mobase.IModInterface):
141141
return
142142
self.needsNameFix = False
143143

144-
def groupRelatedFiles(
145-
self,
146-
entries: list[mobase.FileTreeEntry],
147-
) -> list[list[mobase.FileTreeEntry]]:
148-
"""Group files that belong together (e.g., .pak, .utoc, .ucas with same base name)."""
149-
grouped: dict[str, list[mobase.FileTreeEntry]] = {}
150-
151-
for entry in entries:
152-
# Get base name without extension
153-
name_without_ext = os.path.splitext(entry.name())[0]
154-
if name_without_ext not in grouped:
155-
grouped[name_without_ext] = []
156-
grouped[name_without_ext].append(entry)
157-
158-
return list(grouped.values())
144+
def hasLooseInstallableFiles(self, filetree: mobase.IFileTree) -> bool:
145+
"""Return True if loose installable files exist at the mod root.
146+
147+
This lets merge installs re-run the fixer when a valid mod already has
148+
Content/Paks/~Mods, but a new .pak/.utoc/.ucas/.bk2 was merged at root.
149+
"""
150+
for entry in filetree:
151+
if entry.isFile() and entry.suffix().casefold() in {
152+
"pak",
153+
"utoc",
154+
"ucas",
155+
"bk2",
156+
}:
157+
return True
158+
return False
159159

160160
def dataLooksValid(
161161
self, filetree: mobase.IFileTree
@@ -168,6 +168,13 @@ def dataLooksValid(
168168
GameDataNativeMods = getattr(
169169
self.organizer.managedGame(), "GameDataNativeMods", ""
170170
)
171+
172+
print("dataLooksValid called")
173+
print("hasLooseInstallableFiles =", self.hasLooseInstallableFiles(filetree))
174+
175+
if self.hasLooseInstallableFiles(filetree):
176+
return mobase.ModDataChecker.FIXABLE
177+
171178
if filetree.exists(GameDataPakMods, mobase.IFileTree.DIRECTORY):
172179
return mobase.ModDataChecker.VALID
173180
if filetree.exists(GameDataMovies, mobase.IFileTree.DIRECTORY):
@@ -185,17 +192,44 @@ def moveTreeContent(
185192
targettree: mobase.IFileTree,
186193
destination: str,
187194
) -> None:
195+
print("RUNNING moveTreeContent")
188196
if installtype == "virtual":
197+
print("INSTALLTYPE:", installtype)
189198
for entry in entries:
190199
targettree.move(entry, destination, mobase.IFileTree.MERGE)
191200
elif installtype == "os":
192201
entry = entries[0]
202+
print("INSTALLTYPE:", installtype)
193203
if isinstance(entry, mobase.IFileTree):
204+
destination_root = (
205+
destination.strip("/\\")
206+
.replace("\\", "/")
207+
.split("/", 1)[0]
208+
.casefold()
209+
)
194210
for subentry in entry:
195211
mod_file = subentry.name()
196212
mod_name_val = entry.name()
197213
mod_path = os.path.join(self.organizer.modsPath(), mod_name_val)
214+
215+
# On merge installs, the existing fixed structure is also
216+
# present in this tree. Do not move it into itself; only move
217+
# the newly-added loose files/folders.
218+
if (
219+
destination_root
220+
and subentry.isDir()
221+
and mod_file.casefold() == destination_root
222+
):
223+
print("SKIPPING already-fixed destination root:", mod_file)
224+
continue
225+
198226
insideMods = os.path.join(mod_path, destination)
227+
print("----")
228+
print("MOD:", mod_name_val)
229+
print("FILE:", mod_file)
230+
print("SRC:", os.path.join(mod_path, mod_file))
231+
print("DST:", os.path.join(mod_path, destination, mod_file))
232+
print("TREE:", [e.name() for e in entry])
199233
os.makedirs(insideMods, exist_ok=True)
200234
src = os.path.join(mod_path, mod_file)
201235
dst = os.path.join(mod_path, destination, mod_file)
@@ -213,13 +247,13 @@ def addModDetectionCandidate(
213247
destination: str,
214248
installtype: str,
215249
) -> None:
216-
tree_name = self.sanitizeFolderName(trees[0].name() if trees else "Unknown")
250+
candidate_name = self.sanitizeFolderName(name)
217251

218252
self.modDetectionCandidates.append(
219253
{
220254
"trees": trees,
221-
"name": tree_name,
222-
"display": f"{name} ({category})",
255+
"name": candidate_name,
256+
"display": f"{candidate_name} ({category})",
223257
"destination": destination,
224258
"installtype": installtype,
225259
}
@@ -286,6 +320,9 @@ def setDialogSelection(self, listWidget: QListWidget, select: bool) -> None:
286320
if isinstance(item, QListWidgetItem):
287321
item.setCheckState(state)
288322

323+
def normalizedPathParts(self, path: str) -> set[str]:
324+
return set(path.replace("\\", "/").casefold().split("/"))
325+
289326
def collectModCandidates(
290327
self,
291328
path: str,
@@ -295,6 +332,7 @@ def collectModCandidates(
295332
category = None
296333
entryext = "None"
297334
basename = "Unknown"
335+
298336
GameDataUE4SSRootDir = getattr(
299337
self.organizer.managedGame(), "GameDataUE4SSRoot", ""
300338
)
@@ -315,6 +353,14 @@ def collectModCandidates(
315353
entryext = entry.suffix().casefold()
316354
basename = os.path.splitext(entry.name())[0]
317355

356+
path_parts = self.normalizedPathParts(entry.path())
357+
358+
# Native Unreal mods contain their own Content tree. Once we are inside
359+
# that tree, do not create additional candidates for nested files such as
360+
# .pak/.utoc/.ucas.
361+
if "content" in path_parts:
362+
return mobase.IFileTree.WalkReturn.CONTINUE
363+
318364
if isinstance(entry, mobase.IFileTree) and entry.isDir():
319365
if entry.exists("ue4ss.dll", mobase.IFileTree.FILE) or entry.exists(
320366
"dsound.dll", mobase.IFileTree.FILE
@@ -323,15 +369,11 @@ def collectModCandidates(
323369
elif entry.exists(
324370
"Scripts", mobase.IFileTree.DIRECTORY
325371
) and not entry.exists("ue4ss.dll", mobase.IFileTree.FILE):
326-
disallowedFolders = {"mods"}
327-
tree_path = entry.path()
328-
tree_path_lower = tree_path.replace("\\", "/").casefold()
329-
if not disallowedFolders & set(tree_path_lower.split("/")):
372+
if "mods" not in path_parts:
330373
category = "UE4SS"
331374
elif entry.exists("Content", mobase.IFileTree.DIRECTORY):
332375
category = "Native"
333376

334-
# Check single file for correct extensions
335377
match entryext:
336378
case "pak" | "utoc" | "ucas":
337379
category = "Paks"
@@ -348,7 +390,6 @@ def collectModCandidates(
348390

349391
self.category_groups[basename].append(entry)
350392

351-
# Add grouped entries as candidates
352393
for group_key, entries in self.category_groups.items():
353394
if group_key not in self.processedBasenames:
354395
self.processedBasenames.add(group_key)
@@ -369,7 +410,6 @@ def collectModCandidates(
369410
destination = "/"
370411

371412
if installtype == "os":
372-
# Single file/entry
373413
self.addModDetectionCandidate(
374414
[entry],
375415
sanitized_name,
@@ -380,7 +420,7 @@ def collectModCandidates(
380420
else:
381421
candidate_entries = entries
382422

383-
if group_category == "Root":
423+
if group_category in {"Root", "Native"}:
384424
candidate_entries: list[mobase.FileTreeEntry] = []
385425
for root_entry in entries:
386426
if (
@@ -449,15 +489,33 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None:
449489
if not UnZippedInstallation:
450490
filetree = newtree
451491

492+
GameDataNativeMods = getattr(
493+
self.organizer.managedGame(), "GameDataNativeMods", ""
494+
)
495+
selected_native_indexes = {
496+
index
497+
for index in selectedIndexes
498+
if self.modDetectionCandidates[index]["destination"].endswith(
499+
"/FOLDERNAME/"
500+
)
501+
}
502+
multiple_native = len(selected_native_indexes) > 1
503+
452504
for index in selectedIndexes:
453505
candidate = self.modDetectionCandidates[index]
454-
if "/FOLDERNAME/" in candidate["destination"]:
455-
self.needsNameFix = True
506+
destination = candidate["destination"]
507+
508+
if index in selected_native_indexes:
509+
if multiple_native:
510+
destination = GameDataNativeMods + f"/{candidate['name']}/"
511+
else:
512+
self.needsNameFix = True
513+
456514
self.moveTreeContent(
457515
candidate["installtype"],
458516
candidate["trees"],
459517
filetree,
460-
candidate["destination"],
518+
destination,
461519
)
462520

463521
return filetree

0 commit comments

Comments
 (0)