aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--LICENSE.txt28
-rw-r--r--README.md60
-rw-r--r--ardsmm/__init__.py72
-rw-r--r--ardsmm/__main__.py95
-rw-r--r--examples/config.json31
-rw-r--r--examples/mods.txt40
-rw-r--r--pyproject.toml25
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"