mirror of
https://github.com/ModOrganizer2/modorganizer-basic_games.git
synced 2026-07-27 14:07:29 -07:00
* Refactoring following uibase change for game features. * Switch to ruff for linting and formatting. * Bump stubs to 2.5.1a0.
293 lines
9.9 KiB
Python
293 lines
9.9 KiB
Python
from enum import IntEnum
|
|
from pathlib import Path
|
|
|
|
import mobase
|
|
from PyQt6.QtCore import QDir, QFileInfo, Qt
|
|
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget
|
|
|
|
from ..basic_features.basic_save_game_info import (
|
|
BasicGameSaveGame,
|
|
BasicGameSaveGameInfo,
|
|
)
|
|
from ..basic_game import BasicGame
|
|
from .stalkeranomaly import XRSave
|
|
|
|
|
|
class StalkerAnomalyModDataChecker(mobase.ModDataChecker):
|
|
_valid_folders: list[str] = [
|
|
"appdata",
|
|
"bin",
|
|
"db",
|
|
"gamedata",
|
|
]
|
|
|
|
def hasValidFolders(self, tree: mobase.IFileTree) -> bool:
|
|
for e in tree:
|
|
if e.isDir():
|
|
if e.name().lower() in self._valid_folders:
|
|
return True
|
|
|
|
return False
|
|
|
|
def findLostData(self, tree: mobase.IFileTree) -> list[mobase.FileTreeEntry]:
|
|
lost_db: list[mobase.FileTreeEntry] = []
|
|
|
|
for e in tree:
|
|
if e.isFile():
|
|
if e.suffix().lower().startswith("db"):
|
|
lost_db.append(e)
|
|
|
|
return lost_db
|
|
|
|
def dataLooksValid(
|
|
self, filetree: mobase.IFileTree
|
|
) -> mobase.ModDataChecker.CheckReturn:
|
|
if self.hasValidFolders(filetree):
|
|
return mobase.ModDataChecker.VALID
|
|
|
|
if self.findLostData(filetree):
|
|
return mobase.ModDataChecker.FIXABLE
|
|
|
|
return mobase.ModDataChecker.INVALID
|
|
|
|
def fix(self, filetree: mobase.IFileTree) -> mobase.IFileTree:
|
|
lost_db = self.findLostData(filetree)
|
|
if lost_db:
|
|
rfolder = filetree.addDirectory("db").addDirectory("mods")
|
|
for r in lost_db:
|
|
rfolder.insert(r, mobase.IFileTree.REPLACE)
|
|
|
|
return filetree
|
|
|
|
|
|
class Content(IntEnum):
|
|
INTERFACE = 0
|
|
TEXTURE = 1
|
|
MESH = 2
|
|
SCRIPT = 3
|
|
SOUND = 4
|
|
MCM = 5
|
|
CONFIG = 6
|
|
|
|
|
|
class StalkerAnomalyModDataContent(mobase.ModDataContent):
|
|
content: list[int] = []
|
|
|
|
def getAllContents(self) -> list[mobase.ModDataContent.Content]:
|
|
return [
|
|
mobase.ModDataContent.Content(
|
|
Content.INTERFACE, "Interface", ":/MO/gui/content/interface"
|
|
),
|
|
mobase.ModDataContent.Content(
|
|
Content.TEXTURE, "Textures", ":/MO/gui/content/texture"
|
|
),
|
|
mobase.ModDataContent.Content(
|
|
Content.MESH, "Meshes", ":/MO/gui/content/mesh"
|
|
),
|
|
mobase.ModDataContent.Content(
|
|
Content.SCRIPT, "Scripts", ":/MO/gui/content/script"
|
|
),
|
|
mobase.ModDataContent.Content(
|
|
Content.SOUND, "Sounds", ":/MO/gui/content/sound"
|
|
),
|
|
mobase.ModDataContent.Content(Content.MCM, "MCM", ":/MO/gui/content/menu"),
|
|
mobase.ModDataContent.Content(
|
|
Content.CONFIG, "Configs", ":/MO/gui/content/inifile"
|
|
),
|
|
]
|
|
|
|
def walkContent(
|
|
self, path: str, entry: mobase.FileTreeEntry
|
|
) -> mobase.IFileTree.WalkReturn:
|
|
name = entry.name().lower()
|
|
if entry.isFile():
|
|
ext = entry.suffix().lower()
|
|
if ext in ["dds", "thm"]:
|
|
self.content.append(Content.TEXTURE)
|
|
if path.startswith("gamedata/textures/ui"):
|
|
self.content.append(Content.INTERFACE)
|
|
elif ext in ["omf", "ogf"]:
|
|
self.content.append(Content.MESH)
|
|
elif ext in ["script"]:
|
|
self.content.append(Content.SCRIPT)
|
|
if "_mcm" in name:
|
|
self.content.append(Content.MCM)
|
|
elif ext in ["ogg"]:
|
|
self.content.append(Content.SOUND)
|
|
elif ext in ["ltx", "xml"]:
|
|
self.content.append(Content.CONFIG)
|
|
if path.startswith("gamedata/configs/ui"):
|
|
self.content.append(Content.INTERFACE)
|
|
|
|
return mobase.IFileTree.WalkReturn.CONTINUE
|
|
|
|
def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]:
|
|
self.content = []
|
|
filetree.walk(self.walkContent, "/")
|
|
return self.content
|
|
|
|
|
|
class StalkerAnomalySaveGame(BasicGameSaveGame):
|
|
_filepath: Path
|
|
|
|
xr_save: XRSave
|
|
|
|
def __init__(self, filepath: Path):
|
|
super().__init__(filepath)
|
|
self._filepath = filepath
|
|
self.xr_save = XRSave(self._filepath)
|
|
|
|
def getName(self) -> str:
|
|
xr_save = self.xr_save
|
|
player = xr_save.player
|
|
if player:
|
|
name = player.character_name_str
|
|
time = xr_save.time_fmt
|
|
return f"{name}, {xr_save.save_fmt} [{time}]"
|
|
return ""
|
|
|
|
def allFiles(self) -> list[str]:
|
|
filepath = str(self._filepath)
|
|
paths = [filepath]
|
|
scoc = filepath.replace(".scop", ".scoc")
|
|
if Path(scoc).exists():
|
|
paths.append(scoc)
|
|
dds = filepath.replace(".scop", ".dds")
|
|
if Path(dds).exists():
|
|
paths.append(dds)
|
|
return paths
|
|
|
|
|
|
class StalkerAnomalySaveGameInfoWidget(mobase.ISaveGameInfoWidget):
|
|
def __init__(self, parent: QWidget | None):
|
|
super().__init__(parent)
|
|
layout = QVBoxLayout()
|
|
self._labelSave = self.newLabel(layout)
|
|
self._labelName = self.newLabel(layout)
|
|
self._labelFaction = self.newLabel(layout)
|
|
self._labelHealth = self.newLabel(layout)
|
|
self._labelMoney = self.newLabel(layout)
|
|
self._labelRank = self.newLabel(layout)
|
|
self._labelRep = self.newLabel(layout)
|
|
self.setLayout(layout)
|
|
palette = self.palette()
|
|
palette.setColor(self.backgroundRole(), Qt.GlobalColor.black)
|
|
self.setAutoFillBackground(True)
|
|
self.setPalette(palette)
|
|
self.setWindowFlags(
|
|
Qt.WindowType.ToolTip | Qt.WindowType.BypassGraphicsProxyWidget
|
|
)
|
|
|
|
def newLabel(self, layout: QVBoxLayout) -> QLabel:
|
|
label = QLabel()
|
|
label.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
|
palette = label.palette()
|
|
palette.setColor(label.foregroundRole(), Qt.GlobalColor.white)
|
|
label.setPalette(palette)
|
|
layout.addWidget(label)
|
|
layout.addStretch()
|
|
return label
|
|
|
|
def setSave(self, save: mobase.ISaveGame):
|
|
self.resize(240, 32)
|
|
if not isinstance(save, StalkerAnomalySaveGame):
|
|
return
|
|
xr_save = save.xr_save
|
|
player = xr_save.player
|
|
if player:
|
|
self._labelSave.setText(f"Save: {xr_save.save_fmt}")
|
|
self._labelName.setText(f"Name: {player.character_name_str}")
|
|
self._labelFaction.setText(f"Faction: {xr_save.getFaction()}")
|
|
self._labelHealth.setText(f"Health: {player.health:.2f}%")
|
|
self._labelMoney.setText(f"Money: {player.money} RU")
|
|
self._labelRank.setText(f"Rank: {xr_save.getRank()} ({player.rank})")
|
|
self._labelRep.setText(
|
|
f"Reputation: {xr_save.getReputation()} ({player.reputation})"
|
|
)
|
|
|
|
|
|
class StalkerAnomalySaveGameInfo(BasicGameSaveGameInfo):
|
|
def getSaveGameWidget(self, parent: QWidget | None = None):
|
|
return StalkerAnomalySaveGameInfoWidget(parent)
|
|
|
|
|
|
class StalkerAnomalyGame(BasicGame, mobase.IPluginFileMapper):
|
|
Name = "STALKER Anomaly"
|
|
Author = "Qudix"
|
|
Version = "0.5.0"
|
|
Description = "Adds support for STALKER Anomaly"
|
|
|
|
GameName = "STALKER Anomaly"
|
|
GameShortName = "stalkeranomaly"
|
|
GameNexusName = "stalkeranomaly"
|
|
GameNexusId = 3743
|
|
GameBinary = "AnomalyLauncher.exe"
|
|
GameDataPath = ""
|
|
GameDocumentsDirectory = "%GAME_PATH%/appdata"
|
|
GameSupportURL = (
|
|
r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/"
|
|
"Game:-S.T.A.L.K.E.R.-Anomaly"
|
|
)
|
|
|
|
GameSaveExtension = "scop"
|
|
GameSavesDirectory = "%GAME_DOCUMENTS%/savedgames"
|
|
|
|
def __init__(self):
|
|
BasicGame.__init__(self)
|
|
mobase.IPluginFileMapper.__init__(self)
|
|
|
|
def init(self, organizer: mobase.IOrganizer):
|
|
BasicGame.init(self, organizer)
|
|
self._register_feature(StalkerAnomalyModDataChecker())
|
|
self._register_feature(StalkerAnomalyModDataContent())
|
|
self._register_feature(StalkerAnomalySaveGameInfo())
|
|
organizer.onAboutToRun(lambda _str: self.aboutToRun(_str))
|
|
return True
|
|
|
|
def aboutToRun(self, _str: str) -> bool:
|
|
gamedir = self.gameDirectory()
|
|
if gamedir.exists():
|
|
# For mappings
|
|
gamedir.mkdir("appdata")
|
|
# The game will crash if this file exists in the
|
|
# virtual tree rather than the game dir
|
|
dbg_path = Path(self._gamePath, "gamedata/configs/cache_dbg.ltx")
|
|
if not dbg_path.exists():
|
|
dbg_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(dbg_path, "w", encoding="utf-8"):
|
|
pass
|
|
return True
|
|
|
|
def executables(self) -> list[mobase.ExecutableInfo]:
|
|
info = [
|
|
["Anomaly Launcher", "AnomalyLauncher.exe"],
|
|
["Anomaly (DX11-AVX)", "bin/AnomalyDX11AVX.exe"],
|
|
["Anomaly (DX11)", "bin/AnomalyDX11.exe"],
|
|
["Anomaly (DX10-AVX)", "bin/AnomalyDX10AVX.exe"],
|
|
["Anomaly (DX10)", "bin/AnomalyDX10.exe"],
|
|
["Anomaly (DX9-AVX)", "bin/AnomalyDX9AVX.exe"],
|
|
["Anomaly (DX9)", "bin/AnomalyDX9.exe"],
|
|
["Anomaly (DX8-AVX)", "bin/AnomalyDX8AVX.exe"],
|
|
["Anomaly (DX8)", "bin/AnomalyDX8.exe"],
|
|
]
|
|
gamedir = self.gameDirectory()
|
|
return [
|
|
mobase.ExecutableInfo(inf[0], QFileInfo(gamedir, inf[1])) for inf in info
|
|
]
|
|
|
|
def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]:
|
|
ext = self._mappings.savegameExtension.get()
|
|
return [
|
|
StalkerAnomalySaveGame(path)
|
|
for path in Path(folder.absolutePath()).glob(f"*.{ext}")
|
|
]
|
|
|
|
def mappings(self) -> list[mobase.Mapping]:
|
|
appdata = self.gameDirectory().filePath("appdata")
|
|
m = mobase.Mapping()
|
|
m.createTarget = True
|
|
m.isDirectory = True
|
|
m.source = appdata
|
|
m.destination = appdata
|
|
return [m]
|