diff --git a/.cruft.json b/.cruft.json index f5d9b1d..370c775 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "git@github.com:bl-sdk/common_dotfiles.git", - "commit": "d03eee713ad436d20033d0598eb88f1529c56ca8", + "commit": "3d7189e61172cb95877261df8313ae595fe6c02d", "checkout": null, "context": { "cookiecutter": { @@ -15,7 +15,8 @@ "__project_slug": "mods_base", "include_cpp": false, "include_py": true, - "_template": "git@github.com:bl-sdk/common_dotfiles.git" + "_template": "git@github.com:bl-sdk/common_dotfiles.git", + "_commit": "3d7189e61172cb95877261df8313ae595fe6c02d" } }, "directory": null diff --git a/.gitignore b/.gitignore index ab0f7a4..dcb3783 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vs .vscode +.idea __pycache__ diff --git a/Readme.md b/Readme.md index cfe5459..6150def 100644 --- a/Readme.md +++ b/Readme.md @@ -20,7 +20,15 @@ game specific things: # Changelog ### v1.9 +- Added a new `CoopSupport.HostOnly` value. + +- Specifying a custom class when calling `build_mod` now type hints returning an instance of it, + instead of just `Mod`. + +- `SliderOption`s now throw if initialized with a step larger than their allowed range. + - Added `_(to|from)_json()` methods to all options. + - Changed settings saving and loading to use above methods. ### v1.8 diff --git a/mod.py b/mod.py index bacfebe..44a0486 100644 --- a/mod.py +++ b/mod.py @@ -21,6 +21,8 @@ class Game(Flag): + """A flags enum of the supported games.""" + BL2 = auto() TPS = auto() AoDK = auto() @@ -86,15 +88,25 @@ def get_tree() -> Literal[Game.Willow2, Game.Oak]: class ModType(Enum): + """ + What type of mod this is. + + This does not influence functionality. It's only used for categorization - e.g. influencing + ordering in the mod list. + """ + Standard = auto() Library = auto() class CoopSupport(Enum): + """Enum for how well a mod supports coop. This is informational only.""" + Unknown = auto() Incompatible = auto() RequiresAllPlayers = auto() ClientSide = auto() + HostOnly = auto() @dataclass @@ -112,7 +124,7 @@ class Mod: description: A short description of the mod. version: A string holding the mod's version. This is purely a display value, the module level attributes should be used for version checking. - mod_type: What type of mod this is. This influences ordering in the mod list. + mod_type: What type of mod this is. This does not influence functionality. supported_games: The games this mod supports. When loaded in an unsupported game, a warning will be displayed and the mod will be blocked from enabling. coop_support: How well the mod supports coop, if known. This is purely a display value. diff --git a/mod_factory.py b/mod_factory.py index 002aaa7..3442c73 100644 --- a/mod_factory.py +++ b/mod_factory.py @@ -20,9 +20,9 @@ from .settings import SETTINGS_DIR -def build_mod( +def build_mod[T: Mod = Mod]( *, - cls: type[Mod] = Mod, + cls: type[T] = Mod, deregister_same_settings: bool = True, inject_version_from_pyproject: bool = True, version_info_parser: Callable[[str], tuple[int, ...]] = ( @@ -43,7 +43,7 @@ def build_mod( auto_enable: bool | None = None, on_enable: Callable[[], None] | None = None, on_disable: Callable[[], None] | None = None, -) -> Mod: +) -> T: """ Factory function to create and register a mod. @@ -73,13 +73,16 @@ def build_mod( ^1: Multiple authors are joined into a single string using commas + spaces. ^2: A string of one of the ModType enum value's name. Case sensitive. + Note this only influences ordering in the mod menu. Setting 'mod_type = "Library"' is *not* + equivalent to specifying cls=Library when calling this function. ^3: A list of strings of Game enum values' names. Case sensitive. ^4: A string of one of the CoopSupport enum value's name. Case sensitive. ^5: GroupedOption and NestedOption instances are deliberately ignored, to avoid possible issues gathering their child options twice. They must be explicitly passed via the arg. - Missing fields are not passed on to the mod constructor - e.g. by never specifying supported - games, they won't be passed on and it will use the default, all of them. + Any given fields are passed directly to the mod class constructor - and any missing ones are + not. This means not specifying a field is equivalent to the class default - for example, usually + mod type defaults to Standard, but when using 'cls=Library' it will default to Library. Extra Args: cls: The mod class to construct using. Can be used to select a subclass. diff --git a/options.py b/options.py index ce1968d..3e0eba9 100644 --- a/options.py +++ b/options.py @@ -195,7 +195,8 @@ class SliderOption(ValueOption[float]): value: The option's value. min_value: The minimum value. max_value: The maximum value. - step: How much the value should move each step of the slider. + step: How much the value should move each step of the slider. This does not mean your value + will always be a multiple of the step, it may be possible to get intermediate values. is_integer: If True, the value is treated as an integer. Keyword Args: display_name: The option name to use for display. Defaults to copying the identifier. @@ -214,6 +215,14 @@ class SliderOption(ValueOption[float]): step: float = 1 is_integer: bool = True + def __post_init__(self) -> None: + super().__post_init__() + + # This is generally non-sensical, and we expect most menus will have problems with it + # While you can easily get around it, block it as a first layer of defence + if self.step > (self.max_value - self.min_value): + raise ValueError("Can't give slider option a step larger than its allowed range") + def _from_json(self, value: JSON) -> None: try: self.value = float(value) # type: ignore diff --git a/pyproject.toml b/pyproject.toml index 4ebc61f..48036fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,16 +10,9 @@ target-version = "py313" line-length = 100 [tool.ruff.lint] -# Last time rules scrutinised: ruff 0.6.9 / 2024-10-08 +# Last time rules scrutinised: ruff 0.11.0 / 2025-03-20 select = [ - "F", - "W", - "E", - "C90", - "I", - "N", - "D", - "UP", + "ERA", "YTT", "ANN", "ASYNC", @@ -31,34 +24,51 @@ select = [ "C4", "DTZ", "T10", + "FIX", "FA", + "INT", "ISC", "ICN", "LOG", "G", "PIE", + "T20", "PYI", "Q", "RSE", "RET", - "SLOT", "SIM", + "SLOT", "TID", - "TCH", - "INT", + "TD", + "TC", "ARG", "PTH", - "TD", - "FIX", - "ERA", - "PGH", - "PL", "FLY", + "I", + "C90", + "N", "PERF", + "E", + "W", + "D", + "F", + "PGH", + "PL", + "UP", "FURB", "RUF", ] ignore = [ + "ANN401", + "S101", + "S603", + "S607", + "PYI011", + "PYI021", + "PYI029", + "PYI044", + "TC006", "D100", "D101", "D104", @@ -76,16 +86,6 @@ ignore = [ "D410", "D411", "D413", - "ANN101", - "ANN102", - "ANN401", - "S101", - "S603", - "S607", - "PYI011", - "PYI021", - "PYI029", - "PYI044", "PGH003", "PLR0904", "PLR0911", @@ -100,4 +100,4 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -"*.pyi" = ["D418", "A002", "A003"] +"*.pyi" = ["A002", "A003", "D418"]