Skip to content

Commit da570ea

Browse files
committed
Finished updating ue4ss files to a solid base.
1 parent cf77b29 commit da570ea

22 files changed

Lines changed: 2512 additions & 1570 deletions

games/game_batmanlegacy.py

Lines changed: 147 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
from PyQt6.QtWidgets import (
1111
QDialog,
1212
QDialogButtonBox,
13+
QHBoxLayout,
1314
QLabel,
1415
QListWidget,
1516
QListWidgetItem,
1617
QMainWindow,
18+
QPushButton,
1719
QTabWidget,
1820
QVBoxLayout,
1921
QWidget,
@@ -37,8 +39,6 @@ class Content(IntEnum):
3739

3840

3941
class BatmanLegacyModDataContent(mobase.ModDataContent):
40-
content: list[int] = []
41-
4242
GAMECONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [
4343
(Content.UCAS, "UCAS", ":/MO/gui/content/geometries"),
4444
(Content.UTOC, "UTOC", ":/MO/gui/content/inifile"),
@@ -80,7 +80,7 @@ def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]:
8080

8181

8282
class ModDetectionCandidate(TypedDict):
83-
tree: mobase.IFileTree | mobase.FileTreeEntry
83+
trees: list[mobase.IFileTree | mobase.FileTreeEntry]
8484
name: str
8585
display: str
8686
destination: str
@@ -92,6 +92,8 @@ def __init__(self, organizer: mobase.IOrganizer):
9292
super().__init__()
9393
self.organizer: mobase.IOrganizer = organizer
9494
self.modDetectionCandidates: list[ModDetectionCandidate] = []
95+
self.processedBasenames: set[str] = set() # Track already-grouped files
96+
self.category_groups: dict[str, list[mobase.FileTreeEntry]] = {}
9597

9698
def moveOverwriteMerge(self, source: str, destination: str):
9799
if not os.path.exists(destination):
@@ -113,14 +115,30 @@ def sanitizeFolderName(self, name: str) -> str:
113115
name = "".join(c for c in name if ord(c) >= 32)
114116
name = name.rstrip(". ")
115117
if not name:
116-
name = "FOLDERNAME"
118+
name = "Mod"
117119
return name
118120

121+
def groupRelatedFiles(
122+
self,
123+
entries: list[mobase.FileTreeEntry],
124+
) -> list[list[mobase.FileTreeEntry]]:
125+
"""Group files that belong together (e.g., .pak, .utoc, .ucas with same base name)."""
126+
grouped: dict[str, list[mobase.FileTreeEntry]] = {}
127+
128+
for entry in entries:
129+
# Get base name without extension
130+
name_without_ext = os.path.splitext(entry.name())[0]
131+
if name_without_ext not in grouped:
132+
grouped[name_without_ext] = []
133+
grouped[name_without_ext].append(entry)
134+
135+
return list(grouped.values())
136+
119137
def dataLooksValid(
120138
self, filetree: mobase.IFileTree
121139
) -> mobase.ModDataChecker.CheckReturn:
122-
GameDataUE4SSMods = (
123-
getattr(self.organizer.managedGame(), "GameDataUE4SSRoot", "") + "/Mods"
140+
GameDataUE4SSMods = getattr(
141+
self.organizer.managedGame(), "GameDataUE4SSRoot", ""
124142
)
125143
GameDataPakMods = getattr(self.organizer.managedGame(), "GameDataPakMods", "")
126144
GameDataMovieMods = getattr(
@@ -137,41 +155,42 @@ def dataLooksValid(
137155
def moveTreeContent(
138156
self,
139157
installtype: str,
140-
entry: mobase.IFileTree | mobase.FileTreeEntry,
158+
entries: list[mobase.IFileTree | mobase.FileTreeEntry],
141159
targettree: mobase.IFileTree,
142160
destination: str,
143161
) -> None:
144162
if installtype == "virtual":
145-
targettree.move(entry, destination, mobase.IFileTree.MERGE)
163+
for entry in entries:
164+
targettree.move(entry, destination, mobase.IFileTree.MERGE)
146165
elif installtype == "os":
147-
if isinstance(entry, mobase.IFileTree):
148-
for element in entry:
149-
mod_file = element.name()
150-
mod_name = entry.name()
151-
mod_path = os.path.join(self.organizer.modsPath(), mod_name)
152-
insideMods = os.path.join(mod_path, destination)
153-
os.makedirs(insideMods, exist_ok=True)
154-
src = os.path.join(mod_path, mod_file)
155-
dst = os.path.join(mod_path, destination, mod_file)
156-
shutil.move(
157-
src,
158-
dst,
159-
)
166+
entry = entries[0]
167+
for subentry in entry:
168+
mod_file = subentry.name()
169+
mod_name_val = entry.name()
170+
mod_path = os.path.join(self.organizer.modsPath(), mod_name_val)
171+
insideMods = os.path.join(mod_path, destination)
172+
os.makedirs(insideMods, exist_ok=True)
173+
src = os.path.join(mod_path, mod_file)
174+
dst = os.path.join(mod_path, destination, mod_file)
175+
shutil.move(
176+
src,
177+
dst,
178+
)
160179
return None
161180

162181
def addModDetectionCandidate(
163182
self,
164-
tree: mobase.IFileTree | mobase.FileTreeEntry,
183+
trees: list[mobase.IFileTree | mobase.FileTreeEntry],
165184
name: str,
166185
category: str,
167186
destination: str,
168187
installtype: str,
169188
) -> None:
170-
tree_name = tree.name()
189+
tree_name = self.sanitizeFolderName(trees[0].name() if trees else "Unknown")
171190

172191
self.modDetectionCandidates.append(
173192
{
174-
"tree": tree,
193+
"trees": trees,
175194
"name": tree_name,
176195
"display": f"{name} ({category})",
177196
"destination": destination,
@@ -199,6 +218,19 @@ def showModDetectionDialog(self) -> set[int] | None:
199218

200219
layout.addWidget(listWidget)
201220

221+
selectButtons = QHBoxLayout()
222+
selectAllButton = QPushButton("Select All")
223+
selectNoneButton = QPushButton("Select None")
224+
selectAllButton.clicked.connect(
225+
lambda: self.setDialogSelection(listWidget, True)
226+
) # type: ignore
227+
selectNoneButton.clicked.connect(
228+
lambda: self.setDialogSelection(listWidget, False)
229+
) # type: ignore
230+
selectButtons.addWidget(selectAllButton)
231+
selectButtons.addWidget(selectNoneButton)
232+
layout.addLayout(selectButtons)
233+
202234
buttonBox = QDialogButtonBox(
203235
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
204236
)
@@ -220,14 +252,22 @@ def showModDetectionDialog(self) -> set[int] | None:
220252

221253
return selectedIndexes
222254

255+
def setDialogSelection(self, listWidget: QListWidget, select: bool) -> None:
256+
state = Qt.CheckState.Checked if select else Qt.CheckState.Unchecked
257+
for index in range(listWidget.count()):
258+
item = listWidget.item(index)
259+
if isinstance(item, QListWidgetItem):
260+
item.setCheckState(state)
261+
223262
def collectModCandidates(
224263
self,
225-
tree: mobase.IFileTree | mobase.FileTreeEntry,
264+
path: str,
265+
entry: mobase.FileTreeEntry,
226266
installtype: str = "virtual",
227-
) -> bool:
228-
sanitized_name = self.sanitizeFolderName(tree.name())
267+
):
229268
category = None
230269
entryext = "None"
270+
basename = "Unknown"
231271
GameDataUE4SSRootDir = getattr(
232272
self.organizer.managedGame(), "GameDataUE4SSRoot", ""
233273
)
@@ -239,22 +279,24 @@ def collectModCandidates(
239279
self.organizer.managedGame(), "GameDataMovieMods", ""
240280
)
241281

242-
if installtype == "os" and isinstance(tree, mobase.IFileTree):
243-
for entry in tree:
244-
entryext = os.path.splitext(entry.name())[1].removeprefix(".")
282+
if installtype == "os" and isinstance(entry, mobase.IFileTree):
283+
for subentry in entry:
284+
entryext = os.path.splitext(subentry.name())[1].removeprefix(".")
285+
basename = os.path.splitext(subentry.name())[0]
245286
else:
246-
entryext = tree.suffix().casefold()
287+
entryext = entry.suffix().casefold()
288+
basename = os.path.splitext(entry.name())[0]
247289

248-
if isinstance(tree, mobase.IFileTree) and tree.isDir():
249-
if tree.exists("ue4ss.dll", mobase.IFileTree.FILE) or tree.exists(
290+
if isinstance(entry, mobase.IFileTree) and entry.isDir():
291+
if entry.exists("ue4ss.dll", mobase.IFileTree.FILE) or entry.exists(
250292
"dsound.dll", mobase.IFileTree.FILE
251293
):
252294
category = "Root"
253-
elif tree.exists("Scripts", mobase.IFileTree.DIRECTORY) and not tree.exists(
254-
"ue4ss.dll", mobase.IFileTree.FILE
255-
):
295+
elif entry.exists(
296+
"Scripts", mobase.IFileTree.DIRECTORY
297+
) and not entry.exists("ue4ss.dll", mobase.IFileTree.FILE):
256298
disallowedFolders = {"mods"}
257-
tree_path = tree.path()
299+
tree_path = entry.path()
258300
tree_path_lower = tree_path.replace("\\", "/").casefold()
259301
if not disallowedFolders & set(tree_path_lower.split("/")):
260302
category = "UE4SS"
@@ -268,60 +310,76 @@ def collectModCandidates(
268310
case _:
269311
pass
270312

271-
if category is None:
272-
return False
273-
274-
if category == "UE4SS":
275-
destination = GameDataUE4SSModsDir + "/"
276-
elif category == "Root":
277-
destination = GameDataUE4SSRootDir + "/"
278-
elif category == "Paks":
279-
destination = GameDataPakModsDir + "/"
280-
elif category == "Movie":
281-
destination = GameDataMovieModsDir + "/"
282-
else:
283-
destination = "/"
284-
285-
self.addModDetectionCandidate(
286-
tree,
287-
sanitized_name,
288-
f"{category} Mod",
289-
destination,
290-
installtype,
291-
)
292-
return True
313+
if category is not None:
314+
basename = basename + " " + category
315+
316+
if basename not in self.category_groups:
317+
self.category_groups[basename] = []
318+
319+
self.category_groups[basename].append(entry)
320+
321+
# Add grouped entries as candidates
322+
for basename, entries in self.category_groups.items():
323+
if basename not in self.processedBasenames:
324+
self.processedBasenames.add(basename)
325+
sanitized_name = self.sanitizeFolderName(entries[0].name())
326+
327+
if category == "UE4SS":
328+
destination = GameDataUE4SSModsDir + "/"
329+
elif category == "Root":
330+
destination = GameDataUE4SSRootDir + "/"
331+
elif category == "Paks":
332+
destination = GameDataPakModsDir + "/"
333+
elif category == "Movie":
334+
destination = GameDataMovieModsDir + "/"
335+
else:
336+
destination = "/"
337+
338+
if installtype == "os":
339+
# Single file/entry
340+
self.addModDetectionCandidate(
341+
[entry],
342+
sanitized_name,
343+
f"{category} Mod",
344+
destination,
345+
installtype,
346+
)
347+
else:
348+
candidate_entries = entries
349+
350+
if category == "Root":
351+
candidate_entries = []
352+
for root_entry in entries:
353+
if root_entry.isDir():
354+
candidate_entries.extend(list(root_entry))
355+
else:
356+
candidate_entries.append(root_entry)
357+
358+
self.addModDetectionCandidate(
359+
candidate_entries,
360+
sanitized_name,
361+
f"{category} Mod",
362+
destination,
363+
installtype,
364+
)
293365

294-
def walkEntry(self, path: str, entry: mobase.FileTreeEntry):
295-
self.collectModCandidates(entry)
296366
return mobase.IFileTree.WalkReturn.CONTINUE
297367

298-
def fileExistsInNextSubDir(self, filetree: mobase.IFileTree, name: str):
299-
for branch in filetree:
300-
if isinstance(branch, mobase.IFileTree):
301-
for e in branch:
302-
if e.name() == name:
303-
return True
304-
return False
305-
306-
def allMoveTo(self, filetree: mobase.IFileTree, toMoveTo: str):
307-
entriesToMove: list[mobase.FileTreeEntry] = []
308-
retVal = 0
309-
for e in filetree:
310-
entriesToMove.append(e)
311-
for e in entriesToMove:
312-
filetree.move(e, toMoveTo, mobase.IFileTree.MERGE)
313-
retVal = 1
314-
return retVal
315-
316368
def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None:
317369
self.modDetectionCandidates = []
370+
self.processedBasenames = set()
371+
self.category_groups = {}
372+
UnZippedInstallation = False
318373
newtree = filetree.createOrphanTree("Fixed Tree")
319-
# Check for Non Zipped Mod
374+
320375
if filetree.name() != "":
321-
self.collectModCandidates(filetree, installtype="os")
376+
# Initial Check on Main Directory
377+
self.collectModCandidates("/", filetree, installtype="os")
378+
UnZippedInstallation = True
322379
else:
323-
self.collectModCandidates(filetree)
324-
filetree.walk(self.walkEntry, "/")
380+
# Initial Check on Main Directory
381+
self.collectModCandidates("/", filetree)
382+
filetree.walk(self.collectModCandidates, "/")
325383

326384
if len(self.modDetectionCandidates) == 1:
327385
selectedIndexes = {0}
@@ -330,19 +388,19 @@ def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree | None:
330388
if selectedIndexes is None:
331389
return None
332390

391+
if not UnZippedInstallation:
392+
filetree = newtree
393+
333394
for index in selectedIndexes:
334395
candidate = self.modDetectionCandidates[index]
335396
self.moveTreeContent(
336397
candidate["installtype"],
337-
candidate["tree"],
338-
newtree,
398+
candidate["trees"],
399+
filetree,
339400
candidate["destination"],
340401
)
341402

342-
if newtree:
343-
return newtree
344-
else:
345-
return filetree
403+
return filetree
346404

347405

348406
class BatmanLegacyGame(BasicGame):

0 commit comments

Comments
 (0)