aboutsummaryrefslogtreecommitdiff
path: root/libs/basic_games/games/game_blackandwhite2.py
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/basic_games/games/game_blackandwhite2.py
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/basic_games/games/game_blackandwhite2.py')
-rw-r--r--libs/basic_games/games/game_blackandwhite2.py341
1 files changed, 341 insertions, 0 deletions
diff --git a/libs/basic_games/games/game_blackandwhite2.py b/libs/basic_games/games/game_blackandwhite2.py
new file mode 100644
index 0000000..5d9f60b
--- /dev/null
+++ b/libs/basic_games/games/game_blackandwhite2.py
@@ -0,0 +1,341 @@
+import datetime
+import os
+import struct
+import time
+from collections.abc import Mapping
+from pathlib import Path
+from typing import BinaryIO
+
+from PyQt6.QtCore import QDateTime, QDir, QFile, QFileInfo
+
+import mobase
+
+from ..basic_features import BasicLocalSavegames
+from ..basic_features.basic_save_game_info import (
+ BasicGameSaveGame,
+ BasicGameSaveGameInfo,
+ format_date,
+)
+from ..basic_game import BasicGame
+
+
+class BlackAndWhite2ModDataChecker(mobase.ModDataChecker):
+ _validFolderTree = {
+ "<black & white 2>": ["audio", "data", "plugins", "scripts"],
+ "audio": ["dialogue", "music", "sfx"],
+ "music": [
+ "buildingmusic",
+ "chant",
+ "cutscene",
+ "dynamic music",
+ "epicspell",
+ "townalignment",
+ ],
+ "sfx": ["atmos", "creature", "game", "script", "spells", "video", "grass"],
+ "data": [
+ "art",
+ "balance",
+ "ctr",
+ "effects",
+ "encryptedshaders",
+ "font",
+ "handdemo",
+ "interface",
+ "landscape",
+ "light particle effects",
+ "lipsync",
+ "physics",
+ "sfx",
+ "shaders",
+ "symbols",
+ "text",
+ "textures",
+ "tutorial avi",
+ "visualeffects",
+ "weathersystem",
+ "zones",
+ ],
+ "art": [
+ "binary_anim_libs",
+ "binary_animations",
+ "features",
+ "models",
+ "skins",
+ "textures",
+ "water",
+ ],
+ "ctr": [
+ "badvisor_evil",
+ "badvisor_good",
+ "bape",
+ "bgorilla",
+ "bhand",
+ "blion",
+ "btiger",
+ "bwolf",
+ "damage",
+ "siren",
+ ],
+ "font": ["asian"],
+ "asian": ["korean", "traditional chinese"],
+ "landscape": [
+ "aztec",
+ "bw2",
+ "egyptian",
+ "generic",
+ "greek",
+ "japanese",
+ "norse",
+ "skysettings",
+ ],
+ "tutorial avi": ["placeholder", "stills"],
+ "visualeffects": ["textures"],
+ "scripts": ["bw2"],
+ }
+ _validFileLocation = {
+ "<black & white 2>": ["exe", "dll", "ico", "png", "jpeg", "jpg"]
+ }
+ _mapFile = ["chl", "bmp", "bwe", "ter", "pat", "xml", "wal", "txt"]
+ _fileIgnore = ["readme", "read me", "meta.ini", "thumbs.db", "backup", ".png"]
+
+ def fix(self, filetree: mobase.IFileTree):
+ toMove: list[tuple[mobase.FileTreeEntry, str]] = []
+ for entry in filetree:
+ if any([sub in entry.name().casefold() for sub in self._fileIgnore]):
+ continue
+ elif entry.suffix() == "chl":
+ toMove.append((entry, "/Scripts/BW2/"))
+ elif entry.suffix() == "bmp":
+ toMove.append((entry, "/Data/"))
+ elif entry.suffix() == "txt":
+ toMove.append((entry, "/Scripts/"))
+ else:
+ toMove.append((entry, "/Data/landscape/BW2/"))
+
+ for entry, path in toMove:
+ filetree.move(entry, path, policy=mobase.IFileTree.MERGE)
+
+ return filetree
+
+ def dataLooksValid(
+ self, filetree: mobase.IFileTree
+ ) -> mobase.ModDataChecker.CheckReturn:
+ # qInfo("Data validation start")
+ root = filetree
+ unpackagedMap = False
+
+ for entry in filetree:
+ entryName = entry.name().casefold()
+ canIgnore = any([sub in entryName for sub in self._fileIgnore])
+ if not canIgnore:
+ parent = entry.parent()
+ if parent is not None:
+ if parent != root:
+ parentName = parent.name().casefold()
+ else:
+ # qInfo(str(entryName))
+ parentName = "<black & white 2>"
+
+ if not entry.isDir():
+ if parentName in self._validFileLocation.keys():
+ if (
+ entry.suffix()
+ not in self._validFileLocation[parentName]
+ ):
+ if (
+ entry.suffix() in self._mapFile
+ or entryName == "map.txt"
+ ):
+ unpackagedMap = True
+ else:
+ return mobase.ModDataChecker.INVALID
+ else:
+ unpackagedMap = False
+ if parentName in self._validFolderTree.keys():
+ if entryName not in self._validFolderTree[parentName]:
+ return mobase.ModDataChecker.INVALID
+
+ # qInfo(str(unpackagedMap))
+ if unpackagedMap:
+ return mobase.ModDataChecker.FIXABLE
+ else:
+ return mobase.ModDataChecker.VALID
+
+
+class BlackAndWhite2SaveGame(BasicGameSaveGame):
+ _saveInfLayout = {
+ "start": [0x00000000, 0x00000004],
+ "name": [0x00000004, 0x0000002C],
+ "empty": [0x0000002C, 0x00000104],
+ "land": [0x00000104, 0x00000108],
+ "date": [0x00000108, 0x00000110],
+ "empty1": [0x00000110, 0x00000114],
+ "elapsed": [0x00000114, 0x00000118],
+ "empty2": [0x00000108, 0x0000011C],
+ }
+
+ def __init__(self, filepath: Path):
+ super().__init__(filepath)
+ self._filepath = Path(filepath)
+ self.name: str = ""
+ self.land: int = -1
+ self.elapsed: int = 0
+ self.lastsave: int = 0
+ with open(self._filepath.joinpath("SaveGame.inf"), "rb") as info:
+ # Name embedded in "SaveGame.inf" with UTF-16 encoding
+ self.name = self.readInf(info, "name").decode("utf-16")
+ # Land number embedded in "SaveGame.inf" as an int written in binary
+ self.land = int.from_bytes(self.readInf(info, "land"), "little")
+ # Getting elapsed time in second
+ self.elapsed = int.from_bytes(self.readInf(info, "elapsed"), "little")
+ # Getting date in 100th of nanosecond need to convert NT time
+ # to UNIX time and offset localtime
+ self.lastsave = int(
+ (
+ struct.unpack("q", self.readInf(info, "date"))[0] / 10000
+ - 11644473600000
+ )
+ - (time.localtime().tm_gmtoff * 1000)
+ )
+ info.close()
+
+ def readInf(self, inf: BinaryIO, key: str):
+ inf.seek(self._saveInfLayout[key][0])
+ return inf.read(self._saveInfLayout[key][1] - self._saveInfLayout[key][0])
+
+ def allFiles(self) -> list[str]:
+ files = [str(file) for file in self._filepath.glob("./*")]
+ files.append(str(self._filepath))
+ return files
+
+ def getCreationTime(self) -> QDateTime:
+ return QDateTime.fromMSecsSinceEpoch(self.lastsave)
+
+ def getElapsed(self) -> str:
+ return str(datetime.timedelta(seconds=self.elapsed))
+
+ def getName(self) -> str:
+ return self.name
+
+ def getLand(self) -> str:
+ return str(self.land)
+
+ def getSaveGroupIdentifier(self):
+ return self._filepath.parent.parent.name
+
+
+def getMetadata(savepath: Path, save: mobase.ISaveGame) -> Mapping[str, str]:
+ assert isinstance(save, BlackAndWhite2SaveGame)
+ return {
+ "Name": save.getName(),
+ "Profile": save.getSaveGroupIdentifier(),
+ "Land": save.getLand(),
+ "Saved at": format_date(save.getCreationTime()),
+ "Elapsed time": save.getElapsed(),
+ }
+
+
+PSTART_MENU = (
+ str(os.getenv("ProgramData")) + "\\Microsoft\\Windows\\Start Menu\\Programs"
+)
+
+
+class BlackAndWhite2Game(BasicGame):
+ Name = "Black & White 2 Support Plugin"
+ Author = "Ilyu"
+ Version = "1.0.1"
+
+ GameName = "Black & White 2"
+ GameShortName = "BW2"
+ GameNexusName = "blackandwhite2"
+ GameDataPath = "%GAME_PATH%"
+ GameBinary = "white.exe"
+ GameDocumentsDirectory = "%DOCUMENTS%/Black & White 2"
+ GameSavesDirectory = "%GAME_DOCUMENTS%/Profiles"
+ GameSupportURL = (
+ r"https://github.com/ModOrganizer2/modorganizer-basic_games/wiki/"
+ "Game:-Black-&-White-2"
+ )
+
+ _program_link = PSTART_MENU + "\\Black & White 2\\Black & White® 2.lnk"
+
+ def init(self, organizer: mobase.IOrganizer) -> bool:
+ BasicGame.init(self, organizer)
+
+ self._register_feature(BlackAndWhite2ModDataChecker())
+ self._register_feature(BasicLocalSavegames(self))
+ self._register_feature(
+ BasicGameSaveGameInfo(get_metadata=getMetadata, max_width=400)
+ )
+ return True
+
+ def detectGame(self):
+ super().detectGame()
+
+ program_path = Path(self._program_link)
+ if program_path.exists():
+ installation_path = Path(QFileInfo(self._program_link).symLinkTarget())
+ if installation_path.exists():
+ self.setGamePath(installation_path.parent)
+
+ return
+
+ def executables(self) -> list[mobase.ExecutableInfo]:
+ execs = super().executables()
+
+ """
+ A bat file to load modded executables from VFS.
+ """
+ workaroundPath = self._gamePath + "/" + self.GameBinary[:-4] + ".bat"
+
+ try:
+ workaround = open(workaroundPath, "rt")
+ except FileNotFoundError:
+ with open(workaroundPath, "wt") as workaround:
+ workaround.write('start "" "' + self.GameBinary + '"')
+ workaround.close()
+
+ execs.append(
+ mobase.ExecutableInfo(
+ self.GameShortName + " Modded Exec", QFileInfo(workaroundPath)
+ )
+ )
+
+ return execs
+
+ def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]:
+ profiles: list[Path] = []
+ for path in Path(folder.absolutePath()).glob("*/Saved Games/*"):
+ if (
+ path.name == "Autosave"
+ or path.name == "Pictures"
+ or "_invalid" in path.name
+ ):
+ continue
+ if path.is_dir():
+ saveFolder = QDir(str(path))
+ if not saveFolder.exists("SaveGame.inf"):
+ savePath = saveFolder.absolutePath()
+ QFile.rename(savePath, savePath + "_invalid")
+ continue
+ else:
+ continue
+
+ profiles.append(path)
+
+ return [BlackAndWhite2SaveGame(path) for path in profiles]
+
+
+class BOTGGame(BlackAndWhite2Game):
+ Name = "Black & White 2 Battle of the Gods Support Plugin"
+
+ GameName = "Black & White 2 Battle of the Gods"
+ GameShortName = "BOTG"
+ GameBinary = "BattleOfTheGods.exe"
+ GameDocumentsDirectory = "%DOCUMENTS%/Black & White 2 - Battle of the Gods"
+ GameSavesDirectory = "%GAME_DOCUMENTS%/Profiles"
+
+ _program_link = (
+ PSTART_MENU + "\\Black & White 2 Battle of the Gods"
+ "\\Black & White® 2 Battle of the Gods.lnk"
+ )