Skip to content

Commit 2e357d6

Browse files
committed
adjusted widget for s2hoc pak tab game support
1 parent 358924f commit 2e357d6

3 files changed

Lines changed: 235 additions & 37 deletions

File tree

games/game_stalker2heartofchornobyl.py

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,11 @@ def init(self, organizer: mobase.IOrganizer) -> bool:
8080
try:
8181
os.makedirs(mod_path, exist_ok=True)
8282
if not os.path.exists(mod_path):
83-
self._organizer.log(
84-
mobase.LogLevel.WARNING,
85-
f"Failed to create directory: {mod_path}",
86-
)
83+
print(f"Failed to create directory: {mod_path}")
8784
except OSError as e:
88-
self._organizer.log(
89-
mobase.LogLevel.ERROR, f"OS error creating mod directory: {e}"
90-
)
85+
print(f"OS error creating mod directory: {e}")
9186
except Exception as e:
92-
self._organizer.log(
93-
mobase.LogLevel.ERROR,
94-
f"Unexpected error creating mod directory: {e}",
95-
)
87+
print(f"Unexpected error creating mod directory: {e}")
9688

9789
organizer.onUserInterfaceInitialized(self.init_tab)
9890
return True
@@ -108,25 +100,19 @@ def init_tab(self, main_window: QMainWindow):
108100
self._main_window = main_window
109101
tab_widget: QTabWidget = main_window.findChild(QTabWidget, "tabWidget")
110102
if not tab_widget:
111-
self._organizer.log(
112-
mobase.LogLevel.WARNING, "No main tab widget found!"
113-
)
103+
print("No main tab widget found!")
114104
return
115105

116106
from .stalker2heartofchornobyl.paks import S2HoCPaksTabWidget
117107

118108
self._paks_tab = S2HoCPaksTabWidget(main_window, self._organizer)
119109

120110
tab_widget.addTab(self._paks_tab, "PAK Files")
121-
self._organizer.log(mobase.LogLevel.INFO, "PAK Files tab added!")
111+
print("PAK Files tab added!")
122112
except ImportError as e:
123-
self._organizer.log(
124-
mobase.LogLevel.ERROR, f"Failed to import PAK tab widget: {e}"
125-
)
113+
print(f"Failed to import PAK tab widget: {e}")
126114
except Exception as e:
127-
self._organizer.log(
128-
mobase.LogLevel.ERROR, f"Error initializing PAK tab: {e}"
129-
)
115+
print(f"Error initializing PAK tab: {e}")
130116
import traceback
131117

132118
traceback.print_exc()
@@ -200,9 +186,7 @@ def activeProblems(self) -> list[int]:
200186
mod_path = self.paksModsDirectory().absolutePath()
201187
if not os.path.isdir(mod_path):
202188
problems.add(Problems.MISSING_MOD_DIRECTORIES)
203-
self._organizer.log(
204-
mobase.LogLevel.DEBUG, f"Missing mod directory: {mod_path}"
205-
)
189+
print(f"Missing mod directory: {mod_path}")
206190

207191
for mod in self._organizer.modList().allMods():
208192
mod_info = self._organizer.modList().getMod(mod)
@@ -264,13 +248,9 @@ def startGuidedFix(self, key: int) -> None:
264248
case Problems.MISSING_MOD_DIRECTORIES:
265249
try:
266250
os.makedirs(self.paksModsDirectory().absolutePath(), exist_ok=True)
267-
self._organizer.log(
268-
mobase.LogLevel.INFO, "Created missing mod directories"
269-
)
251+
print("Created missing mod directories")
270252
except Exception as e:
271-
self._organizer.log(
272-
mobase.LogLevel.ERROR, f"Failed to create mod directories: {e}"
273-
)
253+
print(f"Failed to create mod directories: {e}")
274254
case _:
275255
pass
276256

