diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 047c78c1..b87d5827 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,27 @@ repos: - id: ruff args: [--extend-select, I, --fix] - id: ruff-format + - repo: https://github.com/python-poetry/poetry-plugin-export + rev: "1.9.0" + hooks: + - id: poetry-export + args: + [ + "--without=dev", + "-f", + "requirements.txt", + "-o", + "plugin-requirements.txt", + ] + - repo: local + hooks: + - id: update-readme-games + name: Update list of games in README.md + entry: python + language: python + # files: ^(.*/)?\.py$ + args: [pre-commit-hooks/update-readme-games.py] + additional_dependencies: [jinja2==3.1.6] ci: autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..52b6898c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,152 @@ + +# ModOrganizer 2 - Basic Games - How to Contribute? + +## How to add a new game? + +You can create a plugin by providing a python class in the `games` folder. + +**Note:** If your game plugin does not load properly, you should set the log level +to debug and look at the `mo_interface.log` file. + +You need to create a class that inherits `BasicGame` and put it in a `game_XX.py` in `games`. +Below is an example for The Witcher 3 (see also [games/game_witcher3.py](games/game_witcher3.py)): + +```python +from PyQt6.QtCore import QDir +from ..basic_game import BasicGame + + +class Witcher3Game(BasicGame): + + Name = "Witcher 3 Support Plugin" + Author = "Holt59" + Version = "1.0.0a" + + GameName = "The Witcher 3" + GameShortName = "witcher3" + GameBinary = "bin/x64/witcher3.exe" + GameDataPath = "Mods" + GameSaveExtension = "sav" + GameSteamId = 292030 + + def savesDirectory(self): + return QDir(self.documentsDirectory().absoluteFilePath("gamesaves")) +``` + +`BasicGame` inherits `IPluginGame` so you can override methods if you need to. +Each attribute you provide corresponds to a method (e.g., `Version` corresponds +to the `version` method, see the table below). If you override the method, you do +not have to provide the attribute: + +```python +from PyQt6.QtCore import QDir +from ..basic_game import BasicGame + +import mobase + + +class Witcher3Game(BasicGame): + + Name = "Witcher 3 Support Plugin" + Author = "Holt59" + + GameName = "The Witcher 3" + GameShortName = "witcher3" + GameBinary = "bin/x64/witcher3.exe" + GameDataPath = "Mods" + GameSaveExtension = "sav" + GameSteamId = 292030 + + def version(self): + # Don't forget to import mobase! + return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final) + + def savesDirectory(self): + return QDir(self.documentsDirectory().absoluteFilePath("gamesaves")) +``` + +### List of valid keys + +| Name | Description | `IPluginGame` method | Python | +|------|-------------|----------------------|--------| +| Name | Name of the plugin | `name` | `str` | +| Author | Author of the plugin | `author` | `str` | +| Version | Version of the plugin | `version` | `str` or `mobase.VersionInfo` | +| Description| Description (Optional) | `description` | `str` | +| GameName | Name of the game, as displayed by MO2 | `gameName` | `str` | +| GameShortName | Short name of the game | `gameShortName` | `str` | +| GameNexusName| Nexus name of the game (Optional, default to `GameShortName`) | `gameNexusName` | `str` | +| GameValidShortNames | Other valid short names (Optional) | `validShortNames` | `List[str]` or comma-separated list of values | +| GameNexusId | Nexus ID of the game (Optional) | `nexusGameID` | `str` or `int` | +| GameBinary | Name of the game executable, relative to the game path | `binaryName` | `str` | +| GameLauncher | Name of the game launcher, relative to the game path (Optional) | `getLauncherName` | `str` | +| GameDataPath | Name of the folder containing mods, relative to game folder| `dataDirectory` | | +| GameDocumentsDirectory | Documents directory (Optional) | `documentsDirectory` | `str` or `QDir` | +| GameIniFiles | Config files in documents, for profile specific config (Optional) | `iniFiles` | `str` or `List[str]` | +| GameSavesDirectory | Directory containing saves (Optional, default to `GameDocumentsDirectory`) | `savesDirectory` | `str` or `QDir` | +| GameSaveExtension | Save file extension (Optional) `savegameExtension` | `str` | +| GameSteamId | Steam ID of the game (Optional) | `steamAPPId` | `List[str]` or `str` or `int` | +| GameGogId | GOG ID of the game (Optional) | `gogAPPId` | `List[str]` or `str` or `int` | +| GameOriginManifestIds | Origin Manifest ID of the game (Optional) | `originManifestIds` | `List[str]` or `str` | +| GameOriginWatcherExecutables | Executables to watch for Origin DRM (Optional) | `originWatcherExecutables` | `List[str]` or `str` | +| GameEpicId | Epic ID (`AppName`) of the game (Optional) | `epicAPPId` | `List[str]` or `str` | +| GameEaDesktopId | EA Desktop ID of the game (Optional) | `eaDesktopContentId` | `List[str]` or `str` or `int` | + +You can use the following variables for `str`: + +- `%DOCUMENTS%` will be replaced by the standard *Documents* folder. +- `%GAME_PATH%` will be replaced by the path to the game folder. +- `%GAME_DOCUMENTS%` will be replaced by the value of `GameDocumentsDirectory`. + +## Extra features + +The meta-plugin provides some useful extra feature: + +1. **Automatic Steam, GOG, Origin, Epic Games and EA Desktop detection:** If you provide + Steam, GOG, Origin or Epic IDs for the game (via `GameSteamId`, `GameGogId`, + `GameOriginManifestIds`, `GameEpicId` or `GameEaDesktopId`), the game will be listed + in the list of available games when creating a new MO2 instance (if the game is + installed via Steam, GOG, Origin, Epic Games / Legendary or EA Desktop). +2. **Basic save game preview / metadata** (Python): If you can easily obtain a picture + (file) and/or metadata (like from json) for any saves, you can provide basic save-game + preview by using the `BasicGameSaveGameInfo`. See + [games/game_witcher3.py](games/game_witcher3.py) and + [games/game_bladeandsorcery.py](games/game_bladeandsorcery.py) for more details. +3. **Basic local save games** (Python): profile specific save games, as in [games/game_valheim.py](games/game_valheim.py). +4. **Basic mod data checker** (Python): + Check and fix different mod archive layouts for an automatic installation with the proper + file structure, using simple (glob) patterns via `BasicModDataChecker`. + See [games/game_valheim.py](games/game_valheim.py) and [game_subnautica.py](games/game_subnautica.py) for an example. + +Game IDs can be found here: + +- For Steam on [Steam Database](https://steamdb.info/) +- For GOG on [GOG Database](https://www.gogdb.org/) +- For Origin from `C:\ProgramData\Origin\LocalContent` (.mfst files) +- For Epic Games (`AppName`) from: + - `C:\ProgramData\Epic\EpicGamesLauncher\Data\Manifests\` (.item files) + - or `C:\ProgramData\Epic\EpicGamesLauncher\UnrealEngineLauncher\LauncherInstalled.dat` + - or [Unofficial EGS ID DB](https://erri120.github.io/egs-db/) +- For Legendary (alt. Epic launcher) via command `legendary list-games` + or from: `%USERPROFILE%\.config\legendary\installed.json` +- For EA Desktop from `\\__Installer\installerdata.xml` + +## Contribute + +We recommend using a dedicated Python environment to write a new basic game plugins. + +1. Install the required version of Python - Currently Python 3.12 (MO2 2.5). +2. Remove the repository at `${MO2_INSTALL}/plugins/basic_games`. +3. Clone this repository at the location of the old plugin ( + `${MO2_INSTALL}/plugins/basic_games`). +4. Place yourself inside the cloned folder and: + + ```bash + # create a virtual environment (recommended) + py -3.12 -m venv .\venv + .\venv\scripts\Activate.ps1 + + # "install" poetry and the development package + pip install poetry + poetry install + ``` diff --git a/README.md b/README.md index 2c0e98bc..b888dcd1 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,10 @@ providing a very simple python class. ## How to install? -Download the archive for your MO2 version and extract it directly into your MO2 `plugins` folder. - -- Mod Organizer **2.3.2**: [Download](https://github.com/ModOrganizer2/modorganizer-basic_games/releases/download/v0.0.3/basic_games-0.0.3.zip) - and extract in your `plugins/` folder (see below). -- Mod Organizer **2.4**: Basic games is included in Mod Organizer 2.4. - - If you want to use new game plugins that have not been included in the - release, [download the latest archive](https://github.com/ModOrganizer2/modorganizer-basic_games/archive/master.zip) and extract the files - in the existing `basic_games` folder, overwriting existing files. +Basic games is included in Mod Organizer 2.4, if you want to use new game plugins that +have not been included in the release, +[download the latest archive](https://github.com/ModOrganizer2/modorganizer-basic_games/archive/master.zip) +and extract the files in the existing `basic_games` folder, overwriting existing files. **Important:** Extract the *folder* in your `plugins` folder, not the individual files. Your `plugins` folder should look like this: @@ -45,6 +41,8 @@ You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic ## Supported games + + | Game | Author | File | Extras | |------|--------|------|--------| | The Binding of Isaac: Rebirth — [STEAM](https://store.steampowered.com/app/250900/The_Binding_of_Isaac_Rebirth/) |[EzioTheDeadPoet](https://github.com/EzioTheDeadPoet)|[game_thebindingofisaacrebirth.py](games/game_thebindingofisaacrebirth.py)|| @@ -82,152 +80,8 @@ You can rename `modorganizer-basic_games-xxx` to whatever you want (e.g., `basic | Yu-Gi-Oh! Master Duel — [STEAM](https://store.steampowered.com/app/1449850/) | [The Conceptionist](https://github.com/the-conceptionist) & [uwx](https://github.com/uwx) | [game_masterduel.py](games/game_masterduel.py) | | | Zeus and Poseidon — [GOG](https://www.gog.com/game/zeus_poseidon) / [STEAM](https://store.steampowered.com/app/566050/Zeus__Poseidon/) | [Holt59](https://github.com/holt59/) | [game_zeusandpoiseidon.py](games/game_zeusandpoiseidon.py) | | -## How to add a new game? - -You can create a plugin by providing a python class in the `games` folder. - -**Note:** If your game plugin does not load properly, you should set the log level -to debug and look at the `mo_interface.log` file. - -You need to create a class that inherits `BasicGame` and put it in a `game_XX.py` in `games`. -Below is an example for The Witcher 3 (see also [games/game_witcher3.py](games/game_witcher3.py)): - -```python -from PyQt6.QtCore import QDir -from ..basic_game import BasicGame - - -class Witcher3Game(BasicGame): - - Name = "Witcher 3 Support Plugin" - Author = "Holt59" - Version = "1.0.0a" - - GameName = "The Witcher 3" - GameShortName = "witcher3" - GameBinary = "bin/x64/witcher3.exe" - GameDataPath = "Mods" - GameSaveExtension = "sav" - GameSteamId = 292030 - - def savesDirectory(self): - return QDir(self.documentsDirectory().absoluteFilePath("gamesaves")) -``` - -`BasicGame` inherits `IPluginGame` so you can override methods if you need to. -Each attribute you provide corresponds to a method (e.g., `Version` corresponds -to the `version` method, see the table below). If you override the method, you do -not have to provide the attribute: - -```python -from PyQt6.QtCore import QDir -from ..basic_game import BasicGame - -import mobase - - -class Witcher3Game(BasicGame): + - Name = "Witcher 3 Support Plugin" - Author = "Holt59" - - GameName = "The Witcher 3" - GameShortName = "witcher3" - GameBinary = "bin/x64/witcher3.exe" - GameDataPath = "Mods" - GameSaveExtension = "sav" - GameSteamId = 292030 - - def version(self): - # Don't forget to import mobase! - return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final) - - def savesDirectory(self): - return QDir(self.documentsDirectory().absoluteFilePath("gamesaves")) -``` - -### List of valid keys - -| Name | Description | `IPluginGame` method | Python | -|------|-------------|----------------------|--------| -| Name | Name of the plugin | `name` | `str` | -| Author | Author of the plugin | `author` | `str` | -| Version | Version of the plugin | `version` | `str` or `mobase.VersionInfo` | -| Description| Description (Optional) | `description` | `str` | -| GameName | Name of the game, as displayed by MO2 | `gameName` | `str` | -| GameShortName | Short name of the game | `gameShortName` | `str` | -| GameNexusName| Nexus name of the game (Optional, default to `GameShortName`) | `gameNexusName` | `str` | -| GameValidShortNames | Other valid short names (Optional) | `validShortNames` | `List[str]` or comma-separated list of values | -| GameNexusId | Nexus ID of the game (Optional) | `nexusGameID` | `str` or `int` | -| GameBinary | Name of the game executable, relative to the game path | `binaryName` | `str` | -| GameLauncher | Name of the game launcher, relative to the game path (Optional) | `getLauncherName` | `str` | -| GameDataPath | Name of the folder containing mods, relative to game folder| `dataDirectory` | | -| GameDocumentsDirectory | Documents directory (Optional) | `documentsDirectory` | `str` or `QDir` | -| GameIniFiles | Config files in documents, for profile specific config (Optional) | `iniFiles` | `str` or `List[str]` | -| GameSavesDirectory | Directory containing saves (Optional, default to `GameDocumentsDirectory`) | `savesDirectory` | `str` or `QDir` | -| GameSaveExtension | Save file extension (Optional) `savegameExtension` | `str` | -| GameSteamId | Steam ID of the game (Optional) | `steamAPPId` | `List[str]` or `str` or `int` | -| GameGogId | GOG ID of the game (Optional) | `gogAPPId` | `List[str]` or `str` or `int` | -| GameOriginManifestIds | Origin Manifest ID of the game (Optional) | `originManifestIds` | `List[str]` or `str` | -| GameOriginWatcherExecutables | Executables to watch for Origin DRM (Optional) | `originWatcherExecutables` | `List[str]` or `str` | -| GameEpicId | Epic ID (`AppName`) of the game (Optional) | `epicAPPId` | `List[str]` or `str` | -| GameEaDesktopId | EA Desktop ID of the game (Optional) | `eaDesktopContentId` | `List[str]` or `str` or `int` | - -You can use the following variables for `str`: - -- `%DOCUMENTS%` will be replaced by the standard *Documents* folder. -- `%GAME_PATH%` will be replaced by the path to the game folder. -- `%GAME_DOCUMENTS%` will be replaced by the value of `GameDocumentsDirectory`. - -## Extra features - -The meta-plugin provides some useful extra feature: - -1. **Automatic Steam, GOG, Origin, Epic Games and EA Desktop detection:** If you provide - Steam, GOG, Origin or Epic IDs for the game (via `GameSteamId`, `GameGogId`, - `GameOriginManifestIds`, `GameEpicId` or `GameEaDesktopId`), the game will be listed - in the list of available games when creating a new MO2 instance (if the game is - installed via Steam, GOG, Origin, Epic Games / Legendary or EA Desktop). -2. **Basic save game preview / metadata** (Python): If you can easily obtain a picture - (file) and/or metadata (like from json) for any saves, you can provide basic save-game - preview by using the `BasicGameSaveGameInfo`. See - [games/game_witcher3.py](games/game_witcher3.py) and - [games/game_bladeandsorcery.py](games/game_bladeandsorcery.py) for more details. -3. **Basic local save games** (Python): profile specific save games, as in [games/game_valheim.py](games/game_valheim.py). -4. **Basic mod data checker** (Python): - Check and fix different mod archive layouts for an automatic installation with the proper - file structure, using simple (glob) patterns via `BasicModDataChecker`. - See [games/game_valheim.py](games/game_valheim.py) and [game_subnautica.py](games/game_subnautica.py) for an example. - -Game IDs can be found here: - -- For Steam on [Steam Database](https://steamdb.info/) -- For GOG on [GOG Database](https://www.gogdb.org/) -- For Origin from `C:\ProgramData\Origin\LocalContent` (.mfst files) -- For Epic Games (`AppName`) from: - - `C:\ProgramData\Epic\EpicGamesLauncher\Data\Manifests\` (.item files) - - or `C:\ProgramData\Epic\EpicGamesLauncher\UnrealEngineLauncher\LauncherInstalled.dat` - - or [Unofficial EGS ID DB](https://erri120.github.io/egs-db/) -- For Legendary (alt. Epic launcher) via command `legendary list-games` - or from: `%USERPROFILE%\.config\legendary\installed.json` -- For EA Desktop from `\\__Installer\installerdata.xml` - -## Contribute - -We recommend using a dedicated Python environment to write a new basic game plugins. - -1. Install the required version of Python --- Currently Python 3.11 (MO2 2.5). -2. Remove the repository at `${MO2_INSTALL}/plugins/basic_games`. -3. Clone this repository at the location of the old plugin ( - `${MO2_INSTALL}/plugins/basic_games`). -4. Place yourself inside the cloned folder and: - - ```bash - # create a virtual environment (recommended) - py -3.11 -m venv .\venv - .\venv\scripts\Activate.ps1 +## How to add a new game? - # "install" poetry and the development package - pip install poetry - poetry install - ``` +See [CONTRIBUTING](./CONTRIBUTING.md). diff --git a/plugin-requirements.txt b/plugin-requirements.txt index 1f1295ea..fad6127b 100644 --- a/plugin-requirements.txt +++ b/plugin-requirements.txt @@ -1,3 +1,37 @@ -psutil==5.8.0 -vdf==3.4 -lzokay==1.1.5 +lzokay==1.1.5 ; python_version >= "3.12" and python_version < "4.0" \ + --hash=sha256:0d6b1d80121cdbc3cb106b3fafc03b139f3de3c134ce58b38fcc2d751fc5b013 \ + --hash=sha256:1c83a0f1cc7628237466c298e676a64a1ab4914ccf628b3775f8680477b77481 \ + --hash=sha256:266db5697a0d01428e4c31633f8dc1e87666ad17bb46b4c48ec7b7b8b70b94bc \ + --hash=sha256:3c2e81d178161de58bf233eec87c6ef3dba96fecdd9ba72f8f2c8790adc18f74 \ + --hash=sha256:4389b8b6f3c95aaa33308e2129b026140c850d94440fbea4fdeabcd42477f3b2 \ + --hash=sha256:4ee2c7144dd3916cf0392d3851cebe8f28136e7998b7a50a91f63d7276327d70 \ + --hash=sha256:4faeefdef8132c4db995de8e96a649c890c63a81cee06b3bbcae9d224302fa09 \ + --hash=sha256:5426a80664c471c18a47766111629cae5c9435ca8421cd208b00b10de5df5df9 \ + --hash=sha256:829bfd081c8f03e85994d920ce7cc8d45033d7c467fc2fadc17b5c47667ec045 \ + --hash=sha256:867a9fd4a76c830b17366fd2e8bd03fe9b29cf5c13c42bc19562e411cf648dad \ + --hash=sha256:88fa389193cde7feb13fd4b57b6b8d626a6159bfe84cd795e664e0a738debb42 \ + --hash=sha256:a4df719d5fef922e0f43ec2408cd70280171ded18ab096c1e88fd1c3ca4dff46 \ + --hash=sha256:b8f07bdd8c3bd9443d3f5ca69a03b9dc35522c5c92fce61db087ded1f348274f \ + --hash=sha256:eec7679194643bc1417f8883ef3c9d27e95e00632d4d2a7dc9ef1a1ca92cdd95 \ + --hash=sha256:f1997c6994239ea24a2c7c7812f9e06bceb6b216c5537b85d8b585dbfd711cb0 \ + --hash=sha256:f828864453ddfca036de7470da74245d8dcf369c521291037b1b01ac7c5dbd83 +psutil==5.9.8 ; python_version >= "3.12" and python_version < "4.0" \ + --hash=sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d \ + --hash=sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73 \ + --hash=sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8 \ + --hash=sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2 \ + --hash=sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e \ + --hash=sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36 \ + --hash=sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7 \ + --hash=sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c \ + --hash=sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee \ + --hash=sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421 \ + --hash=sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf \ + --hash=sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81 \ + --hash=sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0 \ + --hash=sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631 \ + --hash=sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4 \ + --hash=sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8 +vdf==3.4 ; python_version >= "3.12" and python_version < "4.0" \ + --hash=sha256:68c1a125cc49e343d535af2dd25074e9cb0908c6607f073947c4a04bbe234534 \ + --hash=sha256:fd5419f41e07a1009e5ffd027c7dcbe43d1f7e8ef453aeaa90d9d04b807de2af diff --git a/poetry.lock b/poetry.lock index fa62e2c4..b4bcc0ec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,23 @@ # This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "lzokay" version = "1.1.5" @@ -29,6 +47,77 @@ files = [ [package.extras] test = ["pytest"] +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + [[package]] name = "mobase-stubs" version = "2.5.2" @@ -119,7 +208,7 @@ version = "6.7.0" description = "Python bindings for the Qt cross platform application toolkit" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "PyQt6-6.7.0-1-cp38-abi3-macosx_10_14_universal2.whl", hash = "sha256:656734112853fde1be0963f0ad362e5efd87ba6c6ff234cb1f9fe8003ee254e6"}, {file = "PyQt6-6.7.0-1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fa2d27fc2f5340f3f1e145c815101ef4550771a9e4bfafd4c7c2479fe83d9488"}, @@ -140,7 +229,7 @@ version = "6.7.3" description = "The subset of a Qt installation needed by PyQt6." optional = false python-versions = "*" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "PyQt6_Qt6-6.7.3-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:f517a93b6b1a814d4aa6587adc312e812ebaf4d70415bb15cfb44268c5ad3f5f"}, {file = "PyQt6_Qt6-6.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8551732984fb36a5f4f3db51eafc4e8e6caf18617365830285306f2db17a94c2"}, @@ -155,7 +244,7 @@ version = "13.10.0" description = "The sip module support for PyQt6" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "PyQt6_sip-13.10.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7b1258963717cfae1d30e262bb784db808072a8a674d98f57c2076caaa50499"}, {file = "PyQt6_sip-13.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d27a3fed2a461f179d3cde6a74530fbad629ccaa66ed739b9544fda1932887af"}, @@ -334,4 +423,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.12,<4.0" -content-hash = "77f80ef6ef2075043826e77efc80fdacc537705142b39493709d7391481ab784" +content-hash = "6b67738930d3b15b9b117e659f265cee6e613b3a5ee1fccef99aa418ef049bea" diff --git a/pre-commit-hooks/update-readme-games.jinja b/pre-commit-hooks/update-readme-games.jinja new file mode 100644 index 00000000..d80c940d --- /dev/null +++ b/pre-commit-hooks/update-readme-games.jinja @@ -0,0 +1,10 @@ +{%- for game in games -%} +| {{ game.name }} | {%- for author in game.authors -%} +{%- if author.homepage -%} +[{{ author.name }}]({{ author.homepage }}) +{%- else -%} +{{ author.name }} +{%- endif -%} +{%- if not loop.last %}, {% endif %} +{%- endfor -%} | [{{ game.file.split('/') | last }}]({{ game.file }}) | | +{% endfor %} diff --git a/pre-commit-hooks/update-readme-games.py b/pre-commit-hooks/update-readme-games.py new file mode 100644 index 00000000..820bcf41 --- /dev/null +++ b/pre-commit-hooks/update-readme-games.py @@ -0,0 +1,149 @@ +import ast +import logging +from pathlib import Path +from typing import TypedDict, cast + +from jinja2 import Template + +START_TAG = "" +END_TAG = "" + +_FILE = Path(__file__) +ROOT = _FILE.parent.parent +TPLT = _FILE.with_suffix(".jinja") + +# careful with the new lines here +HEADER = """ + + +| Game | Author | File | Extras | +|------|--------|------|--------| +""" +FOOTER = """ +""" + +CUSTOM_AUTHOR_HOMEPAGES = { + "Luca/EzioTheDeadPoet": "https://eziothedeadpoet.github.io/AboutMe/", + "R3z Shark": "", + "Miner Of Worlds": "", + "Kane Dou": "", + "Ryan Young": "", + "The Conceptionist": "", +} + + +class Author(TypedDict): + name: str + homepage: str + + +class BasicGameInformation(TypedDict, total=False): + name: str + authors: list[Author] + file: str + + +def extract_basic_games(path: Path): + logging.info(f"extracting basic games from {path}...") + with open(path, "r") as fp: + module = ast.parse(fp.read()) + + games: list[ast.ClassDef] = [] + for stmt in module.body: + if not isinstance(stmt, ast.ClassDef): + continue + + for base in stmt.bases: + if isinstance(base, ast.Name) and base.id == "BasicGame": + games.append(stmt) + break + + # not sure if this is possible? would mean something like xxx.BasicGame + if isinstance(base, ast.Attribute) and base.attr == "BasicGame": + games.append(stmt) + break + + return games + + +def extract_basic_game_information(path: Path, game: ast.ClassDef): + value: BasicGameInformation = { + "file": "https://github.com/ModOrganizer/modorganizer-basic_games/blob/master/" + + path.relative_to(ROOT).as_posix() + } + for stmt in game.body: + if not isinstance(stmt, ast.Assign): + continue + + # skip multiple assignments (should not be any in a class?) + if len(stmt.targets) != 1: + continue + + target = stmt.targets[0] + if not isinstance(target, ast.Name): # can this happen? + continue + + match target.id: + case "GameName": + assert isinstance(stmt.value, ast.Constant) + value["name"] = stmt.value.value + case "Author": + assert isinstance(stmt.value, ast.Constant) + author_s = cast(str, stmt.value.value) + + authors = [p.strip() for p in author_s.split(",")] + authors = [p.strip() for pp in authors for p in pp.split("&")] + value["authors"] = sorted( + [ + { + "name": author, + "homepage": CUSTOM_AUTHOR_HOMEPAGES.get( + author, f"https://github.com/{author}" + ), + } + for author in authors + ], + key=lambda a: a["name"], + ) + case _: + pass + + print(stmt, stmt.value, target.id) + return value + + +def generate_table(): + # list the games + games: list[tuple[Path, ast.ClassDef]] = [] + for path in ROOT.joinpath("games").glob("**/*.py"): + if path.parent.name == "quarantine": + continue + + games.extend((path, game) for game in extract_basic_games(path)) + + infos = [extract_basic_game_information(path, game) for path, game in games] + + with open(TPLT, "r") as fp: + template = Template(fp.read()) + + content = template.render(games=sorted(infos, key=lambda g: g.get("name", ""))) + + return HEADER + content + FOOTER + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + + # read the README content + with open(ROOT.joinpath("README.md"), "r") as fp: + readme = fp.read() + + # find the start and end block + start = readme.find(START_TAG) + end = readme.find(END_TAG) + assert start >= 0 and end >= 0 + + readme = readme[:start] + START_TAG + "\n" + generate_table() + readme[end:] + + with open(ROOT.joinpath("README2.md"), "w") as fp: + fp.write(readme) diff --git a/pyproject.toml b/pyproject.toml index 3ec5c87d..ea1107c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,11 +13,13 @@ dynamic = ["dependencies"] [tool.poetry] package-mode = false +[tool.poetry.requires-plugins] +poetry-plugin-export = ">=1.8" + [tool.poetry.dependencies] psutil = "^5.8.0" vdf = "3.4" lzokay = "1.1.5" -pyqt6 = "6.7.0" [tool.poetry.group.dev.dependencies] mobase-stubs = "^2.5.2" @@ -26,6 +28,7 @@ pyright = "^1.1.400" ruff = "^0.11.7" types-psutil = "^5.9.5.20240516" poethepoet = "^0.34.0" +jinja2 = "3.1.6" [build-system] requires = ["poetry-core (>=2.0)"]