Initial commit.

This commit is contained in:
Mikaël Capelle
2020-06-10 23:22:53 +02:00
commit 32d16df52e
9 changed files with 454 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.mypy_cache
__pycache__
+7
View File
@@ -0,0 +1,7 @@
Copyright 2020 Holt59
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+108
View File
@@ -0,0 +1,108 @@
# Mod Organizer 2 - Simple Games Plugin
Mod Organizer 2 meta-plugin to make creating game plugins easier and faster.
## Why?
In order to create a MO2 game plugin, one must implements the `IPluginGame` interface,
but the interface contains a lot of things that most games do not require.
The goal of this meta-plugin is to load create plugins for "simple" games by simply
providing a `.ini` file or a very simply python class.
## How to?
You can either provide a python class or a `.ini` file.
### Using a `.ini` file
You simply need to put your `.ini` file in games with the following content:
```ini
[DEFAULT]
# Name of the plugin (avoid space, whatever, ...):
Name=Witcher 3 Support Plugin
# Your name or username:
Author=Holt59
# Version of the plugin - Does not really make sense for .ini:
Version=1.0.0
# Name of the game, as you want it displayed by MO2:
GameName=The Witcher 3
# Short name of the game, used, e.g., for nexus:
GameShortName=witcher3
# Path to the executable, relative to the game folder:
GameBinary=bin/x64/witcher3.exe
# Name of the folder containing the data, relative to the game folder:
GameDataPath=mods
# Savegame extensions for the game:
GameSaveExtension=sav
```
### Using a Python file
You need to create a class that inherits `BasicGame` and put it in a `.py` file in `games`. Below is
an example for The Witcher 3:
```python
# -*- encoding: utf-8 -*-
from PyQt5.QtCore import QDir
from ..basic_game import BasicGame
class Witcher3Game(BasicGame):
Name: str = "Witcher 3 Support Plugin"
Author: str = "Holt59"
Version: str = "1.0.0a"
GameName: str = "The Witcher 3"
GameShortName: str = "witcher3"
GameBinary: str = "bin/x64/witcher3.exe"
GameDataPath: str = "Mods"
GameSaveExtension: str = "sav"
def steamAPPId(self):
return "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). If you override the method, you do not have to provide
the attribute:
```python
class Witcher3Game(BasicGame):
Name: str = "Witcher 3 Support Plugin"
Author: str = "Holt59"
GameName: str = "The Witcher 3"
GameShortName: str = "witcher3"
GameBinary: str = "bin/x64/witcher3.exe"
GameDataPath: str = "Mods"
GameSaveExtension: str = "sav"
def version(self):
# Don't forget to import mobase!
return mobase.VersionInfo(1, 0, 0, mobase.ReleaseType.final)
def steamAPPId(self):
return "292030"
def savesDirectory(self):
return QDir(self.documentsDirectory().absoluteFilePath("gamesaves"))
```
+43
View File
@@ -0,0 +1,43 @@
# -*- encoding: utf-8 -*-
import importlib
import glob
import os
import typing
from .basic_game import BasicGame
from .basic_game_ini import BasicIniGame
# List of game class from python:
game_plugins: typing.List[BasicGame] = []
# We are going to list all game plugins:
curpath = os.path.abspath(os.path.dirname(__file__))
# List all the .ini files:
for file in glob.glob(os.path.join(curpath, "games", "*.ini")):
game_plugins.append(BasicIniGame(file))
# List all the python plugins:
for file in glob.glob(os.path.join(curpath, "games", "*.py")):
module_p = os.path.relpath(file, os.path.join(curpath, "games"))
if module_p == "__init__.py":
continue
# Import the module:
module = importlib.import_module(".games." + module_p[:-3], __package__)
# Lookup game plugins:
for name in dir(module):
if hasattr(module, name):
obj = getattr(module, name)
if (
isinstance(obj, type)
and issubclass(obj, BasicGame)
and obj is not BasicGame
):
game_plugins.append(obj())
def createPlugins():
return game_plugins
+237
View File
@@ -0,0 +1,237 @@
# -*- encoding: utf-8 -*-
import sys
import typing
from PyQt5.QtCore import QDir, QFileInfo, QStandardPaths
import mobase
class BasicGame(mobase.IPluginGame):
""" This class implements some methods from mobase.IPluginGame
to make it easier to create game plugins without having to implement
all the methods of mobase.IPluginGame. """
# List of fields that can be provided by child class:
Name: str
Author: str
Version: str
GameName: str
GameShortName: str
GameBinary: str
GameDataPath: str
GameSaveExtension: str
# File containing the plugin:
_fromName: str
# Organizer obtained in init:
_organizer: mobase.IOrganizer
# Path to the game, as set by MO2:
_gamePath: str
# The feature map:
_featureMap: typing.Dict = {}
# List of attributes - These should be the same as function with a
# _ prefix:
_name: str
_author: str
_version: mobase.VersionInfo
_gameName: str
_gameShortName: str
_binaryName: str
_dataDirectory: str
_savegameExtension: str
# Match the name of the public attribute (from child class), to the
# name of the protected attribute and the corresponding function:
NAME_MAPPING = [
["Name", "name"],
["Author", "author"],
["Version", "version"],
["GameName", "gameName"],
["GameShortName", "gameShortName"],
["GameBinary", "binaryName"],
["GameDataPath", "dataDirectory"],
["GameSaveExtension", "savegameExtension"],
]
def __init__(self):
super(BasicGame, self).__init__()
# if not hasattr(self, "_fromName"):
self._fromName = self.__class__.__name__
# We init the member and check that everything is provided:
for pub, prt in self.NAME_MAPPING:
if hasattr(self, pub):
value = getattr(self, pub)
if pub == "Version":
value = mobase.VersionInfo(value)
setattr(self, "_" + prt, value)
elif getattr(self, prt) is getattr(BasicGame, prt):
print(
"Basic game plugin from {} is missing {} property.".format(
self._fromName, pub
),
file=sys.stderr,
)
"""
Here IPlugin interface stuff.
"""
def init(self, organizer):
self._organizer = organizer
return True
def name(self):
return self._name
def author(self):
return self._author
def description(self):
return "Adds basic support for game {}.".format(self.gameName())
def version(self):
return self._version
def isActive(self):
return True
def settings(self):
return []
def gameName(self):
return self._gameName
def gameShortName(self):
return self._gameShortName
def gameIcon(self):
return mobase.getIconForExecutable(
self.gameDirectory().absoluteFilePath(self.binaryName())
)
def validShortNames(self):
return []
def gameNexusName(self):
return ""
def nexusModOrganizerID(self):
return 0
def nexusGameID(self):
return 0
def steamAPPId(self):
return ""
def binaryName(self):
return self._binaryName
def getLauncherName(self):
return ""
def executables(self):
return [
mobase.ExecutableInfo(
self.gameName(),
QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())),
)
]
def savegameExtension(self):
return self._savegameExtension
def savegameSEExtension(self):
return ""
def initializeProfile(self, path, settings):
pass
def primarySources(self):
return []
def primaryPlugins(self):
return []
def gameVariants(self):
return []
def setGameVariant(self, variantStr):
pass
def gameVersion(self):
pversion = mobase.getProductVersion(
self.gameDirectory().absoluteFilePath(self.binaryName())
)
if not pversion:
pversion = mobase.getFileVersion(
self.gameDirectory().absoluteFilePath(self.binaryName())
)
return pversion
def iniFiles(self):
return []
def DLCPlugins(self):
return []
def CCPlugins(self):
return []
def loadOrderMechanism(self):
return mobase.LoadOrderMechanism.PluginsTxt
def sortMechanism(self):
return mobase.SortMechanism.NONE
def looksValid(self, aQDir: QDir):
return aQDir.exists(self.binaryName())
def isInstalled(self):
return False
def gameDirectory(self):
"""
@return directory (QDir) to the game installation.
"""
return QDir(self._gamePath)
def dataDirectory(self):
return QDir(self.gameDirectory().absoluteFilePath(self._dataDirectory))
def setGamePath(self, pathStr):
self._gamePath = pathStr
def documentsDirectory(self):
folders = [
"{}/My Games/{}".format(
QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation),
self.gameName(),
),
"{}/{}".format(
QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation),
self.gameName(),
),
]
for folder in folders:
qdir = QDir(folder)
if qdir.exists():
return qdir
return None
def savesDirectory(self):
return self.documentsDirectory()
def _featureList(self):
return self._featureMap
+22
View File
@@ -0,0 +1,22 @@
# -*- encoding: utf-8 -*-
import configparser
import os
from .basic_game import BasicGame
class BasicIniGame(BasicGame):
def __init__(self, path: str):
# Set the _fromName to get more "correct" errors:
self._fromName = os.path.basename(path)
# Read the file:
config = configparser.ConfigParser()
config.read(path)
# Just fill the class with values:
for k, v in config["DEFAULT"].items():
setattr(self, k, v)
super().__init__()
View File
+25
View File
@@ -0,0 +1,25 @@
# -*- encoding: utf-8 -*-
from PyQt5.QtCore import QDir
from ..basic_game import BasicGame
class Witcher3Game(BasicGame):
Name: str = "Witcher 3 Support Plugin"
Author: str = "Holt59"
Version: str = "1.0.0a"
GameName: str = "The Witcher 3"
GameShortName: str = "witcher3"
GameBinary: str = "bin/x64/witcher3.exe"
GameDataPath: str = "Mods"
GameSaveExtension: str = "sav"
def steamAPPId(self):
return "292030"
def savesDirectory(self):
return QDir(self.documentsDirectory().absoluteFilePath("gamesaves"))
+10
View File
@@ -0,0 +1,10 @@
[flake8]
# Use black line length:
max-line-length = 88
extend-ignore =
# See https://github.com/PyCQA/pycodestyle/issues/373
E203, E266
[mypy]
warn_return_any = True
warn_unused_configs = True