games/stalker2heartofchornobyl/paks/model.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,4 @@ def dropMimeData(
257257
index -= 1
258258

259259
self.set_paks(new_paks)
260-
<<<<<<< HEAD
261260
return True
262-
=======
263-
return False
264-
>>>>>>> ab91432d429d5ec75630e299423146320437832d
Lines changed: 225 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,225 @@
1-
Creating
2-
clean
3-
widget.py
1+
import os
2+
from functools import cmp_to_key
3+
from pathlib import Path
4+
from typing import cast
5+
6+
from PyQt6.QtCore import QDir, QFileInfo
7+
from PyQt6.QtWidgets import QGridLayout, QWidget
8+
9+
import mobase
10+
11+
from ....basic_features.utils import is_directory
12+
from .model import S2HoCPaksModel
13+
from .view import S2HoCPaksView
14+
15+
16+
def pak_sort(a: tuple[str, str], b: tuple[str, str]) -> int:
17+
"""Sort function for PAK files"""
18+
if a[0] < b[0]:
19+
return -1
20+
elif a[0] > b[0]:
21+
return 1
22+
else:
23+
return 0
24+
25+
26+
class S2HoCPaksTabWidget(QWidget):
27+
"""
28+
Widget for managing PAK files in Stalker 2: Heart of Chornobyl.
29+
"""
30+
31+
def __init__(self, parent: QWidget, organizer: mobase.IOrganizer):
32+
super().__init__(parent)
33+
self._organizer = organizer
34+
self._view = S2HoCPaksView(self)
35+
self._layout = QGridLayout(self)
36+
self._layout.addWidget(self._view)
37+
self._model = S2HoCPaksModel(self._view, organizer)
38+
self._view.setModel(self._model)
39+
self._model.dataChanged.connect(self.write_paks_list)
40+
self._view.data_dropped.connect(self.write_paks_list)
41+
organizer.onProfileChanged(lambda profile_a, profile_b: self._parse_pak_files())
42+
organizer.modList().onModInstalled(lambda mod: self._parse_pak_files())
43+
organizer.modList().onModRemoved(lambda mod: self._parse_pak_files())
44+
organizer.modList().onModStateChanged(lambda mods: self._parse_pak_files())
45+
self._parse_pak_files()
46+
47+
def load_paks_list(self) -> list[str]:
48+
profile = QDir(self._organizer.profilePath())
49+
paks_txt = QFileInfo(profile.absoluteFilePath("stalker2_paks.txt"))
50+
paks_list: list[str] = []
51+
if paks_txt.exists():
52+
try:
53+
with open(paks_txt.absoluteFilePath(), "r", encoding="utf-8") as paks_file:
54+
for line in paks_file:
55+
stripped_line = line.strip()
56+
if stripped_line:
57+
paks_list.append(stripped_line)
58+
except (IOError, OSError):
59+
pass
60+
return paks_list
61+
62+
def write_paks_list(self):
63+
"""Write the PAK list to file and then move the files"""
64+
profile = QDir(self._organizer.profilePath())
65+
paks_txt = QFileInfo(profile.absoluteFilePath("stalker2_paks.txt"))
66+
try:
67+
with open(paks_txt.absoluteFilePath(), "w", encoding="utf-8") as paks_file:
68+
for _, pak in sorted(self._model.paks.items()):
69+
name, _, _, _ = pak
70+
paks_file.write(f"{name}\n")
71+
self.write_pak_files()
72+
except (IOError, OSError) as e:
73+
print(f"Error writing PAK list: {e}")
74+
75+
def write_pak_files(self):
76+
"""Move PAK files to their target numbered directories"""
77+
for index, pak in sorted(self._model.paks.items()):
78+
_, _, current_path, target_path = pak
79+
if current_path and current_path != target_path:
80+
path_dir = Path(current_path)
81+
target_dir = Path(target_path)
82+
if not target_dir.exists():
83+
target_dir.mkdir(parents=True, exist_ok=True)
84+
if path_dir.exists():
85+
for pak_file in path_dir.glob("*.pak"):
86+
ucas_file = pak_file.with_suffix(".ucas")
87+
utoc_file = pak_file.with_suffix(".utoc")
88+
for file in (pak_file, ucas_file, utoc_file):
89+
if not file.exists():
90+
continue
91+
try:
92+
file.rename(target_dir.joinpath(file.name))
93+
except FileExistsError:
94+
pass
95+
data = self._model.paks[index]
96+
self._model.paks[index] = (
97+
data[0],
98+
data[1],
99+
data[3],
100+
data[3],
101+
)
102+
break
103+
if not list(path_dir.iterdir()):
104+
path_dir.rmdir()
105+
106+
def _shake_paks(self, sorted_paks: dict[str, str]) -> list[str]:
107+
"""Preserve order from paks.txt if it exists, otherwise use alphabetical"""
108+
shaken_paks: list[str] = []
109+
shaken_paks_p: list[str] = []
110+
paks_list = self.load_paks_list()
111+
for pak in paks_list:
112+
if pak in sorted_paks.keys():
113+
if pak.casefold().endswith("_p"):
114+
shaken_paks_p.append(pak)
115+
else:
116+
shaken_paks.append(pak)
117+
sorted_paks.pop(pak)
118+
for pak in sorted_paks.keys():
119+
if pak.casefold().endswith("_p"):
120+
shaken_paks_p.append(pak)
121+
else:
122+
shaken_paks.append(pak)
123+
return shaken_paks + shaken_paks_p
124+
125+
def _parse_pak_files(self):
126+
"""Parse PAK files from mods, following numbered folder assignment pattern"""
127+
from ...game_stalker2heartofchornobyl import S2HoCGame
128+
129+
mods = self._organizer.modList().allMods()
130+
paks: dict[str, str] = {}
131+
pak_paths: dict[str, tuple[str, str]] = {}
132+
pak_source: dict[str, str] = {}
133+
existing_folders: set[int] = set()
134+
135+
game = self._organizer.managedGame()
136+
if isinstance(game, S2HoCGame):
137+
pak_mods_dir = QFileInfo(game.paksModsDirectory().absolutePath())
138+
if pak_mods_dir.exists() and pak_mods_dir.isDir():
139+
for entry in QDir(pak_mods_dir.absoluteFilePath()).entryInfoList(
140+
QDir.Filter.Dirs | QDir.Filter.NoDotAndDotDot
141+
):
142+
try:
143+
folder_num = int(entry.completeBaseName())
144+
existing_folders.add(folder_num)
145+
except ValueError:
146+
pass
147+
148+
for mod in mods:
149+
mod_item = self._organizer.modList().getMod(mod)
150+
if not self._organizer.modList().state(mod) & mobase.ModState.ACTIVE:
151+
continue
152+
filetree = mod_item.fileTree()
153+
154+
has_logicmods = (
155+
filetree.find("Content/Paks/LogicMods")
156+
or filetree.find("Paks/LogicMods")
157+
)
158+
if isinstance(has_logicmods, mobase.IFileTree):
159+
continue
160+
161+
pak_mods = filetree.find("Paks/~mods")
162+
if not pak_mods:
163+
pak_mods = filetree.find("Content/Paks/~mods")
164+
if isinstance(pak_mods, mobase.IFileTree) and pak_mods.name() == "~mods":
165+
for entry in pak_mods:
166+
if is_directory(entry):
167+
for sub_entry in entry:
168+
if (
169+
sub_entry.isFile()
170+
and sub_entry.suffix().casefold() == "pak"
171+
):
172+
pak_name = sub_entry.name()[
173+
: -1 - len(sub_entry.suffix())
174+
]
175+
paks[pak_name] = entry.name()
176+
pak_paths[pak_name] = (
177+
mod_item.absolutePath()
178+
+ "/"
179+
+ cast(
180+
mobase.IFileTree, sub_entry.parent()
181+
).path("/"),
182+
mod_item.absolutePath() + "/" + pak_mods.path("/"),
183+
)
184+
pak_source[pak_name] = mod_item.name()
185+
else:
186+
if entry.suffix().casefold() == "pak":
187+
pak_name = entry.name()[: -1 - len(entry.suffix())]
188+
paks[pak_name] = ""
189+
pak_paths[pak_name] = (
190+
mod_item.absolutePath()
191+
+ "/"
192+
+ cast(mobase.IFileTree, entry.parent()).path("/"),
193+
mod_item.absolutePath() + "/" + pak_mods.path("/"),
194+
)
195+
pak_source[pak_name] = mod_item.name()
196+
197+
sorted_paks = dict(sorted(paks.items(), key=cmp_to_key(pak_sort)))
198+
shaken_paks: list[str] = self._shake_paks(sorted_paks)
199+
200+
final_paks: dict[str, tuple[str, str, str]] = {}
201+
pak_index = 8999
202+
203+
for pak in shaken_paks:
204+
while pak_index in existing_folders:
205+
pak_index -= 1
206+
207+
current_folder = paks[pak]
208+
if current_folder.isdigit():
209+
target_dir = pak_paths[pak][1] + "/" + current_folder
210+
existing_folders.add(int(current_folder))
211+
else:
212+
target_dir = pak_paths[pak][1] + "/" + str(pak_index).zfill(4)
213+
existing_folders.add(pak_index)
214+
pak_index -= 1
215+
216+
final_paks[pak] = (pak_source[pak], pak_paths[pak][0], target_dir)
217+
218+
new_data_paks: dict[int, tuple[str, str, str, str]] = {}
219+
i = 0
220+
for pak, data in final_paks.items():
221+
source, current_path, target_path = data
222+
new_data_paks[i] = (pak, source, current_path, target_path)
223+
i += 1
224+
225+
self._model.set_paks(new_data_paks)

0 commit comments

Comments
 (0)