diff options
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | LICENSE.txt | 28 | ||||
-rw-r--r-- | README.md | 60 | ||||
-rw-r--r-- | ardsmm/__init__.py | 72 | ||||
-rw-r--r-- | ardsmm/__main__.py | 95 | ||||
-rw-r--r-- | examples/config.json | 31 | ||||
-rw-r--r-- | examples/mods.txt | 40 | ||||
-rw-r--r-- | pyproject.toml | 25 |
8 files changed, 357 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1dcf03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +*.egg-info +*.pyc +*/_version.py +.venv +.idea diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..9bd2278 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Joseph Hunkeler + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..66bd8fc --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Arma Reforger Dedicated Server Mod Manager (ardsmm) + +## Installation + +### Using Virtualenv + +``` +python -m venv venv + +# If on Windows: +.\venv\Scripts\activate + +# If on Linux/Unix: +source venv/bin/activate + +pip install git+https://github.com/jhunkeler/ardsmm +``` + +## Using Conda + +``` +conda create -n "ardsmm" python +conda activate ardsmm +pip install git+https://github.com/jhunkeler/ardsmm +``` + +## Usage + +``` +usage: ardsmm [-h] [-i] [-o OUTPUT_FILE] [-m MOD_FILE] [-I INDENT] [-V] [configfile] + +positional arguments: + configfile An Arma Reforger dedicated server JSON config + +options: + -h, --help show this help message and exit + -i, --in-place modify JSON config file in-place + -o OUTPUT_FILE, --output-file OUTPUT_FILE + write JSON output to file (default: stdout) + -m MOD_FILE, --mod-file MOD_FILE + read mods from file (default: stdin) + -I INDENT, --indent INDENT + set JSON indentation level (default: 4) + -V, --version display version number and exit + +``` + +## Examples + +Create a new configuration file with your `config.json` as the baseline. Mod definitions are appended to `{ "game": { "mods": [] } }`. Mods are appended to `{ "game": { "mods": [] } }`: + +``` +ardsmm -o test.json -m examples/mods.txt examples/config.json +``` + +Or add mods to an existing configuration file: + +``` +ardsmm -i -m examples/mods.txt examples/config.json +``` diff --git a/ardsmm/__init__.py b/ardsmm/__init__.py new file mode 100644 index 0000000..cb0b90e --- /dev/null +++ b/ardsmm/__init__.py @@ -0,0 +1,72 @@ +import json +import sys +from typing import Any +from ardsmm._version import __version__, __version_tuple__ + + +class ArmaConfigMod: + data: dict[Any, Any] + MOD_SCHEMA = { + "modId": str, + "name": str, + "version": str, + } + + def __init__(self, mod_dict): + if isinstance(mod_dict, str): + if mod_dict.endswith(","): + mod_dict = mod_dict[:-1] + self.data = json.loads(mod_dict) + else: + self.data = mod_dict + self.check() + + def check(self): + for key, expected_type in self.MOD_SCHEMA.items(): + if key not in self.data.keys(): + raise KeyError(f"Mod key '{key}' is missing") + elif expected_type is not type(self.data[key]): + raise TypeError(f"Mod '{key}' value should be {expected_type}, but got {type(self.data[key])}") + + +class ArmaConfig: + mods: list[Any] + data: dict[Any, Any] + file: str + DEFAULT_INDENT = 4 + + def __init__(self, configfile): + self.file = configfile + self.data = {} + self.mods = [] + self.read() + + def read(self): + with open(self.file, "r") as fp: + self.data = json.load(fp) + + if not self.data.get("game"): + self.data["game"] = {} + if not self.data["game"].get("mods"): + self.data["game"]["mods"]: [] + + for mod in self.data["game"]["mods"]: + self.mods.append(ArmaConfigMod(mod)) + + def to_string(self, indent=DEFAULT_INDENT): + return json.dumps(self.data, indent=indent) + + def append_mod(self, s): + mod = ArmaConfigMod(s) + + if mod.data["name"] in [x.data["name"] for x in self.mods]: + print(f"[Skip ] {mod.data['name']} exists", file=sys.stderr) + return + print(f"[Append] {mod.data['name']}", file=sys.stderr) + self.mods.append(mod) + + def update(self): + self.data["game"]["mods"] = sorted( + [x.data for x in self.mods], + key=lambda y: y["name"] + ) diff --git a/ardsmm/__main__.py b/ardsmm/__main__.py new file mode 100644 index 0000000..cb24c18 --- /dev/null +++ b/ardsmm/__main__.py @@ -0,0 +1,95 @@ +from argparse import ArgumentParser + +import ardsmm +import os.path +import platform +import json +import sys + +PROG_NAME = os.path.basename(os.path.dirname(__file__)) +IS_WINDOWS = platform.platform().startswith("Windows") +INPUT_CONT_MSG = "CTRL-D" +if IS_WINDOWS: + INPUT_CONT_MSG = "enter" + + +def parse_args(): + parser = ArgumentParser(prog=PROG_NAME) + parser.add_argument("-i", "--in-place", action="store_true", help="modify JSON config file in-place") + parser.add_argument("-o", "--output-file", type=str, help="write JSON output to file (default: stdout)") + parser.add_argument("-m", "--mod-file", type=str, help="read mods from file (default: stdin)") + parser.add_argument("-I", "--indent", type=int, default=4, help=f"set JSON indentation level (default: {ardsmm.ArmaConfig.DEFAULT_INDENT})") + parser.add_argument("-V", "--version", action="store_true", help="display version number and exit") + parser.add_argument("configfile", type=str, nargs="?", help="An Arma Reforger dedicated server JSON config") + return parser.parse_args() + + +def main(): + args = parse_args() + input_data = "" + + if args.version: + print(ardsmm._version.__version__) + return 0 + + if not args.configfile: + print("Missing required positional argument: configfile", file=sys.stderr) + return 1 + + if args.in_place and args.output_file: + print("-i/--in-place and -o/--output-file are mutually exclusive options", file=sys.stderr) + return 1 + + if args.mod_file: + print(f"Reading Arma Reforger mod list from {args.mod_file}", file=sys.stderr) + if os.path.exists(args.mod_file): + input_data = open(args.mod_file).read() + else: + print(f"Input file not found: {args.mod_file}", file=sys.stderr) + return 1 + else: + if sys.stdin.isatty(): + print(f"\nPaste Arma Reforger mod list and hit {INPUT_CONT_MSG} to continue...\n", + file=sys.stderr) + + if IS_WINDOWS: + while True: + line = sys.stdin.readline() + if not line.rstrip(): + break + input_data += line + else: + input_data = sys.stdin.read() + + if not input_data: + print("Warning: No mods consumed!", file=sys.stderr) + + config = ardsmm.ArmaConfig(args.configfile) + try: + data = json.loads("[" + input_data + "]") + for x in data: + config.append_mod(x) + config.update() + except json.JSONDecodeError as e: + print("Invalid JSON", file=sys.stderr) + print(f"Reason: {e}", file=sys.stderr) + return 1 + + filename = "" + if args.output_file: + filename = args.output_file + elif args.in_place: + filename = args.configfile + + if filename: + print(f"Writing to {filename}", file=sys.stderr) + with open(filename, "w+") as fp: + fp.write(config.to_string(args.indent)) + else: + print(config.to_string(args.indent)) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/config.json b/examples/config.json new file mode 100644 index 0000000..61b0275 --- /dev/null +++ b/examples/config.json @@ -0,0 +1,31 @@ +{ + "bindAddress": "127.0.0.1", + "bindPort": 2001, + "game": { + "crossPlatform": true, + "gameProperties": { + "VONDisableDirectSpeechUI": false, + "VONDisableUI": false, + "battlEye": true, + "disableThirdPerson": false, + "fastValidation": true, + "networkViewDistance": 500, + "serverMaxViewDistance": 500, + "serverMinGrassDistance": 50 + }, + "maxPlayers": 50, + "mods": [ + ], + "name": "Test test test", + "password": "", + "passwordAdmin": "", + "scenarioId": "{ECC61978EDCC2B5A}Missions/23_Campaign.conf", + "supportedPlatforms": [ + "PLATFORM_PC", + "PLATFORM_XBL" + ], + "visible": true + }, + "publicAddress": "127.0.0.1", + "publicPort": 2001 +}
\ No newline at end of file diff --git a/examples/mods.txt b/examples/mods.txt new file mode 100644 index 0000000..faa719b --- /dev/null +++ b/examples/mods.txt @@ -0,0 +1,40 @@ + { + "modId": "60C4C12DAE90727B", + "name": "ACE Medical", + "version": "1.0.3" + }, + { + "modId": "5EB744C5F42E0800", + "name": "ACE Chopping", + "version": "1.1.4" + }, + { + "modId": "5DBD560C5148E1DA", + "name": "ACE Carrying", + "version": "1.0.18" + }, + { + "modId": "5AAAC70D754245DD", + "name": "Server Admin Tools", + "version": "1.0.56" + }, + { + "modId": "606B100247F5C709", + "name": "Bacon Loadout Editor", + "version": "1.0.27" + }, + { + "modId": "60EAEA0389DB3CC2", + "name": "ACE Trenches", + "version": "1.0.0" + }, + { + "modId": "60E573C9B04CC408", + "name": "ACE Backblast", + "version": "1.0.0" + }, + { + "modId": "60C4CE4888FF4621", + "name": "ACE Core", + "version": "1.0.3" + }, diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f9a7b94 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "ardsmm" +authors = [ + {name = "Joseph Hunkeler", email = "jhunkeler@gmail.com"}, +] +description = "Arma Reforger dedicated server mod manager" +readme = "README.md" +requires-python = ">=3.10" +keywords = ["arma", "reforger", "dedicated", "server", "mods"] +license = {text = "BSD-3-Clause"} +classifiers = [ + "Programming Language :: Python :: 3", +] +dependencies = [] +dynamic = ["version"] + +[project.scripts] +ardsmm = "ardsmm.__main__:main" + +[tool.setuptools_scm] +version_file = "ardsmm/_version.py" |