diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs index f4a6dfe..bf70918 100644 --- a/.settings/org.eclipse.core.resources.prefs +++ b/.settings/org.eclipse.core.resources.prefs @@ -1,3 +1,10 @@ eclipse.preferences.version=1 +encoding//src/pygamecfg/__init__.py=utf-8 encoding//src/pygamecfg/__main__.py=utf-8 +encoding//src/pygamecfg/common_ut.py=utf-8 +encoding//src/pygamecfg/core_gamecfg.py=utf-8 +encoding//src/pygamecfg/game_cod4.py=utf-8 +encoding//src/pygamecfg/game_ut2k4.py=utf-8 +encoding//src/pygamecfg/game_ut99.py=utf-8 +encoding//src/pygamecfg/tool_ini.py=utf-8 encoding/=UTF-8 diff --git a/Jenkinsfile b/Jenkinsfile index 0c4181a..13ee83d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -341,7 +341,7 @@ pipeline { |'''.strip() | |import copier - |copier.run_auto("./", "../_gitrepo",vcs_ref="HEAD",use_prereleases=True,defaults=True,cleanup_on_error=False) + |copier.run_copy("./", "../_gitrepo",vcs_ref="HEAD",use_prereleases=True,defaults=True,cleanup_on_error=False) | |__EOWRAPPER__ """.stripMargin()) diff --git a/README.md b/README.md index 82c6bd2..efa5f28 100644 --- a/README.md +++ b/README.md @@ -8,46 +8,8 @@ ![](docs-static/Library.jpg) -# Python project template +# pyGameCFG -A nice template to start blank python projets. - -This template automate a lot of handy things and allow CI/CD automatic releases generation. - -It is also collectings data to feed Jenkins build. Checkout [Latest Documentation](https://chacha.ddns.net/mkdocs-web/chacha/{{repository}}/{{branch}}/latest/). -## Features - -### Generic pipeline skeleton: - - Prepare - - GetCode - - BuildPackage - - Install - - CheckCode - - PlotMetrics - - RunUnitTests - - GenDOC - - PostRelease - -### CI/CD Environment - - Jenkins - - Gitea (with patch for dynamic Readme variables: https://chacha.ddns.net/gitea/chacha/GiteaMarkupVariable) - - Docker - - MkDocsWeb - -### CI/CD Helper libs - - VirtualEnv - - Changelog generation based on commits - - copier - - pylint + pylint_json2html - - mypy - - unittest + xmlrunner + junitparser + junit2htmlreport - - mkdocs - -### Python project - - Full .toml implementation - - .whl automatic generation - - dynamic versionning using git repository - - embedded unit-test \ No newline at end of file diff --git a/docs-static/usage.md b/docs-static/usage.md index 7763bbd..8f04b05 100644 --- a/docs-static/usage.md +++ b/docs-static/usage.md @@ -1,16 +1 @@ # Usage - -## Pulvinar dolor -Donec dapibus est fermentum justo volutpat condimentum. Integer quis nunc neque. Donec dictum vehicula justo, in facilisis ex tincidunt in. -Vivamus sollicitudin sem dui, id mollis orci facilisis ut. Proin sed pulvinar dolor. Donec volutpat commodo urna imperdiet pulvinar. Fusce eget aliquam risus. -Vivamus viverra luctus ex, in finibus mi. Nullam elementum dapibus mollis. Ut suscipit volutpat ex, quis feugiat lacus consectetur eu. - -## Condimentum faucibus -Quisque auctor egestas sem, luctus suscipit ex maximus vitae. Duis facilisis augue et condimentum faucibus. -Donec cursus, enim a sagittis egestas, lectus lorem eleifend libero, at tincidunt leo magna at libero. -Nunc eros velit, suscipit luctus tempor vel, finibus et est. Curabitur efficitur pretium pulvinar. -Donec urna lectus, vulputate quis turpis sed, placerat congue urna. Phasellus aliquet fermentum quam, non auctor elit porta nec. Morbi eu ligula at nisl ultricies condimentum vitae id ante. - -## Aliquam lacinia -In volutpat lorem ex, et fringilla nibh faucibus quis. Mauris et arcu elementum, auctor dui vitae, egestas arcu. Duis sit amet aliquam quam. -Phasellus a odio turpis. Etiam tristique mi eu enim varius, eget facilisis est vestibulum. Aliquam lacinia nec purus sed luctus. Cras at laoreet erat. \ No newline at end of file diff --git a/helpers/doc_gen.py b/helpers/doc_gen.py index 232278e..7c86dd9 100644 --- a/helpers/doc_gen.py +++ b/helpers/doc_gen.py @@ -50,6 +50,7 @@ class doc_gen(helper_withresults_base): reference_path = doc_path / "reference" cls._reset_dir(reference_path) + # create one .md per python module for path in sorted((cls.project_rootdir_path / "src").rglob("*.py")): module_path = path.relative_to(cls.project_rootdir_path / "src").with_suffix("") doc_path = path.relative_to(cls.project_rootdir_path / "src").with_suffix(".md") @@ -57,14 +58,12 @@ class doc_gen(helper_withresults_base): parts = list(module_path.parts) - if parts[-1] == "__init__": - parts = parts[:-1] - elif parts[-1] == "__main__": + if parts[-1] in ("__init__", "__main__"): continue - cls._reset_dir(os.path.dirname(full_doc_path)) + cls._create_dir(full_doc_path.parent.resolve()) with open(full_doc_path, "w+") as fd: - identifier = "src." + ".".join(parts) + identifier = ".".join(parts) print("::: " + identifier, file=fd) cmdopts = [f"{sys.executable}", "-m", "mkdocs", "-v", "build", "--site-dir", str(site_path), "--clean"] @@ -93,6 +92,7 @@ class doc_gen(helper_withresults_base): with open(cls.project_rootdir_path / "mkdocs.yml", "w") as mkdocsCfgFile: mkdocsCfgFile.write(yaml.dump(mkdocsCfg, Dumper=Dumper, default_flow_style=False, sort_keys=False)) + print(" !! start doc generation") res = cls.run_cmd(cmdopts) print(res.decode()) print(" !! done") diff --git a/helpers/helper_base.py b/helpers/helper_base.py index ab88e6a..e1bf5db 100644 --- a/helpers/helper_base.py +++ b/helpers/helper_base.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING from abc import ABC, abstractmethod import os +import shutil from pathlib import Path import subprocess @@ -34,11 +35,17 @@ class helper_base(ABC): return None @staticmethod - def _reset_dir(dirpath: Path): + def _create_dir(dirpath: Path): dirpath = Path(dirpath) if not os.path.exists(dirpath): os.makedirs(dirpath) - [f.unlink() for f in Path(dirpath).glob("*") if f.is_file()] + + @staticmethod + def _reset_dir(dirpath: Path): + dirpath = Path(dirpath) + if os.path.exists(dirpath): + shutil.rmtree(dirpath) + os.makedirs(dirpath) @classmethod def reset_result_dir(cls): diff --git a/helpers/quality_check.py b/helpers/quality_check.py index 4028148..c63207b 100644 --- a/helpers/quality_check.py +++ b/helpers/quality_check.py @@ -63,7 +63,7 @@ class quality_check(helper_withresults_base): [ "--load-plugins=pylint.extensions.mccabe", "--output-format=json,parseable", - "--disable=invalid-name", + "--disable=invalid-name,too-few-public-methods,too-many-arguments", # ignore "--ignore=_version.py", "--reports=y", "--score=yes", diff --git a/mkdocs.yml b/mkdocs.yml index a76672d..a3011c8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,19 +1,11 @@ -# pyChaChaDummyProject (c) by chacha -# -# pyChaChaDummyProject is licensed under a -# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. -# -# You should have received a copy of the license along with this -# work. If not, see . - docs_dir: docs -site_name: 'pygamecfg' -site_url: 'https://chacha.ddns.net/mkdocs-web/chacha/pygamecfg/latest/' -site_description: 'A simple game config tool that provide bash API to read / write game config files (cod, ut ...)' -site_author: 'chacha' -repo_url: 'https://chacha.ddns.net/gitea/chacha/pygamecfg' +site_name: pygamecfg +site_url: https://chacha.ddns.net/mkdocs-web/chacha/pygamecfg/latest/ +site_description: A simple ini parser / factory +site_author: chacha +repo_url: https://chacha.ddns.net/gitea/chacha/pygamecfg use_directory_urls: false -copyright: 'CC BY-NC-SA 4.0' +copyright: CC BY-NC-SA 4.0 theme: name: material features: @@ -22,11 +14,11 @@ theme: - navigation.tabs - navigation.tabs.sticky - navigation.footer - - toc.integrate + - navigation.path - navigation.top - navigation.section - content.code.annotate - - navigation.prune + - navigation.expand - toc.follow palette: - media: '(prefers-color-scheme: dark)' @@ -52,19 +44,30 @@ plugins: default_handler: python handlers: python: + path: + - src options: filters: - '!^_[^_]' - inherited_members: true + inherited_members: false show_if_no_docstring: true show_signature_annotations: true show_source: false show_category_heading: true group_by_category: true - docstring_section_style: spacy show_root_full_path: false merge_init_into_class: true separate_signature: true + heading_level: 2 + docstring_section_style: spacy + show_root_toc_entry: false +- with-pdf: + cover_subtitle: User Manual + cover_logo: C:\Users\chacha\git\pygamecfg\docs-static\Library.jpg + verbose: false + exclude_pages: + - LICENSE + output_path: C:\Users\chacha\git\pygamecfg\helpers-results\doc_gen\site\pdf\manual.pdf markdown_extensions: - def_list - tables @@ -115,4 +118,4 @@ markdown_extensions: emoji_generator: !!python/name:materialx.emoji.to_svg extra: branch: master - repository: pygitversionhelper + repository: pygamecfg diff --git a/pyproject.toml b/pyproject.toml index 87d9661..d430840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,8 @@ classifiers = [ dependencies = [ 'importlib-metadata; python_version<"3.9"', 'packaging', - 'pysimpleini>=0.3.1' + 'pysimpleini>=0.3.1', + 'typed-argument-parser==1.*' ] dynamic = ["version"] @@ -48,6 +49,7 @@ where = ["src"] [tool.setuptools.package-data] "pygamecfg.data" = ["*.*"] +"pysimpleini" = ["py.typed"] [project.urls] Homepage = "https://chacha.ddns.net/gitea/chacha/pygamecfg" @@ -60,8 +62,8 @@ coverage-check = ["coverage>=7.0"] complexity-check = ["radon>=5.1"] quality-check = ["pylint>=2.15","pylint-json2html>=0.4","pandas>=1.5"] type-check = ["mypy[reports]>=0.99" ] -doc-gen = ["mkdocs>=1.4.0", "mkdocs-material>=8.5","mkdocs-pymdownx-material-extras", "mkdocs-localsearch>=0.9.0", "mkdocstrings[python]>=0.19", "mkdocs-with-pdf>=0.9.3","pyyaml>=6.0","pymdown-extensions>=9","mkdocs-markdownextradata-plugin","mkdocs-mermaid2-plugin"] +doc-gen = ["mkdocs>=1.4.0", "mkdocs-material>=8.5","mkdocs-material-extensions","mkdocs-pymdownx-material-extras", "mkdocs-localsearch>=0.9.0", "mkdocstrings[python]>=0.19", "mkdocs-with-pdf>=0.9.3","pyyaml>=6.0","pymdown-extensions>=9","mkdocs-markdownextradata-plugin","mkdocs-mermaid2-plugin","mkdocs-autorefs"] -#[project.scripts] -#my-script = "my_package.module:function" +# [project.scripts] +# my-script = "my_package.module:function" diff --git a/pyproject.toml.bak b/pyproject.toml.bak new file mode 100644 index 0000000..1fb8af4 --- /dev/null +++ b/pyproject.toml.bak @@ -0,0 +1,68 @@ +# pyChaChaDummyProject (c) by chacha +# +# pyChaChaDummyProject is licensed under a +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. +# +# You should have received a copy of the license along with this +# work. If not, see . + +[build-system] +requires = ["setuptools>=63", "wheel", "setuptools_scm"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +version_scheme= "post-release" + +[project] +name = "pygamecfg" +description = "A simple game config tool that provide bash API to read / write game config files (cod, ut ...)" +readme = "README.md" +requires-python = ">=3.9" +keywords = ["chacha","chacha","template","pygamecfg"] +license = { file = "LICENSE.md" } + +authors = [ + {name="chacha",email="1000CHACHA0001@gmail.com"}, +] +maintainers = [ + {name="chacha",email="1000CHACHA0001@gmail.com"}, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", +] +dependencies = [ + 'importlib-metadata; python_version<"3.9"', + 'packaging', + 'pysimpleini>=0.3.1', + 'typed-argument-parser==1.*' +] +dynamic = ["version"] + +[tool.setuptools] +platforms = ["any"] +include-package-data = true + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"pygamecfg.data" = ["*.*"] + +[project.urls] +Homepage = "https://chacha.ddns.net/gitea/chacha/pygamecfg" +Documentation = "https://chacha.ddns.net/mkdocs-web/chacha/pygamecfg/master/latest/" +Tracker = "https://chacha.ddns.net/gitea/chacha/pygamecfg/issues" + +[project.optional-dependencies] +test = ["junitparser>=2.8","junit2html>=30.1","xmlrunner>=1.7","mypy>=0.99" ] +coverage-check = ["coverage>=7.0"] +complexity-check = ["radon>=5.1"] +quality-check = ["pylint>=2.15","pylint-json2html>=0.4","pandas>=1.5"] +type-check = ["mypy[reports]>=0.99" ] +doc-gen = ["mkdocs>=1.4.0", "mkdocs-material>=8.5","mkdocs-material-extensions","mkdocs-pymdownx-material-extras", "mkdocs-localsearch>=0.9.0", "mkdocstrings[python]>=0.19", "mkdocs-with-pdf>=0.9.3","pyyaml>=6.0","pymdown-extensions>=9","mkdocs-markdownextradata-plugin","mkdocs-mermaid2-plugin","mkdocs-autorefs"] + +# [project.scripts] +# my-script = "my_package.module:function" + diff --git a/src/pygamecfg/__init__.py b/src/pygamecfg/__init__.py index e1661c2..2e38913 100644 --- a/src/pygamecfg/__init__.py +++ b/src/pygamecfg/__init__.py @@ -1,6 +1,9 @@ -# pygamecfg (c) by chacha +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# pyGameCFG(c) by chacha # -# pygamecfg is licensed under a +# pyGameCFG is licensed under a # Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. # # You should have received a copy of the license along with this @@ -13,7 +16,6 @@ Main module __init__ file. from importlib.metadata import distribution, version, PackageNotFoundError import warnings -from .core import GameOptions_Factory try: # pragma: no cover __version__ = version("pygamecfg") @@ -34,3 +36,9 @@ try: # pragma: no cover except PackageNotFoundError: # pragma: no cover warnings.warn('can not read dist.metadata["Name"], assuming local test context, setting it to ') __Name__ = "pygamecfg" + +from pygamecfg.core_gamecfg import GameOptions_Factory + +from . import game_cod4 +from . import game_ut99 +from . import game_ut2k4 diff --git a/src/pygamecfg/__main__.py b/src/pygamecfg/__main__.py index 60680b5..ff3c678 100644 --- a/src/pygamecfg/__main__.py +++ b/src/pygamecfg/__main__.py @@ -1,8 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# PySimpleINI (c) by chacha + +# pyGameCFG(c) by chacha # -# PySimpleINI is licensed under a +# pyGameCFG is licensed under a # Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. # # You should have received a copy of the license along with this @@ -10,52 +11,102 @@ """CLI interface module""" from __future__ import annotations +from typing import Literal, cast, Union -from argparse import ArgumentParser +import sys +from tap import Tap from . import __Summuary__, __Name__ from . import GameOptions_Factory -parser = ArgumentParser(description=__Summuary__) -parser.add_argument("-v", "--verbosity", action="count", default=0, help="increase output verbosity") -parser.add_argument("-b", "--basegamedir", help="set the game base dir", default="./") -parser.add_argument("-c", "--configfile", help="set the default config file", default="") -parser.add_argument("-g", "--game", help="the target game", choices=["ut99", "cod4"]) -subparsers = parser.add_subparsers(dest="command", help="command type", required=True) +class pygamecfg_args_SetOption(Tap): + """SetOption CLI arg subparser""" -SetOption_subparser = subparsers.add_parser("SetOption", help="Set/Add a game config file option value (may need reboot)") -SetOption_subparser.add_argument("option") -SetOption_subparser.add_argument("value", nargs="?") + option: str + value: str = "" + + def configure(self) -> None: + self.add_argument("option") + self.add_argument("value") -RemOption_subparser = subparsers.add_parser("RemOption", help="Remove a game config file option, w or w/o value (may need reboot)") -RemOption_subparser.add_argument("option") -RemOption_subparser.add_argument("value", nargs="?") +class pygamecfg_args_RemOption(Tap): + """RemOption CLI arg subparser""" -GetOption_subparser = subparsers.add_parser("GetOption", help="Get a game config file option value") -GetOption_subparser.add_argument("option") + option: str + value: str = "" -args = parser.parse_args() - -if args.verbosity: - print("Using base game dir: {0}".format(args.basegamedir)) - print("Using config file: {0}".format(args.configfile)) - -if args.configfile == "": - if args.game == "ut99": - args.configfile = "./System/UnrealTournament.ini" - elif args.game == "cod4": - args.configfile = "./main/server.cfg" -_GameOptions = GameOptions_Factory(args.game, args.basegamedir, args.configfile) + def configure(self) -> None: + self.add_argument("option") + self.add_argument("value") -if args.command == "SetOption": - _GameOptions.set(args.option, args.value) -elif args.command == "RemOption": - _GameOptions.rem(args.option, args.value) -elif args.command == "GetOption": - res = _GameOptions.get(args.option) - print(res) -else: - raise RuntimeError("Invalid argument") +class pygamecfg_args_GetOption(Tap): + """GetOption CLI arg subparser""" + + option: str + + def configure(self) -> None: + self.add_argument("option") + + +class pygamecfg_args(Tap): + """Main CLI arg parser""" + + verbosity: int = 0 + basegamedir: str = "./" + configfile: str = "" + game: Literal["ut99", "cod4", "ut2k4"] + + def configure(self) -> None: + self.add_argument("-v", "--verbosity", action="count", help="increase output verbosity") + self.add_argument("-b", "--basegamedir", help="set the game base dir") + self.add_argument("-c", "--configfile", help="set the default config file") + self.add_argument("-g", "--game", help="the target game") + self.add_subparsers(dest="command", help="command type", required=True) + self.add_subparser("SetOption", pygamecfg_args_SetOption, help="Set/Add a game config file option value") + self.add_subparser("RemOption", pygamecfg_args_RemOption, help="Remove a game config file option, w or w/o value") + self.add_subparser("GetOption", pygamecfg_args_GetOption, help="Get a game config file option value") + + def process_args(self) -> None: + """dynamically add self.command to avoid conflict with Tap/argparse while keep pylint happy""" + self.command: Union[str, None] = cast(Union[str, None], self.command) # pylint: disable=attribute-defined-outside-init + + +def fct_main(i_args: list[str]) -> None: + """CLI main function""" + parser: pygamecfg_args = pygamecfg_args(prog=__Name__, description=__Summuary__) + + args: pygamecfg_args = parser.parse_args(i_args) + + if args.verbosity: + print(f"Using base game dir: {args.basegamedir}") + print(f"Using config file: {args.configfile}") + + if args.configfile == "": + if args.game == "ut99": + args.configfile = "./System/UnrealTournament.ini" + if args.game == "ut2k4": + args.configfile = "./System/UT2004.ini" + elif args.game == "cod4": + args.configfile = "./main/server.cfg" + + GameOptions = GameOptions_Factory(args.game, args.basegamedir, args.configfile) + if args.command == "SetOption": + GameOptions.set( + cast(pygamecfg_args_SetOption, args).option, cast(pygamecfg_args_SetOption, args).value # pylint: disable=no-member + ) + elif args.command == "RemOption": + GameOptions.rem( + cast(pygamecfg_args_RemOption, args).option, cast(pygamecfg_args_RemOption, args).value # pylint: disable=no-member + ) + elif args.command == "GetOption": + res = GameOptions.get(cast(pygamecfg_args_GetOption, args).option) # pylint: disable=no-member + print(res) + else: + raise RuntimeError("Invalid argument") + + +if __name__ == "__main__": + fct_main(sys.argv[1:]) diff --git a/src/pygamecfg/common_ut.py b/src/pygamecfg/common_ut.py new file mode 100644 index 0000000..cf79dbb --- /dev/null +++ b/src/pygamecfg/common_ut.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# pyGameCFG(c) by chacha +# +# pyGameCFG is licensed under a +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. +# +# You should have received a copy of the license along with this +# work. If not, see . + +"""common UT functions""" +from __future__ import annotations +from typing import Union + +from os.path import join +from pathlib import Path + +from pysimpleini import PySimpleINI + +from .core_gamecfg import GameOption, OptionType + + +class GameOption_UT(GameOption): + """generic UT Option class""" + + szGameType = "" + TValueType = OptionType.OT_INVALID + + szOptionName = "" + szSectionName = "" + szKeyName = "" + bForceAdd: bool = False + bRemovable: bool = False + + cachedFile: Union[None, PySimpleINI] = None + cachedFilePath: Union[None, Path] = None + + Cls_PySimpleINI: type[PySimpleINI] = PySimpleINI + + @classmethod + def openFile(cls, filepath: Path) -> PySimpleINI: + """Open the file""" + if (not cls.cachedFile) or (filepath != cls.cachedFilePath): + cls.cachedFilePath = filepath + cls.cachedFile = cls.Cls_PySimpleINI(filepath) + return cls.cachedFile + + def __init__(self, GameRootDir: str, ConfigFileRelPath: str) -> None: + super().__init__(GameRootDir, ConfigFileRelPath) + self.mainConfigFilePath: Path = Path(join(GameRootDir, ConfigFileRelPath)) + self.inifile = self.openFile(self.mainConfigFilePath) + + def set(self, value: str) -> None: + if not self.szOptionName: + raise RuntimeError("szOptionName is not set") + self.format(value) + self.inifile.setaddkeyvalue(self.szSectionName, self.szKeyName, self.szFormatedValue, self.bForceAdd) + self.inifile.writefile() + + def rem(self, value: Union[None, str]) -> None: + if not self.szOptionName: + raise RuntimeError("szOptionName is not set") + if not self.bRemovable: + raise RuntimeError("this options is not removable") + self.inifile.delkey_ex(self.szSectionName, self.szKeyName, None, value) + self.inifile.writefile() + + def get(self) -> Union[str, list[str]]: + if not self.szOptionName: + raise RuntimeError("szOptionName not set") + return self.inifile.getkeyvalue(self.szSectionName, self.szKeyName) diff --git a/src/pygamecfg/core.py b/src/pygamecfg/core.py deleted file mode 100644 index 9c6d921..0000000 --- a/src/pygamecfg/core.py +++ /dev/null @@ -1,97 +0,0 @@ -from __future__ import annotations -from typing import Union - -from abc import ABCMeta, abstractmethod -from enum import Enum - - -class OptionType(Enum): - OT_INVALID = 0 - OT_STRING = 1 - OT_INTEGER = 2 - OT_BOOLEAN = 3 - OT_FLOAT = 4 - - -class GameOption(metaclass=ABCMeta): - szGameType: str = "" - szOptionName: str = "" - TValueType: OptionType = OptionType.OT_INVALID - szDefaultValue: str = "" - szHelp: str = "" - szFormatedValue: str = "" - - @abstractmethod - def __init__(self, GameRootDir: str, ConfigFileRelPath: Union[None, str] = None): - self.GameRootDir = GameRootDir - self.ConfigFileRelPath = ConfigFileRelPath - - def format(self, value: Union[int, str, float]) -> None: - if self.TValueType == OptionType.OT_STRING: - self.szFormatedValue = str(value) - elif self.TValueType == OptionType.OT_INTEGER: - self.szFormatedValue = str(int(value)) - elif self.TValueType == OptionType.OT_BOOLEAN: - try: - intval = int(value) - self.szFormatedValue = str(bool(intval)) - except: - self.szFormatedValue = str(bool(1)) if value.lower() == "true" else str(bool(0)) - elif self.TValueType == OptionType.OT_FLOAT: - self.szFormatedValue = str(float(value)) - else: - raise RuntimeError("Invalid Option TValueType") - - print("setting option <{0}> to: {1}".format(self.szOptionName, self.szFormatedValue)) - - @abstractmethod - def set(self, value: str) -> None: - raise NotImplementedError("method not implemented") - - @abstractmethod - def rem(self, value: Union[None, str]) -> None: - raise NotImplementedError("method not implemented") - - @abstractmethod - def get(self) -> Union[None, str]: - raise NotImplementedError("method not implemented") - - -class GameOptions_Factory: - ar_Options_cls = [] - - def __init__(self, szGameType: str, GameRootDir: str, ConfigFileRelPath: Union[None, str] = None) -> None: - self.szGameType = szGameType - self.ar_Options = [] - for Options_cls in self.ar_Options_cls: - if Options_cls.szGameType == szGameType: - self.ar_Options.append(Options_cls(GameRootDir, ConfigFileRelPath)) - - @classmethod - def GameOptionRegister(cls, Option: GameOption) -> None: - cls.ar_Options_cls.append(Option) - - def set(self, OptionName: str, value: str) -> None: - for _option in self.ar_Options: - if _option.szOptionName == OptionName: - _option.set(value) - return - raise RuntimeError("Option not found") - - def rem(self, OptionName: str, value: Union[None, str]) -> None: - for _option in self.ar_Options: - if _option.szOptionName == OptionName: - _option.rem(value) - return - raise RuntimeError("Option not found") - - def get(self, OptionName: str) -> Union[None, str]: - for _option in self.ar_Options: - if _option.szOptionName == OptionName: - return _option.get() - raise RuntimeError("Option not found") - - -def GameOptions_Factory_Register(cls): - GameOptions_Factory.GameOptionRegister(cls) - return cls diff --git a/src/pygamecfg/core_gamecfg.py b/src/pygamecfg/core_gamecfg.py new file mode 100644 index 0000000..d5abe22 --- /dev/null +++ b/src/pygamecfg/core_gamecfg.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# pyGameCFG(c) by chacha +# +# pyGameCFG is licensed under a +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. +# +# You should have received a copy of the license along with this +# work. If not, see . + +""" Core file of pygamecfg +contain generic management code for GameOption +""" +from __future__ import annotations +from typing import Union + +from abc import ABCMeta, abstractmethod +from enum import Enum + + +class OptionType(Enum): + """Supported option data type""" + + OT_INVALID = 0 + OT_STRING = 1 + OT_INTEGER = 2 + OT_BOOLEAN = 3 + OT_FLOAT = 4 + + +class GameOption(metaclass=ABCMeta): + """Game option base type""" + + szGameType: str = "" + szOptionName: str = "" + TValueType: OptionType = OptionType.OT_INVALID + szDefaultValue: str = "" + szHelp: str = "" + szFormatedValue: str = "" + + def __init__(self, GameRootDir: str, ConfigFileRelPath: Union[None, str] = None): + """GameOption constructor. + + ///warning + This object does not aim to be created + /// + + Args: + GameRootDir: root dir of the game + ConfigFileRelPath: path to the configfile (relative to rootdir) + """ + self.GameRootDir = GameRootDir + self.ConfigFileRelPath = ConfigFileRelPath + + def __enter__(self) -> GameOption: + """contextlib enter hook""" + return self + + def __exit__(self, exception_type, exception_value, exception_traceback) -> None: + """contextlib exit hook""" + self.close() + + def close(self) -> None: + """user-define close() function (for subclassing)""" + + def format(self, value: Union[int, str, float]) -> None: + """standard function to format options before writing it to file (overloadable)""" + + if self.TValueType == OptionType.OT_STRING: + self.szFormatedValue = str(value) + elif self.TValueType == OptionType.OT_INTEGER: + self.szFormatedValue = str(int(value)) + elif self.TValueType == OptionType.OT_BOOLEAN: + try: + intval = int(value) + self.szFormatedValue = str(bool(intval)) + except ValueError: + self.szFormatedValue = str(True) if str(value).lower() == "true" else str(False) + elif self.TValueType == OptionType.OT_FLOAT: + self.szFormatedValue = str(float(value)) + else: + raise RuntimeError("Invalid Option TValueType") + + print(f"setting option <{self.szOptionName}> to: {self.szFormatedValue}") + + @abstractmethod + def set(self, value: str) -> None: + """generic set function""" + raise NotImplementedError("method not implemented") + + @abstractmethod + def rem(self, value: Union[None, str]) -> None: + """generic rem function""" + raise NotImplementedError("method not implemented") + + @abstractmethod + def get(self) -> Union[str, list[str]]: + """generic get function""" + raise NotImplementedError("method not implemented") + + +class GameOptions_Factory: + """factory that manage game options based on Game and the option itself""" + + ar_Options_cls: list[type[GameOption]] = [] + ar_Options_cls_filtered: list[type[GameOption]] = [] + GameRootDir: str = "./" + ConfigFileRelPath: str = "" + + def __init__(self, szGameType: str, GameRootDir: str, ConfigFileRelPath: str) -> None: + self.szGameType = szGameType + self.GameRootDir = GameRootDir + self.ConfigFileRelPath = ConfigFileRelPath + for Options_cls in GameOptions_Factory.ar_Options_cls: + if Options_cls.szGameType == szGameType: + self.ar_Options_cls_filtered.append(Options_cls) + + @classmethod + def GameOptionRegister(cls, Option: type[GameOption]) -> None: + """interface option used by decorator to register option implementation classes""" + GameOptions_Factory.ar_Options_cls.append(Option) + + def set(self, OptionName: str, value: str) -> None: + """generic set function (API call)""" + for _option in GameOptions_Factory.ar_Options_cls_filtered: + if _option.szOptionName == OptionName: + with _option(self.GameRootDir, self.ConfigFileRelPath) as _optionInst: + _optionInst.set(value) + return + raise RuntimeError("Option not found") + + def rem(self, OptionName: str, value: Union[None, str]) -> None: + """generic rem function (API call)""" + for _option in self.ar_Options_cls_filtered: + if _option.szOptionName == OptionName: + with _option(self.GameRootDir, self.ConfigFileRelPath) as _optionInst: + _optionInst.rem(value) + return + raise RuntimeError("Option not found") + + def get(self, OptionName: str) -> Union[str, list[str]]: + """generic get function (API call)""" + for _option in self.ar_Options_cls_filtered: + if _option.szOptionName == OptionName: + with _option(self.GameRootDir, self.ConfigFileRelPath) as _optionInst: + return _optionInst.get() + raise RuntimeError("Option not found") + + +def GameOptions_Factory_Register(cls: type[GameOption]) -> type[GameOption]: + """decorator to register game option concrete implementation""" + GameOptions_Factory.GameOptionRegister(cls) + return cls diff --git a/src/pygamecfg/data/.keep b/src/pygamecfg/data/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/pygamecfg/data/__init__.py b/src/pygamecfg/data/__init__.py deleted file mode 100644 index dec0d77..0000000 --- a/src/pygamecfg/data/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# pygamecfg (c) by chacha -# -# pygamecfg is licensed under a -# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. -# -# You should have received a copy of the license along with this -# work. If not, see . diff --git a/src/pygamecfg/cod4.py b/src/pygamecfg/game_cod4.py similarity index 75% rename from src/pygamecfg/cod4.py rename to src/pygamecfg/game_cod4.py index 447b88c..be19ac6 100644 --- a/src/pygamecfg/cod4.py +++ b/src/pygamecfg/game_cod4.py @@ -1,10 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# pyGameCFG(c) by chacha +# +# pyGameCFG is licensed under a +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. +# +# You should have received a copy of the license along with this +# work. If not, see . + +# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring,duplicate-code from __future__ import annotations from typing import Union import re from os.path import join -from .core import GameOptions_Factory_Register, GameOption, OptionType +from .core_gamecfg import GameOptions_Factory_Register, GameOption, OptionType class GameOption_COD4(GameOption): @@ -19,7 +31,10 @@ class GameOption_COD4(GameOption): def __init__(self, GameRootDir: str, ConfigFileRelPath: str) -> None: super().__init__(GameRootDir, ConfigFileRelPath) self.mainConfigFilePath = join(GameRootDir, ConfigFileRelPath) - self.cfgfile = open(self.mainConfigFilePath, "r") + self.cfgfile = open(self.mainConfigFilePath, "r", encoding="utf8") # pylint: disable=consider-using-with + + def close(self) -> None: + self.cfgfile.close() def set(self, value: str) -> None: if not self.szOptionName: @@ -31,7 +46,7 @@ class GameOption_COD4(GameOption): value = self.szPrefix + " " + self.szKeyName + " " + value for line in self.cfgfile.readlines(): - if re.search(r"\s+" + self.szPrefix + r"\s+"): + if re.search(r"\s+" + self.szPrefix + r"\s+", line): print(f"found: {line}") def rem(self, value: Union[str, None] = None) -> None: @@ -54,11 +69,11 @@ class GameOption_COD4(GameOption): raise RuntimeError("Option not found in file") self.cfgfile.close() - with open(self.mainConfigFilePath, "w") as ofile: + with open(self.mainConfigFilePath, "w", encoding="utf8") as ofile: ofile.write(newFile) - self.cfgfile = open(self.mainConfigFilePath, "r") + self.cfgfile = open(self.mainConfigFilePath, "r", encoding="utf8") # pylint: disable=consider-using-with - def get(self) -> Union[None, str]: + def get(self) -> str: if not self.szOptionName: raise RuntimeError("szOptionName not set") @@ -67,7 +82,7 @@ class GameOption_COD4(GameOption): else: regex = r"^\s*" + self.szPrefix + r"\s+" + self.szOptionName + r"\s*(?P.*)" bfound = False - res = None + for line in self.cfgfile.readlines(): if result := re.search(regex, line): if bfound: diff --git a/src/pygamecfg/ut2k4.py b/src/pygamecfg/game_ut2k4.py similarity index 83% rename from src/pygamecfg/ut2k4.py rename to src/pygamecfg/game_ut2k4.py index 296a09f..07c2d96 100644 --- a/src/pygamecfg/ut2k4.py +++ b/src/pygamecfg/game_ut2k4.py @@ -1,75 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# pyGameCFG(c) by chacha +# +# pyGameCFG is licensed under a +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. +# +# You should have received a copy of the license along with this +# work. If not, see . + +# pylint: disable=missing-class-docstring,missing-function-docstring,duplicate-code +"""UT2k4 command set""" from __future__ import annotations from typing import Union -from pysimpleini import PySimpleINI, KeyNotFoundError, SectionNotFoundError, Section, Key -from os.path import join +from pysimpleini import KeyNotFoundError -from .core import GameOptions_Factory_Register, GameOption, OptionType +from .core_gamecfg import GameOptions_Factory_Register, OptionType +from .tool_ini import PySimpleINI_GroupKeysInSection +from .common_ut import GameOption_UT -class ChaChaSimpleINI_UT2k4(PySimpleINI): - def GroupKeysInSection(self, szSectionName: str, szKeyName: str) -> None: - try: - _section = self.getSection(szSectionName) - if type(_section) is Section: - ar_ServerPackages = _section.getKey(szKeyName) - if isinstance(ar_ServerPackages, Key): - ar_ServerPackages = [ar_ServerPackages] - else: # array - pass - for ServerPackages in ar_ServerPackages: - _section.delKey(ServerPackages.name, None, ServerPackages.value) - for ServerPackages in ar_ServerPackages: - _section.setAddKeyValue(ServerPackages.name, ServerPackages.value, True) - except SectionNotFoundError: - pass - - def writeFile(self, bBeautify: bool = False) -> None: - self.GroupKeysInSection("Engine.GameEngine", "ServerPackages") - self.GroupKeysInSection("Engine.GameEngine", "ServerActors") - self.GroupKeysInSection("Core.System", "Suppress") - self.GroupKeysInSection("Core.System", "Paths") - self.GroupKeysInSection("Editor.EditorEngine", "EditPackages") - self.GroupKeysInSection("Editor.EditorEngine", "CutdownPackages") - super().writeFile(bBeautify) +class PySimpleINI_UT2k4(PySimpleINI_GroupKeysInSection): + GroupRules = [ + ("Engine.GameEngine", "ServerPackages"), + ("Engine.GameEngine", "ServerActors"), + ("Core.System", "Suppress"), + ("Core.System", "Paths"), + ("Core.System", "Paths"), + ("Editor.EditorEngine", "EditPackages"), + ("Editor.EditorEngine", "CutdownPackages"), + ] -class GameOption_UT2k4(GameOption): +class GameOption_UT2k4(GameOption_UT): szGameType = "ut2k4" - TValueType = OptionType.OT_INVALID - - szOptionName = "" - szSectionName = "" - szKeyName = "" - bForceAdd: bool = False - bRemovable: bool = False - - def __init__(self, GameRootDir: str, ConfigFileRelPath: Union[None, str]) -> None: - super().__init__(GameRootDir, ConfigFileRelPath) - self.mainConfigFilePath = join(GameRootDir, ConfigFileRelPath) - - def set(self, value: str) -> None: - if not self.szOptionName: - raise RuntimeError("szOptionName is not set") - self.format(value) - self.inifile = ChaChaSimpleINI_UT2k4(self.mainConfigFilePath) - self.inifile.setAddKeyValue(self.szSectionName, self.szKeyName, self.szFormatedValue, self.bForceAdd) - self.inifile.writeFile() - - def rem(self, value: Union[None, str]) -> None: - if not self.szOptionName: - raise RuntimeError("szOptionName is not set") - if not self.bRemovable: - raise RuntimeError("this options is not removable") - self.inifile.delKeyEx(self.szSectionName, self.szKeyName, None, value) - self.inifile.writeFile() - - def get(self) -> Union[None, str]: - if not self.szOptionName: - raise RuntimeError("szOptionName not set") - print("get option <{0}>".format(self.szOptionName)) - res = self.inifile.getKeyValue(self.szSectionName, self.szKeyName) - return res + Cls_PySimpleINI = PySimpleINI_UT2k4 class GameOption_UT2k4_GenAdd(GameOption_UT2k4): @@ -156,10 +122,10 @@ class GameOption_UT2k4_HostName(GameOption_UT2k4): szDefaultValue = "ChaCha Test Server" szHelp = "Server's HostName" - def set(self, value: str): + def set(self, value: str) -> None: super().set(value) - self.inifile.setAddKeyValue("Engine.GameReplicationInfo", "ShortName", value) - self.inifile.writeFile() + self.inifile.setaddkeyvalue("Engine.GameReplicationInfo", "ShortName", value) + self.inifile.writefile() @GameOptions_Factory_Register @@ -201,12 +167,12 @@ class GameOption_UT2k4_HTTPDownloadServer(GameOption_UT2k4): szDefaultValue = "http://chacha.ddns.net/games/ut2k4" szHelp = "FastDL url" - def set(self, value: str): + def set(self, value: str) -> None: super().set(value) - self.inifile.setAddKeyValue("IpDrv.HTTPDownload", "UseCompression", "True") - self.inifile.delKey("IpDrv.HTTPDownload", "ProxyServerHost") - self.inifile.delKey("IpDrv.HTTPDownload", "ProxyServerPort") - self.inifile.writeFile() + self.inifile.setaddkeyvalue("IpDrv.HTTPDownload", "UseCompression", "True") + self.inifile.delkey("IpDrv.HTTPDownload", "ProxyServerHost") + self.inifile.delkey("IpDrv.HTTPDownload", "ProxyServerPort") + self.inifile.writefile() @GameOptions_Factory_Register @@ -238,10 +204,10 @@ class GameOption_UT2k4_NetServerMaxTickRate(GameOption_UT2k4): szDefaultValue = "60" szHelp = "Server Max TickRate" - def set(self, value: str): + def set(self, value: str) -> None: super().set(value) - self.inifile.setAddKeyValue("Engine.DemoRecDrive", "NetServerMaxTickRate", value) - self.inifile.writeFile() + self.inifile.setaddkeyvalue("Engine.DemoRecDrive", "NetServerMaxTickRate", value) + self.inifile.writefile() @GameOptions_Factory_Register @@ -253,10 +219,10 @@ class GameOption_UT2k4_LanServerMaxTickRate(GameOption_UT2k4): szDefaultValue = "60" szHelp = "Lan Server Max TickRate" - def set(self, value: str): + def set(self, value: str) -> None: super().set(value) - self.inifile.setAddKeyValue("Engine.DemoRecDrive", "LanServerMaxTickRate", value) - self.inifile.writeFile() + self.inifile.setaddkeyvalue("Engine.DemoRecDrive", "LanServerMaxTickRate", value) + self.inifile.writefile() @GameOptions_Factory_Register @@ -731,7 +697,7 @@ class GameOption_UT2k4_WebServer(GameOption_UT2k4): def set(self, value: str) -> None: super().set(value) if int(value) > 0: - self.inifile.setAddKeyValue("UWeb.WebServer", "bEnabled", "True") + self.inifile.setaddkeyvalue("UWeb.WebServer", "bEnabled", "True") else: - self.inifile.setAddKeyValue("UWeb.WebServer", "bEnabled", "False") - self.inifile.writeFile() + self.inifile.setaddkeyvalue("UWeb.WebServer", "bEnabled", "False") + self.inifile.writefile() diff --git a/src/pygamecfg/ut99.py b/src/pygamecfg/game_ut99.py similarity index 77% rename from src/pygamecfg/ut99.py rename to src/pygamecfg/game_ut99.py index 5ebab08..781ed72 100644 --- a/src/pygamecfg/ut99.py +++ b/src/pygamecfg/game_ut99.py @@ -1,75 +1,40 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# pyGameCFG(c) by chacha +# +# pyGameCFG is licensed under a +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. +# +# You should have received a copy of the license along with this +# work. If not, see . + +# pylint: disable=missing-class-docstring,missing-function-docstring,duplicate-code +"""UT99 command set""" from __future__ import annotations from typing import Union -from pysimpleini import PySimpleINI, KeyNotFoundError, SectionNotFoundError, Section, Key -from os.path import join +from pysimpleini import KeyNotFoundError, SectionNotFoundError -from .core import GameOptions_Factory_Register, GameOption, OptionType +from .core_gamecfg import GameOptions_Factory_Register, OptionType +from .tool_ini import PySimpleINI_GroupKeysInSection +from .common_ut import GameOption_UT -class PySimpleINI_UT99(PySimpleINI): - def GroupKeysInSection(self, szSectionName: str, szKeyName: str) -> None: - try: - _section = self.getSection(szSectionName) - if type(_section) is Section: - ar_ServerPackages = _section.getKey(szKeyName) - if isinstance(ar_ServerPackages, Key): - ar_ServerPackages = [ar_ServerPackages] - else: # array - pass - for ServerPackages in ar_ServerPackages: - _section.delKey(ServerPackages.name, None, ServerPackages.value) - for ServerPackages in ar_ServerPackages: - _section.setAddKeyValue(ServerPackages.name, ServerPackages.value, True) - except SectionNotFoundError: - pass - - def writeFile(self, bBeautify: bool = False) -> None: - self.GroupKeysInSection("XC_Engine.XC_GameEngine", "ServerPackages") - self.GroupKeysInSection("XC_Engine.XC_GameEngine", "ServerActors") - self.GroupKeysInSection("Engine.GameEngine", "ServerPackages") - self.GroupKeysInSection("Engine.GameEngine", "ServerActors") - self.GroupKeysInSection("Core.System", "Suppress") - self.GroupKeysInSection("Editor.EditorEngine", "EditPackages") - super().writeFile(bBeautify) +class PySimpleINI_UT99(PySimpleINI_GroupKeysInSection): + GroupRules = [ + ("XC_Engine.XC_GameEngine", "ServerPackages"), + ("XC_Engine.XC_GameEngine", "ServerActors"), + ("Engine.GameEngine", "ServerPackages"), + ("Engine.GameEngine", "ServerActors"), + ("Core.System", "Suppress"), + ("Editor.EditorEngine", "EditPackages"), + ] -class GameOption_UT99(GameOption): +class GameOption_UT99(GameOption_UT): szGameType = "ut99" - TValueType = OptionType.OT_INVALID - - szOptionName = "" - szSectionName = "" - szKeyName = "" - bForceAdd: bool = False - bRemovable: bool = False - - def __init__(self, GameRootDir: str, ConfigFileRelPath: str) -> None: - super().__init__(GameRootDir, ConfigFileRelPath) - self.mainConfigFilePath = join(GameRootDir, ConfigFileRelPath) - self.inifile = PySimpleINI_UT99(self.mainConfigFilePath) - - def set(self, value: str) -> None: - if not self.szOptionName: - raise RuntimeError("szOptionName is not set") - self.format(value) - self.inifile.setAddKeyValue(self.szSectionName, self.szKeyName, self.szFormatedValue, self.bForceAdd) - self.inifile.writeFile() - - def rem(self, value: Union[None, str]) -> None: - if not self.szOptionName: - raise RuntimeError("szOptionName is not set") - if not self.bRemovable: - raise RuntimeError("this options is not removable") - self.inifile.delKeyEx(self.szSectionName, self.szKeyName, None, value) - self.inifile.writeFile() - - def get(self) -> Union[None, str]: - if not self.szOptionName: - raise RuntimeError("szOptionName not set") - # print("get option <{0}>".format(self.szOptionName)) - res = self.inifile.getKeyValue(self.szSectionName, self.szKeyName) - return res + Cls_PySimpleINI = PySimpleINI_UT99 class GameOption_UT99_GenAdd(GameOption_UT99): @@ -96,7 +61,7 @@ class GameOption_UT99_GenAdd__Engine(GameOption_UT99_GenAdd): prev = self.szSectionName try: - self.inifile.getSection("XC_Engine.XC_GameEngine") + self.inifile.getsection("XC_Engine.XC_GameEngine") self.szSectionName = "XC_Engine.XC_GameEngine" super().set(value) except SectionNotFoundError: @@ -110,7 +75,7 @@ class GameOption_UT99_GenAdd__Engine(GameOption_UT99_GenAdd): prev = self.szSectionName try: - self.inifile.getSection("XC_Engine.XC_GameEngine") + self.inifile.getsection("XC_Engine.XC_GameEngine") self.szSectionName = "XC_Engine.XC_GameEngine" super().rem(value) except SectionNotFoundError: @@ -175,8 +140,8 @@ class GameOption_UT99_HostName(GameOption_UT99): def set(self, value: str) -> None: super().set(value) - self.inifile.setAddKeyValue("Engine.GameReplicationInfo", "ShortName", value) - self.inifile.writeFile() + self.inifile.setaddkeyvalue("Engine.GameReplicationInfo", "ShortName", value) + self.inifile.writefile() @GameOptions_Factory_Register @@ -240,8 +205,8 @@ class GameOption_UT99_AdminName(GameOption_UT99): def set(self, value: str) -> None: super().set(value) - self.inifile.setAddKeyValue("UTServerAdmin.UTServerAdmin", "AdminUsername", value) - self.inifile.writeFile() + self.inifile.setaddkeyvalue("UTServerAdmin.UTServerAdmin", "AdminUsername", value) + self.inifile.writefile() @GameOptions_Factory_Register @@ -255,10 +220,10 @@ class GameOption_UT99_HTTPDownloadServer(GameOption_UT99): def set(self, value: str) -> None: super().set(value) - self.inifile.setAddKeyValue("IpDrv.HTTPDownload", "UseCompression", "True") - self.inifile.delKey("IpDrv.HTTPDownload", "ProxyServerHost") - self.inifile.delKey("IpDrv.HTTPDownload", "ProxyServerPort") - self.inifile.writeFile() + self.inifile.setaddkeyvalue("IpDrv.HTTPDownload", "UseCompression", "True") + self.inifile.delkey("IpDrv.HTTPDownload", "ProxyServerHost") + self.inifile.delkey("IpDrv.HTTPDownload", "ProxyServerPort") + self.inifile.writefile() @GameOptions_Factory_Register @@ -292,8 +257,8 @@ class GameOption_UT99_NetServerMaxTickRate(GameOption_UT99): def set(self, value: str) -> None: super().set(value) - self.inifile.setAddKeyValue("Engine.DemoRecDriver", "NetServerMaxTickRate", value) - self.inifile.writeFile() + self.inifile.setaddkeyvalue("Engine.DemoRecDriver", "NetServerMaxTickRate", value) + self.inifile.writefile() @GameOptions_Factory_Register @@ -307,8 +272,8 @@ class GameOption_UT99_LanServerMaxTickRate(GameOption_UT99): def set(self, value: str) -> None: super().set(value) - self.inifile.setAddKeyValue("Engine.DemoRecDriver", "LanServerMaxTickRate", value) - self.inifile.writeFile() + self.inifile.setaddkeyvalue("Engine.DemoRecDriver", "LanServerMaxTickRate", value) + self.inifile.writefile() @GameOptions_Factory_Register @@ -322,8 +287,19 @@ class GameOption_UT99_AdminPassword(GameOption_UT99): def set(self, value: str) -> None: super().set(value) - self.inifile.setAddKeyValue("UTServerAdmin.UTServerAdmin", "AdminPassword", value) - self.inifile.writeFile() + self.inifile.setaddkeyvalue("UTServerAdmin.UTServerAdmin", "AdminPassword", value) + self.inifile.writefile() + + def get(self) -> Union[str, list[str]]: + try: + return super().get() + except KeyNotFoundError: + return self.inifile.getkeyvalue( + "UTServerAdmin.UTServerAdmin", + "AdminPassword", + ) + + raise NotImplementedError("method not implemented") @GameOptions_Factory_Register @@ -448,15 +424,15 @@ class GameOption_UT99_WebServer(GameOption_UT99): def set(self, value: str) -> None: # fix ut99 v469c try: - self.inifile.delKey("UWeb.WebServer", "Listenport") - except: + self.inifile.delkey("UWeb.WebServer", "Listenport") + except KeyNotFoundError: pass super().set(value) if int(value) > 0: - self.inifile.setAddKeyValue("UWeb.WebServer", "bEnabled", "True") + self.inifile.setaddkeyvalue("UWeb.WebServer", "bEnabled", "True") else: - self.inifile.setAddKeyValue("UWeb.WebServer", "bEnabled", "False") - self.inifile.writeFile() + self.inifile.setaddkeyvalue("UWeb.WebServer", "bEnabled", "False") + self.inifile.writefile() @GameOptions_Factory_Register diff --git a/src/pygamecfg/py.typed b/src/pygamecfg/py.typed new file mode 100644 index 0000000..807db6d --- /dev/null +++ b/src/pygamecfg/py.typed @@ -0,0 +1 @@ +# PlaceHolder \ No newline at end of file diff --git a/src/pygamecfg/tool_ini.py b/src/pygamecfg/tool_ini.py new file mode 100644 index 0000000..5526da5 --- /dev/null +++ b/src/pygamecfg/tool_ini.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# pyGameCFG(c) by chacha +# +# pyGameCFG is licensed under a +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. +# +# You should have received a copy of the license along with this +# work. If not, see . + +"""utility module that contain PySimpleINI based helpers""" +from __future__ import annotations +from typing import Union +from pysimpleini import PySimpleINI, SectionNotFoundError, Key, Section + + +class PySimpleINI_GroupKeysInSection(PySimpleINI): + """a class base on PySimpleINI that allow user to force some key to be group together in a section""" + + GroupRules: list[tuple[str, str]] = [] + + def groupkeysinsection(self, szSectionName: str, szKeyName: str) -> None: + """internal function that actually group keys""" + try: + _section: list[Section] = self.getsection(szSectionName) + if len(_section) == 1: + ar_ServerPackages: Union[list[Key], Key] = _section[0].getkey(szKeyName) + if isinstance(ar_ServerPackages, Key): + ar_ServerPackages = [ar_ServerPackages] + else: # array + pass + for ServerPackages in ar_ServerPackages: + _section[0].delkey(ServerPackages.getname(), None, ServerPackages.getvalue()) + for ServerPackages in ar_ServerPackages: + _section[0].setaddkeyvalue(ServerPackages.getname(), ServerPackages.getvalue(), True) + except SectionNotFoundError: + pass + + def writefile(self, bBeautify: bool = False, bWipeComments: bool = False) -> None: + """overload of the write function to call the group function before""" + for GroupRule in self.GroupRules: + self.groupkeysinsection(GroupRule[0], GroupRule[1]) + super().writefile(bBeautify, bWipeComments) diff --git a/test/data/UT99/System/UT99.ini b/test/data/UT99/System/UT99.ini new file mode 100644 index 0000000..9a4f86c --- /dev/null +++ b/test/data/UT99/System/UT99.ini @@ -0,0 +1,520 @@ +[URL] +Protocol=unreal +ProtocolDescription=Unreal Protocol +Name=Player +Map=Index.unr +LocalMap=DM-Codex.unr +Host= +Portal= +MapExt=unr +SaveExt=usa +Port=7777 +Class=Botpack.TMale1 + +[FirstRun] +FirstRun=0 + +[PackageRemap] +UnrealShare=UnrealI + +[Engine.Engine] +GameRenderDevice=SoftDrv.SoftwareRenderDevice +AudioDevice=Galaxy.GalaxyAudioSubsystem +NetworkDevice=IpDrv.TcpNetDriver +DemoRecordingDevice=Engine.DemoRecDriver +Console=UTMenu.UTConsole +Language=int +GameEngine=Engine.GameEngine +EditorEngine=Editor.EditorEngine +WindowedRenderDevice=SoftDrv.SoftwareRenderDevice +RenderDevice=GlideDrv.GlideRenderDevice +DefaultGame=Botpack.DeathMatchPlus +DefaultServerGame=Botpack.DeathMatchPlus +ViewportManager=WinDrv.WindowsClient +Render=Render.Render +Input=Engine.Input +Canvas=Engine.Canvas + +[Core.System] +PurgeCacheDays=30 +SavePath=../Save +CachePath=../Cache +CacheExt=.uxx +Paths=../System/*.u +Paths=../Maps/*.unr +Paths=../Textures/*.utx +Paths=../Sounds/*.uax +Paths=../Music/*.umx +Suppress=DevLoad +Suppress=DevSave +Suppress=DevNetTraffic +Suppress=DevGarbage +Suppress=DevKill +Suppress=DevReplace +Suppress=DevSound +Suppress=DevCompile +Suppress=DevBind +Suppress=DevBsp + +[Engine.GameEngine] +CacheSizeMegs=4 +UseSound=True +ServerActors=IpDrv.UdpBeacon +ServerActors=IpServer.UdpServerQuery +ServerActors=IpServer.UdpServerUplink MasterServerAddress=unreal.epicgames.com MasterServerPort=27900 +ServerActors=IpServer.UdpServerUplink MasterServerAddress=master0.gamespy.com MasterServerPort=27900 +ServerActors=IpServer.UdpServerUplink MasterServerAddress=master.mplayer.com MasterServerPort=27900 +ServerActors=UWeb.WebServer +ServerPackages=SoldierSkins +ServerPackages=CommandoSkins +ServerPackages=FCommandoSkins +ServerPackages=SGirlSkins +ServerPackages=BossSkins +ServerPackages=Botpack + +[WinDrv.WindowsClient] +WindowedViewportX=640 +WindowedViewportY=480 +WindowedColorBits=16 +FullscreenViewportX=640 +FullscreenViewportY=480 +FullscreenColorBits=16 +Brightness=0.500000 +MipFactor=1.000000 +UseDirectDraw=True +UseJoystick=False +CaptureMouse=True +StartupFullscreen=True +CurvedSurfaces=False +LowDetailTextures=False +ScreenFlashes=True +NoLighting=False +SlowVideoBuffering=True +DeadZoneXYZ=True +DeadZoneRUV=False +InvertVertical=False +ScaleXYZ=1000.0 +ScaleRUV=2000.0 +MinDesiredFrameRate=30.0 +Decals=True +NoDynamicLights=False + +[XDrv.XClient] +WindowedViewportX=640 +WindowedViewportY=480 +WindowedColorBits=16 +FullscreenViewportX=640 +FullscreenViewportY=480 +FullscreenColorBits=16 +Brightness=0.500000 +MipFactor=1.000000 +SlowVideoBuffering=False +StartupFullscreen=True +CurvedSurfaces=False +CaptureMouse=True +LowDetailTextures=False +ScreenFlashes=True +NoLighting=False +DeadZoneXYZ=True +DeadZoneRUV=False +InvertVertical=False +ScaleXYZ=1000.0 +ScaleRUV=2000.0 +MinDesiredFrameRate=30.0 + +[Engine.Player] +ConfiguredInternetSpeed=2600 +ConfiguredLanSpeed=20000 + +[Audio.GenericAudioSubsystem] +UseFilter=True +UseSurround=False +UseStereo=True +UseCDMusic=False +UseDigitalMusic=False +UseSpatial=False +UseReverb=False +Use3dHardware=False +LowSoundQuality=False +ReverseStereo=False +Latency=40 +OutputRate=22050Hz +Channels=16 +MusicVolume=160 +SoundVolume=200 +AmbientFactor=0.7 + +[Galaxy.GalaxyAudioSubsystem] +UseDirectSound=True +UseFilter=True +UseSurround=False +UseStereo=True +UseCDMusic=False +UseDigitalMusic=True +UseSpatial=False +UseReverb=True +Use3dHardware=False +LowSoundQuality=False +ReverseStereo=False +Latency=40 +OutputRate=22050Hz +EffectsChannels=16 +DopplerSpeed=9000.000000 +MusicVolume=160 +SoundVolume=200 +AmbientFactor=0.700000 + +[IpDrv.TcpNetDriver] +AllowDownloads=True +ConnectionTimeout=15.0 +InitialConnectTimeout=120.0 +AckTimeout=1.0 +KeepAliveTime=0.2 +MaxClientRate=20000 +MaxDownloadSize=0 +SimLatency=0 +RelevantTimeout=5.0 +SpawnPrioritySeconds=1.0 +ServerTravelPause=4.0 +NetServerMaxTickRate=20 +LanServerMaxTickRate=35 +DownloadManagers=IpDrv.HTTPDownload +DownloadManagers=Engine.ChannelDownload + +[IpDrv.HTTPDownload] +RedirectToURL=http://uz.ut-files.com/ +ProxyServerHost= +ProxyServerPort=3128 +UseCompression=True + +[Engine.DemoRecDriver] +DemoSpectatorClass=Botpack.CHSpectator +MaxClientRate=25000 +ConnectionTimeout=15.0 +InitialConnectTimeout=500.0 +AckTimeout=1.0 +KeepAliveTime=1.0 +SimLatency=0 +RelevantTimeout=5.0 +SpawnPrioritySeconds=1.0 +ServerTravelPause=4.0 +NetServerMaxTickRate=60 +LanServerMaxTickRate=60 + +[Engine.GameReplicationInfo] +ServerName=Alt Serv Name FULL +ShortName=Alt Serv Name SHORT +AdminName=TestAdminName +AdminEmail=TestAdminName@test.com +Region=0 +MOTDLine1=TestMOTDLine1 +MOTDLine2=TestMOTDLine2 +MOTDLine3=TestMOTDLine3 +MOTDLine4=TestMOTDLine4 + +[IpDrv.TcpipConnection] +SimPacketLoss=0 +SimLatency=0 + +[IpServer.UdpServerQuery] +DoUplink=True +UpdateMinutes=1 +MasterServerAddress=unreal.epicgames.com +MasterServerPort=27900 +Region=0 + +[IpDrv.UdpBeacon] +DoBeacon=True +BeaconTime=0.50 +BeaconTimeout=5.0 +BeaconProduct=ut + +[SoftDrv.SoftwareRenderDevice] +Translucency=True +VolumetricLighting=True +ShinySurfaces=False +Coronas=False +HighDetailActors=False +HighResTextureSmooth=True +LowResTextureSmooth=False +FastTranslucency=True + +[GlideDrv.GlideRenderDevice] +Translucency=True +VolumetricLighting=True +ShinySurfaces=True +Coronas=True +HighDetailActors=True +DetailBias=-1.500000 +RefreshRate=60Hz +DetailTextures=True +FastUglyRefresh=False +ScreenSmoothing=True +Resolution=Default + +[MetalDrv.MetalRenderDevice] +Translucency=True +VolumetricLighting=True +ShinySurfaces=True +Coronas=True +HighDetailActors=True +DetailTextures=False + +[OpenGLDrv.OpenGLRenderDevice] +Translucency=True +VolumetricLighting=False +ShinySurfaces=True +Coronas=True +HighDetailActors=True +DetailTextures=True + +[D3DDrv.D3DRenderDevice] +Translucency=True +VolumetricLighting=False +ShinySurfaces=True +Coronas=True +HighDetailActors=True +UseMipmapping=True +UseTrilinear=False +UseMultitexture=True +UsePageFlipping=True +UsePalettes=True +UseFullscreen=True +UseGammaCorrection=True +DetailTextures=False +Use3dfx=False +UseTripleBuffering=True +UsePrecache=True +Use32BitTextures=False + +[SglDrv.SglRenderDevice] +Translucency=True +VolumetricLighting=False +ShinySurfaces=False +Coronas=True +HighDetailActors=False +ColorDepth=16 +DetailTextures=False +FastUglyRefresh=False +TextureDetailBias=Near +VertexLighting=False + +[Editor.EditorEngine] +UseSound=True +CacheSizeMegs=6 +GridEnabled=True +SnapVertices=True +SnapDistance=10.000000 +GridSize=(X=16.000000,Y=16.000000,Z=16.000000) +RotGridEnabled=True +RotGridSize=(Pitch=1024,Yaw=1024,Roll=1024) +GameCommandLine=-log +FovAngleDegrees=90.000000 +GodMode=True +AutoSave=False +AutoSaveTimeMinutes=5 +AutoSaveIndex=6 +C_WorldBox=(R=0,G=0,B=107,A=0) +C_GroundPlane=(R=0,G=0,B=63,A=0) +C_GroundHighlight=(R=0,G=0,B=127,A=0) +C_BrushWire=(R=255,G=63,B=63,A=0) +C_Pivot=(R=0,G=255,B=0,A=0) +C_Select=(R=0,G=0,B=127,A=0) +C_AddWire=(R=127,G=127,B=255,A=0) +C_SubtractWire=(R=255,G=192,B=63,A=0) +C_GreyWire=(R=163,G=163,B=163,A=0) +C_Invalid=(R=163,G=163,B=163,A=0) +C_ActorWire=(R=127,G=63,B=0,A=0) +C_ActorHiWire=(R=255,G=127,B=0,A=0) +C_White=(R=255,G=255,B=255,A=0) +C_SemiSolidWire=(R=127,G=255,B=0,A=0) +C_NonSolidWire=(R=63,G=192,B=32,A=0) +C_WireGridAxis=(R=119,G=119,B=119,A=0) +C_ActorArrow=(R=163,G=0,B=0,A=0) +C_ScaleBox=(R=151,G=67,B=11,A=0) +C_ScaleBoxHi=(R=223,G=149,B=157,A=0) +C_Mover=(R=255,G=0,B=255,A=0) +C_OrthoBackground=(R=163,G=163,B=163,A=0) +C_Current=(R=0,G=0,B=0,A=0) +C_BrushVertex=(R=0,G=0,B=0,A=0) +C_BrushSnap=(R=0,G=0,B=0,A=0) +C_Black=(R=0,G=0,B=0,A=0) +C_Mask=(R=0,G=0,B=0,A=0) +C_WireBackground=(R=0,G=0,B=0,A=0) +C_ZoneWire=(R=0,G=0,B=0,A=0) +EditPackages=Core +EditPackages=Engine +EditPackages=Editor +EditPackages=UWindow +EditPackages=Fire +EditPackages=IpDrv +EditPackages=UWeb +EditPackages=UBrowser +EditPackages=UnrealShare +EditPackages=UnrealI +EditPackages=UMenu +EditPackages=IpServer +EditPackages=Botpack +EditPackages=UTServerAdmin +EditPackages=UTMenu +EditPackages=UTBrowser + +[UMenu.UnrealConsole] +RootWindow=UMenu.UMenuRootWindow +UWindowKey=IK_Esc +ShowDesktop=True + +[UMenu.UMenuMenuBar] +ShowHelp=True +GameUMenuDefault="UTMenu.UTGameMenu" +MultiplayerUMenuDefault="UTMenu.UTMultiplayerMenu" +OptionsUMenuDefault="UTMenu.UTOptionsMenu" + +[Botpack.ChallengeBotInfo] +Difficulty=1 + +[Botpack.DeathMatchPlus] +bNoviceMode=True +bHardCoreMode=True +bUseTranslocator=False +bCoopWeaponMode=False +bForceRespawn=True +TimeLimit=323 +FragLimit=321 +InitialBots=7 +MinPlayers=11 +bTournament=False + +[Botpack.CTFGame] +bUseTranslocator=True +bCoopWeaponMode=True +GoalTeamScore=3 +bForceRespawn=False +TimeLimit=223 + +[Botpack.Domination] +bDumbDown=True +bUseTranslocator=True +bCoopWeaponMode=True +GoalTeamScore=100 +bForceRespawn=False +TimeLimit=423 + +[Botpack.Assault] +bUseTranslocator=False +bCoopWeaponMode=True +bForceRespawn=True +TimeLimit=123 + +[Botpack.TeamGamePlus] +bBalanceTeams=True +GoalTeamScore=30 +bPlayersBalanceTeams=True +MaxTeamSize=4 + +[Engine.GameInfo] +bLowGore=False +bVeryLowGore=False +MaxSpectators=1 +MaxPlayers=4 +ServerLogName=server.log +bWorldLog=True +bBatchLocal=False +DemoBuild=0 +DemoHasTuts=0 +PlayerViewDelay=1.000000 +PlayerSpeechDelay=0.300000 +PlayerTauntDelay=2.000000 +bLogAdminActions=False +LoginDelaySeconds=0.000000 +MaxLoginAttempts=0 +ActionToTake=DO_Nothing +IPPolicies[0]=ACCEPT,* +IPPolicies[1]= +GamePassword=TestPwd + +[UnrealShare.UnrealGameOptionsMenu] +bCanModifyGore=True + +[UBrowser.UBrowserMainClientWindow] +LANTabName=UBrowserLAN +ServerListNames[0]=UBrowserUT +ServerListNames[1]=UBrowserLAN +ServerListNames[2]=UBrowserPopulated +ServerListNames[3]=UBrowserDeathmatch +ServerListNames[4]=UBrowserTeamGames +ServerListNames[5]=UBrowserCTF +ServerListNames[6]=UBrowserDOM +ServerListNames[7]=UBrowserAS +ServerListNames[8]=UBrowserLMS +ServerListNames[9]=UBrowserAll +ServerListNames[10]=None +ServerListNames[11]=None +ServerListNames[12]=None +ServerListNames[13]=None +ServerListNames[14]=None +ServerListNames[15]=None +ServerListNames[16]=None +ServerListNames[17]=None +ServerListNames[18]=None +ServerListNames[19]=None + +[UBrowserUT] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,bCompatibleServersOnly=True + +[UBrowserLAN] +ListFactories[0]=UBrowser.UBrowserLocalFact,BeaconProduct=ut +URLAppend=?LAN +AutoRefreshTime=10 +bNoAutoSort=True + +[UBrowserPopulated] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,MinPlayers=1,bCompatibleServersOnly=True + +[UBrowserDeathmatch] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=DeathMatchPlus,bCompatibleServersOnly=True + +[UBrowserTeamGames] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=TeamGamePlus,bCompatibleServersOnly=True + +[UBrowserCTF] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=CTFGame,bCompatibleServersOnly=True + +[UBrowserDOM] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=Domination,bCompatibleServersOnly=True + +[UBrowserAS] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=Assault,bCompatibleServersOnly=True + +[UBrowserLMS] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=LastManStanding,bCompatibleServersOnly=True + +[UBrowserAll] +ListFactories[0]=UBrowser.UBrowserGSpyFact,MasterServerAddress=unreal.epicgames.com,MasterServerTCPPort=28900,Region=0,GameName=ut +ListFactories[1]=UBrowser.UBrowserGSpyFact,MasterServerAddress=master0.gamespy.com,MasterServerTCPPort=28900,Region=0,GameName=ut +bHidden=True +bFallbackFactories=True + +[UTMenu.UTMultiplayerMenu] +OnlineServices[0]=LOCALIZE,MPlayer +OnlineServices[1]=LOCALIZE,Heat +OnlineServices[2]=LOCALIZE,WON + +[UWeb.WebServer] +Applications[0]=UTServerAdmin.UTServerAdmin +ApplicationPaths[0]=/ServerAdmin +Applications[1]=UTServerAdmin.UTImageServer +ApplicationPaths[1]=/images +DefaultApplication=0 +bEnabled=True +ListenPort=9999 +MaxConnections=30 + +[UBrowser.UBrowserHTTPClient] +ProxyServerAddress= +ProxyServerPort= + +[UTServerAdmin.UTServerAdmin] +AdminUsername=TestAdminUser +AdminPassword=TestAdminPwd + diff --git a/test/data/UT99/System/UnrealTournament.ini b/test/data/UT99/System/UnrealTournament.ini new file mode 100644 index 0000000..6b121f7 --- /dev/null +++ b/test/data/UT99/System/UnrealTournament.ini @@ -0,0 +1,520 @@ +[URL] +Protocol=unreal +ProtocolDescription=Unreal Protocol +Name=Player +Map=Index.unr +LocalMap=DM-Codex.unr +Host= +Portal= +MapExt=unr +SaveExt=usa +Port=7777 +Class=Botpack.TMale1 + +[FirstRun] +FirstRun=0 + +[PackageRemap] +UnrealShare=UnrealI + +[Engine.Engine] +GameRenderDevice=SoftDrv.SoftwareRenderDevice +AudioDevice=Galaxy.GalaxyAudioSubsystem +NetworkDevice=IpDrv.TcpNetDriver +DemoRecordingDevice=Engine.DemoRecDriver +Console=UTMenu.UTConsole +Language=int +GameEngine=Engine.GameEngine +EditorEngine=Editor.EditorEngine +WindowedRenderDevice=SoftDrv.SoftwareRenderDevice +RenderDevice=GlideDrv.GlideRenderDevice +DefaultGame=Botpack.DeathMatchPlus +DefaultServerGame=Botpack.DeathMatchPlus +ViewportManager=WinDrv.WindowsClient +Render=Render.Render +Input=Engine.Input +Canvas=Engine.Canvas + +[Core.System] +PurgeCacheDays=30 +SavePath=../Save +CachePath=../Cache +CacheExt=.uxx +Paths=../System/*.u +Paths=../Maps/*.unr +Paths=../Textures/*.utx +Paths=../Sounds/*.uax +Paths=../Music/*.umx +Suppress=DevLoad +Suppress=DevSave +Suppress=DevNetTraffic +Suppress=DevGarbage +Suppress=DevKill +Suppress=DevReplace +Suppress=DevSound +Suppress=DevCompile +Suppress=DevBind +Suppress=DevBsp + +[Engine.GameEngine] +CacheSizeMegs=4 +UseSound=True +ServerActors=IpDrv.UdpBeacon +ServerActors=IpServer.UdpServerQuery +ServerActors=IpServer.UdpServerUplink MasterServerAddress=unreal.epicgames.com MasterServerPort=27900 +ServerActors=IpServer.UdpServerUplink MasterServerAddress=master0.gamespy.com MasterServerPort=27900 +ServerActors=IpServer.UdpServerUplink MasterServerAddress=master.mplayer.com MasterServerPort=27900 +ServerActors=UWeb.WebServer +ServerPackages=SoldierSkins +ServerPackages=CommandoSkins +ServerPackages=FCommandoSkins +ServerPackages=SGirlSkins +ServerPackages=BossSkins +ServerPackages=Botpack + +[WinDrv.WindowsClient] +WindowedViewportX=640 +WindowedViewportY=480 +WindowedColorBits=16 +FullscreenViewportX=640 +FullscreenViewportY=480 +FullscreenColorBits=16 +Brightness=0.500000 +MipFactor=1.000000 +UseDirectDraw=True +UseJoystick=False +CaptureMouse=True +StartupFullscreen=True +CurvedSurfaces=False +LowDetailTextures=False +ScreenFlashes=True +NoLighting=False +SlowVideoBuffering=True +DeadZoneXYZ=True +DeadZoneRUV=False +InvertVertical=False +ScaleXYZ=1000.0 +ScaleRUV=2000.0 +MinDesiredFrameRate=30.0 +Decals=True +NoDynamicLights=False + +[XDrv.XClient] +WindowedViewportX=640 +WindowedViewportY=480 +WindowedColorBits=16 +FullscreenViewportX=640 +FullscreenViewportY=480 +FullscreenColorBits=16 +Brightness=0.500000 +MipFactor=1.000000 +SlowVideoBuffering=False +StartupFullscreen=True +CurvedSurfaces=False +CaptureMouse=True +LowDetailTextures=False +ScreenFlashes=True +NoLighting=False +DeadZoneXYZ=True +DeadZoneRUV=False +InvertVertical=False +ScaleXYZ=1000.0 +ScaleRUV=2000.0 +MinDesiredFrameRate=30.0 + +[Engine.Player] +ConfiguredInternetSpeed=2600 +ConfiguredLanSpeed=20000 + +[Audio.GenericAudioSubsystem] +UseFilter=True +UseSurround=False +UseStereo=True +UseCDMusic=False +UseDigitalMusic=False +UseSpatial=False +UseReverb=False +Use3dHardware=False +LowSoundQuality=False +ReverseStereo=False +Latency=40 +OutputRate=22050Hz +Channels=16 +MusicVolume=160 +SoundVolume=200 +AmbientFactor=0.7 + +[Galaxy.GalaxyAudioSubsystem] +UseDirectSound=True +UseFilter=True +UseSurround=False +UseStereo=True +UseCDMusic=False +UseDigitalMusic=True +UseSpatial=False +UseReverb=True +Use3dHardware=False +LowSoundQuality=False +ReverseStereo=False +Latency=40 +OutputRate=22050Hz +EffectsChannels=16 +DopplerSpeed=9000.000000 +MusicVolume=160 +SoundVolume=200 +AmbientFactor=0.700000 + +[IpDrv.TcpNetDriver] +AllowDownloads=True +ConnectionTimeout=15.0 +InitialConnectTimeout=120.0 +AckTimeout=1.0 +KeepAliveTime=0.2 +MaxClientRate=20000 +MaxDownloadSize=0 +SimLatency=0 +RelevantTimeout=5.0 +SpawnPrioritySeconds=1.0 +ServerTravelPause=4.0 +NetServerMaxTickRate=20 +LanServerMaxTickRate=35 +DownloadManagers=IpDrv.HTTPDownload +DownloadManagers=Engine.ChannelDownload + +[IpDrv.HTTPDownload] +RedirectToURL=http://uz.ut-files.com/ +ProxyServerHost= +ProxyServerPort=3128 +UseCompression=True + +[Engine.DemoRecDriver] +DemoSpectatorClass=Botpack.CHSpectator +MaxClientRate=25000 +ConnectionTimeout=15.0 +InitialConnectTimeout=500.0 +AckTimeout=1.0 +KeepAliveTime=1.0 +SimLatency=0 +RelevantTimeout=5.0 +SpawnPrioritySeconds=1.0 +ServerTravelPause=4.0 +NetServerMaxTickRate=60 +LanServerMaxTickRate=60 + +[Engine.GameReplicationInfo] +ServerName=Test Server Name FULL +ShortName=Test Server Name SHORT +AdminName=TestAdminName +AdminEmail=TestAdminName@test.com +Region=0 +MOTDLine1=TestMOTDLine1 +MOTDLine2=TestMOTDLine2 +MOTDLine3=TestMOTDLine3 +MOTDLine4=TestMOTDLine4 + +[IpDrv.TcpipConnection] +SimPacketLoss=0 +SimLatency=0 + +[IpServer.UdpServerQuery] +DoUplink=True +UpdateMinutes=1 +MasterServerAddress=unreal.epicgames.com +MasterServerPort=27900 +Region=0 + +[IpDrv.UdpBeacon] +DoBeacon=True +BeaconTime=0.50 +BeaconTimeout=5.0 +BeaconProduct=ut + +[SoftDrv.SoftwareRenderDevice] +Translucency=True +VolumetricLighting=True +ShinySurfaces=False +Coronas=False +HighDetailActors=False +HighResTextureSmooth=True +LowResTextureSmooth=False +FastTranslucency=True + +[GlideDrv.GlideRenderDevice] +Translucency=True +VolumetricLighting=True +ShinySurfaces=True +Coronas=True +HighDetailActors=True +DetailBias=-1.500000 +RefreshRate=60Hz +DetailTextures=True +FastUglyRefresh=False +ScreenSmoothing=True +Resolution=Default + +[MetalDrv.MetalRenderDevice] +Translucency=True +VolumetricLighting=True +ShinySurfaces=True +Coronas=True +HighDetailActors=True +DetailTextures=False + +[OpenGLDrv.OpenGLRenderDevice] +Translucency=True +VolumetricLighting=False +ShinySurfaces=True +Coronas=True +HighDetailActors=True +DetailTextures=True + +[D3DDrv.D3DRenderDevice] +Translucency=True +VolumetricLighting=False +ShinySurfaces=True +Coronas=True +HighDetailActors=True +UseMipmapping=True +UseTrilinear=False +UseMultitexture=True +UsePageFlipping=True +UsePalettes=True +UseFullscreen=True +UseGammaCorrection=True +DetailTextures=False +Use3dfx=False +UseTripleBuffering=True +UsePrecache=True +Use32BitTextures=False + +[SglDrv.SglRenderDevice] +Translucency=True +VolumetricLighting=False +ShinySurfaces=False +Coronas=True +HighDetailActors=False +ColorDepth=16 +DetailTextures=False +FastUglyRefresh=False +TextureDetailBias=Near +VertexLighting=False + +[Editor.EditorEngine] +UseSound=True +CacheSizeMegs=6 +GridEnabled=True +SnapVertices=True +SnapDistance=10.000000 +GridSize=(X=16.000000,Y=16.000000,Z=16.000000) +RotGridEnabled=True +RotGridSize=(Pitch=1024,Yaw=1024,Roll=1024) +GameCommandLine=-log +FovAngleDegrees=90.000000 +GodMode=True +AutoSave=False +AutoSaveTimeMinutes=5 +AutoSaveIndex=6 +C_WorldBox=(R=0,G=0,B=107,A=0) +C_GroundPlane=(R=0,G=0,B=63,A=0) +C_GroundHighlight=(R=0,G=0,B=127,A=0) +C_BrushWire=(R=255,G=63,B=63,A=0) +C_Pivot=(R=0,G=255,B=0,A=0) +C_Select=(R=0,G=0,B=127,A=0) +C_AddWire=(R=127,G=127,B=255,A=0) +C_SubtractWire=(R=255,G=192,B=63,A=0) +C_GreyWire=(R=163,G=163,B=163,A=0) +C_Invalid=(R=163,G=163,B=163,A=0) +C_ActorWire=(R=127,G=63,B=0,A=0) +C_ActorHiWire=(R=255,G=127,B=0,A=0) +C_White=(R=255,G=255,B=255,A=0) +C_SemiSolidWire=(R=127,G=255,B=0,A=0) +C_NonSolidWire=(R=63,G=192,B=32,A=0) +C_WireGridAxis=(R=119,G=119,B=119,A=0) +C_ActorArrow=(R=163,G=0,B=0,A=0) +C_ScaleBox=(R=151,G=67,B=11,A=0) +C_ScaleBoxHi=(R=223,G=149,B=157,A=0) +C_Mover=(R=255,G=0,B=255,A=0) +C_OrthoBackground=(R=163,G=163,B=163,A=0) +C_Current=(R=0,G=0,B=0,A=0) +C_BrushVertex=(R=0,G=0,B=0,A=0) +C_BrushSnap=(R=0,G=0,B=0,A=0) +C_Black=(R=0,G=0,B=0,A=0) +C_Mask=(R=0,G=0,B=0,A=0) +C_WireBackground=(R=0,G=0,B=0,A=0) +C_ZoneWire=(R=0,G=0,B=0,A=0) +EditPackages=Core +EditPackages=Engine +EditPackages=Editor +EditPackages=UWindow +EditPackages=Fire +EditPackages=IpDrv +EditPackages=UWeb +EditPackages=UBrowser +EditPackages=UnrealShare +EditPackages=UnrealI +EditPackages=UMenu +EditPackages=IpServer +EditPackages=Botpack +EditPackages=UTServerAdmin +EditPackages=UTMenu +EditPackages=UTBrowser + +[UMenu.UnrealConsole] +RootWindow=UMenu.UMenuRootWindow +UWindowKey=IK_Esc +ShowDesktop=True + +[UMenu.UMenuMenuBar] +ShowHelp=True +GameUMenuDefault="UTMenu.UTGameMenu" +MultiplayerUMenuDefault="UTMenu.UTMultiplayerMenu" +OptionsUMenuDefault="UTMenu.UTOptionsMenu" + +[Botpack.ChallengeBotInfo] +Difficulty=1 + +[Botpack.DeathMatchPlus] +bNoviceMode=True +bHardCoreMode=True +bUseTranslocator=False +bCoopWeaponMode=False +bForceRespawn=True +TimeLimit=323 +FragLimit=321 +InitialBots=7 +MinPlayers=11 +bTournament=False + +[Botpack.CTFGame] +bUseTranslocator=True +bCoopWeaponMode=True +GoalTeamScore=3 +bForceRespawn=False +TimeLimit=223 + +[Botpack.Domination] +bDumbDown=True +bUseTranslocator=True +bCoopWeaponMode=True +GoalTeamScore=100 +bForceRespawn=False +TimeLimit=423 + +[Botpack.Assault] +bUseTranslocator=False +bCoopWeaponMode=True +bForceRespawn=True +TimeLimit=123 + +[Botpack.TeamGamePlus] +bBalanceTeams=True +GoalTeamScore=30 +bPlayersBalanceTeams=True +MaxTeamSize=4 + +[Engine.GameInfo] +bLowGore=False +bVeryLowGore=False +MaxSpectators=1 +MaxPlayers=4 +ServerLogName=server.log +bWorldLog=True +bBatchLocal=False +DemoBuild=0 +DemoHasTuts=0 +PlayerViewDelay=1.000000 +PlayerSpeechDelay=0.300000 +PlayerTauntDelay=2.000000 +bLogAdminActions=False +LoginDelaySeconds=0.000000 +MaxLoginAttempts=0 +ActionToTake=DO_Nothing +IPPolicies[0]=ACCEPT,* +IPPolicies[1]= +GamePassword=TestPwd + +[UnrealShare.UnrealGameOptionsMenu] +bCanModifyGore=True + +[UBrowser.UBrowserMainClientWindow] +LANTabName=UBrowserLAN +ServerListNames[0]=UBrowserUT +ServerListNames[1]=UBrowserLAN +ServerListNames[2]=UBrowserPopulated +ServerListNames[3]=UBrowserDeathmatch +ServerListNames[4]=UBrowserTeamGames +ServerListNames[5]=UBrowserCTF +ServerListNames[6]=UBrowserDOM +ServerListNames[7]=UBrowserAS +ServerListNames[8]=UBrowserLMS +ServerListNames[9]=UBrowserAll +ServerListNames[10]=None +ServerListNames[11]=None +ServerListNames[12]=None +ServerListNames[13]=None +ServerListNames[14]=None +ServerListNames[15]=None +ServerListNames[16]=None +ServerListNames[17]=None +ServerListNames[18]=None +ServerListNames[19]=None + +[UBrowserUT] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,bCompatibleServersOnly=True + +[UBrowserLAN] +ListFactories[0]=UBrowser.UBrowserLocalFact,BeaconProduct=ut +URLAppend=?LAN +AutoRefreshTime=10 +bNoAutoSort=True + +[UBrowserPopulated] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,MinPlayers=1,bCompatibleServersOnly=True + +[UBrowserDeathmatch] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=DeathMatchPlus,bCompatibleServersOnly=True + +[UBrowserTeamGames] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=TeamGamePlus,bCompatibleServersOnly=True + +[UBrowserCTF] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=CTFGame,bCompatibleServersOnly=True + +[UBrowserDOM] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=Domination,bCompatibleServersOnly=True + +[UBrowserAS] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=Assault,bCompatibleServersOnly=True + +[UBrowserLMS] +ListFactories[0]=UBrowser.UBrowserSubsetFact,SupersetTag=UBrowserAll,GameType=LastManStanding,bCompatibleServersOnly=True + +[UBrowserAll] +ListFactories[0]=UBrowser.UBrowserGSpyFact,MasterServerAddress=unreal.epicgames.com,MasterServerTCPPort=28900,Region=0,GameName=ut +ListFactories[1]=UBrowser.UBrowserGSpyFact,MasterServerAddress=master0.gamespy.com,MasterServerTCPPort=28900,Region=0,GameName=ut +bHidden=True +bFallbackFactories=True + +[UTMenu.UTMultiplayerMenu] +OnlineServices[0]=LOCALIZE,MPlayer +OnlineServices[1]=LOCALIZE,Heat +OnlineServices[2]=LOCALIZE,WON + +[UWeb.WebServer] +Applications[0]=UTServerAdmin.UTServerAdmin +ApplicationPaths[0]=/ServerAdmin +Applications[1]=UTServerAdmin.UTImageServer +ApplicationPaths[1]=/images +DefaultApplication=0 +bEnabled=True +ListenPort=9999 +MaxConnections=30 + +[UBrowser.UBrowserHTTPClient] +ProxyServerAddress= +ProxyServerPort= + +[UTServerAdmin.UTServerAdmin] +AdminUsername=TestAdminUser +AdminPassword=TestAdminPwd + diff --git a/test/test_gen.py b/test/test_gen.py new file mode 100644 index 0000000..faad99f --- /dev/null +++ b/test/test_gen.py @@ -0,0 +1,30 @@ +# pygamecfg (c) by chacha +# +# pygamecfg is licensed under a +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. +# +# You should have received a copy of the license along with this +# work. If not, see . + +import unittest +from io import StringIO +from contextlib import redirect_stdout, redirect_stderr + +from src import pygamecfg +from src.pygamecfg.__main__ import fct_main + + +class Testtest_gen(unittest.TestCase): + def test_version(self): + self.assertNotEqual(pygamecfg.__version__, "?.?.?") + + def test_normal_help(self): + with self.assertRaises(SystemExit) as cm: + fct_main(["-h"]) + + def test_defect_nogame(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + with self.assertRaises(SystemExit) as cm: + fct_main(["GetOption", "test"]) + self.assertIn("pygamecfg: error: the following arguments are required: -g/--game", capted_stderr.getvalue()) + self.assertIn("", capted_stdout.getvalue()) diff --git a/test/test_ut99.py b/test/test_ut99.py index 6c00d88..b650efe 100644 --- a/test/test_ut99.py +++ b/test/test_ut99.py @@ -7,26 +7,303 @@ # work. If not, see . import unittest -from io import StringIO -from contextlib import redirect_stdout,redirect_stderr - -print(__name__) -print(__package__) +from os import linesep +from io import StringIO +from contextlib import redirect_stdout, redirect_stderr from src import pygamecfg +from src.pygamecfg.__main__ import fct_main -class Testtest_module(unittest.TestCase): - def test_version(self): - self.assertNotEqual(pygamecfg.__version__,"?.?.?") - def test_test_module(self): - +class Testtest_ut99(unittest.TestCase): + def test_normal_ServerPackages(self): with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: - self.assertEqual(pygamecfg.test_function(41),42) - - self.assertEqual(len(capted_stderr.getvalue()),0) - self.assertEqual(capted_stdout.getvalue().strip(),"Hello world !") - self.assertEqual(len(capted_stderr.getvalue()),0) + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "ServerPackages"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual( + "['SoldierSkins', 'CommandoSkins', 'FCommandoSkins', 'SGirlSkins', 'BossSkins', 'Botpack']\n", capted_stdout.getvalue() + ) + self.assertEqual("", capted_stderr.getvalue()) + def test_normal_ServerActors(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "ServerActors"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual( + "['IpDrv.UdpBeacon', 'IpServer.UdpServerQuery', 'IpServer.UdpServerUplink MasterServerAddress=unreal.epicgames.com MasterServerPort=27900', 'IpServer.UdpServerUplink MasterServerAddress=master0.gamespy.com MasterServerPort=27900', 'IpServer.UdpServerUplink MasterServerAddress=master.mplayer.com MasterServerPort=27900', 'UWeb.WebServer']\n", + capted_stdout.getvalue(), + ) + self.assertEqual("", capted_stderr.getvalue()) - \ No newline at end of file + def test_normal_Port(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "Port"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("7777\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_Map(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "Map"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("Index.unr\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_GameType(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "GameType"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("Botpack.DeathMatchPlus\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_HostName(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "HostName"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("Test Server Name FULL\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_MOTD(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "MOTD"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("TestMOTDLine1\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_MOTD2(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "MOTD2"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("TestMOTDLine2\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_MOTD3(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "MOTD3"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("TestMOTDLine3\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_MOTD4(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "MOTD4"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("TestMOTDLine4\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_AdminEmail(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "AdminEmail"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("TestAdminName@test.com\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_AdminName(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "AdminName"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("TestAdminName\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_HTTPDownloadServer(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "HTTPDownloadServer"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("http://uz.ut-files.com/\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_MaxClientRate(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "MaxClientRate"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("20000\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_NetServerMaxTickRate(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "NetServerMaxTickRate"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("20\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_LanServerMaxTickRate(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "LanServerMaxTickRate"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("35\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_AdminPassword(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "AdminPassword"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("TestAdminPwd\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_GamePassword(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "GamePassword"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("TestPwd\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_MaxPlayers(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "MaxPlayers"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("4\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_MaxSpectators(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "MaxSpectators"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("1\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_AS_TimeLimit(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "AS_TimeLimit"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("123\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_DOM_TimeLimit(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "DOM_TimeLimit"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("423\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_CTF_TimeLimit(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "CTF_TimeLimit"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("223\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_DM_TimeLimit(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "DM_TimeLimit"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("323\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_GoalTeamScore(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "GoalTeamScore"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("30\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_MaxTeamSize(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "MaxTeamSize"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("4\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_DM_FragLimit(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "DM_FragLimit"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("321\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_ServerLogName(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "ServerLogName"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("server.log\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_WebServer(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "WebServer"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("9999\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_TournamentMode(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "TournamentMode"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("False\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_InitialBots(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "InitialBots"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("7\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_MinPlayers(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "MinPlayers"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("11\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_AS_UseTranslocator(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "AS_UseTranslocator"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("True\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_CTF_UseTranslocator(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "CTF_UseTranslocator"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("True\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_DM_UseTranslocator(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "DM_UseTranslocator"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("False\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_DOM_UseTranslocator(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "DOM_UseTranslocator"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("True\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_CTF_ForceRespawn(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "CTF_ForceRespawn"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("False\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_DM_ForceRespawn(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "DM_ForceRespawn"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("True\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_DOM_ForceRespawn(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "DOM_ForceRespawn"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("False\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_AS_ForceRespawn(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "GetOption", "AS_ForceRespawn"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("True\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue()) + + def test_normal_customcfgfile_HostName(self): + with redirect_stdout(StringIO()) as capted_stdout, redirect_stderr(StringIO()) as capted_stderr: + fct_main(["-g", "ut99", "-b", "test/data/UT99", "-c", "System/UT99.ini", "GetOption", "HostName"]) + # /!\ add '\n' at the end of the string cause Python terminal newline is always this, regardless Windows / Linux os.linesep + self.assertEqual("Alt Serv Name FULL\n", capted_stdout.getvalue()) + self.assertEqual("", capted_stderr.getvalue())