Skip to content

Commit 0cb14af

Browse files
authored
Merge pull request #11 from Justin99x/master
Add _(to|from)_json() methods to all option types
2 parents dc21cdd + 9ccbac7 commit 0cb14af

File tree

4 files changed

+131
-94
lines changed

4 files changed

+131
-94
lines changed

Readme.md

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ game specific things:
1919

2020
# Changelog
2121

22+
### v1.9
23+
- Added `_(to|from)_json()` methods to all options.
24+
- Changed settings saving and loading to use above methods.
25+
2226
### v1.8
2327
- Fixed that nested and grouped options' children would not get their `.mod` attribute set.
2428

__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from .dot_sdkmod import open_in_mod_dir
99

1010
# Need to define a few things first to avoid circular imports
11-
__version_info__: tuple[int, int] = (1, 8)
11+
__version_info__: tuple[int, int] = (1, 9)
1212
__version__: str = f"{__version_info__[0]}.{__version_info__[1]}"
1313
__author__: str = "bl-sdk"
1414

options.py

+117-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from abc import ABC, abstractmethod
22
from collections.abc import Callable, Mapping, Sequence
33
from dataclasses import KW_ONLY, dataclass, field
4-
from typing import TYPE_CHECKING, Any, Literal, Self
4+
from types import EllipsisType
5+
from typing import TYPE_CHECKING, Any, Literal, Self, cast
56

67
from unrealsdk import logging
78

@@ -45,6 +46,27 @@ class BaseOption(ABC):
4546
def __init__(self) -> None:
4647
raise NotImplementedError
4748

49+
@abstractmethod
50+
def _to_json(self) -> JSON | EllipsisType:
51+
"""
52+
Turns this option into a JSON value.
53+
54+
Returns:
55+
This option's JSON representation, or Ellipsis if it should be considered to have no
56+
value.
57+
"""
58+
raise NotImplementedError
59+
60+
@abstractmethod
61+
def _from_json(self, value: JSON) -> None:
62+
"""
63+
Assigns this option's value, based on a previously retrieved JSON value.
64+
65+
Args:
66+
value: The JSON value to assign.
67+
"""
68+
raise NotImplementedError
69+
4870
def __post_init__(self) -> None:
4971
if self.display_name is None: # type: ignore
5072
self.display_name = self.identifier
@@ -81,6 +103,9 @@ class ValueOption[J: JSON](BaseOption):
81103
def __init__(self) -> None:
82104
raise NotImplementedError
83105

106+
def _to_json(self) -> J:
107+
return self.value
108+
84109
def __post_init__(self) -> None:
85110
super().__post_init__()
86111
self.default_value = self.value
@@ -147,6 +172,9 @@ class HiddenOption[J: JSON](ValueOption[J]):
147172
init=False,
148173
)
149174

175+
def _from_json(self, value: JSON) -> None:
176+
self.value = cast(J, value)
177+
150178
def save(self) -> None:
151179
"""Saves the settings of the mod this option is associated with."""
152180
if self.mod is None:
@@ -186,6 +214,17 @@ class SliderOption(ValueOption[float]):
186214
step: float = 1
187215
is_integer: bool = True
188216

217+
def _from_json(self, value: JSON) -> None:
218+
try:
219+
self.value = float(value) # type: ignore
220+
if self.is_integer:
221+
self.value = round(self.value)
222+
except ValueError:
223+
logging.error(
224+
f"'{value}' is not a valid value for option '{self.identifier}', sticking"
225+
f" with the default",
226+
)
227+
189228

190229
@dataclass
191230
class SpinnerOption(ValueOption[str]):
@@ -214,6 +253,16 @@ class SpinnerOption(ValueOption[str]):
214253
choices: list[str]
215254
wrap_enabled: bool = False
216255

256+
def _from_json(self, value: JSON) -> None:
257+
value = str(value)
258+
if value in self.choices:
259+
self.value = value
260+
else:
261+
logging.error(
262+
f"'{value}' is not a valid value for option '{self.identifier}', sticking"
263+
f" with the default",
264+
)
265+
217266

218267
@dataclass
219268
class BoolOption(ValueOption[bool]):
@@ -240,6 +289,13 @@ class BoolOption(ValueOption[bool]):
240289
true_text: str | None = None
241290
false_text: str | None = None
242291

292+
def _from_json(self, value: JSON) -> None:
293+
# Special case a false string
294+
if isinstance(value, str) and value.strip().lower() == "false":
295+
value = False
296+
297+
self.value = bool(value)
298+
243299

244300
@dataclass
245301
class DropdownOption(ValueOption[str]):
@@ -266,6 +322,16 @@ class DropdownOption(ValueOption[str]):
266322

267323
choices: list[str]
268324

325+
def _from_json(self, value: JSON) -> None:
326+
value = str(value)
327+
if value in self.choices:
328+
self.value = value
329+
else:
330+
logging.error(
331+
f"'{value}' is not a valid value for option '{self.identifier}', sticking"
332+
f" with the default",
333+
)
334+
269335

270336
@dataclass
271337
class ButtonOption(BaseOption):
@@ -291,6 +357,12 @@ class ButtonOption(BaseOption):
291357
_: KW_ONLY
292358
on_press: Callable[[Self], None] | None = None
293359

360+
def _to_json(self) -> EllipsisType:
361+
return ...
362+
363+
def _from_json(self, value: JSON) -> None:
364+
pass
365+
294366
def __call__(self, on_press: Callable[[Self], None]) -> Self:
295367
"""
296368
Sets the on press callback.
@@ -339,6 +411,12 @@ class KeybindOption(ValueOption[str | None]):
339411

340412
is_rebindable: bool = True
341413

414+
def _from_json(self, value: JSON) -> None:
415+
if value is None:
416+
self.value = None
417+
else:
418+
self.value = str(value)
419+
342420
@classmethod
343421
def from_keybind(cls, bind: KeybindType) -> Self:
344422
"""
@@ -388,6 +466,25 @@ class GroupedOption(BaseOption):
388466

389467
children: Sequence[BaseOption]
390468

469+
def _to_json(self) -> JSON:
470+
return {
471+
option.identifier: child_json
472+
for option in self.children
473+
if (child_json := option._to_json()) is not ...
474+
}
475+
476+
def _from_json(self, value: JSON) -> None:
477+
if isinstance(value, Mapping):
478+
for option in self.children:
479+
if option.identifier not in value:
480+
continue
481+
option._from_json(value[option.identifier])
482+
else:
483+
logging.error(
484+
f"'{value}' is not a valid value for option '{self.identifier}', sticking"
485+
f" with the default",
486+
)
487+
391488

392489
@dataclass
393490
class NestedOption(BaseOption):
@@ -411,3 +508,22 @@ class NestedOption(BaseOption):
411508
"""
412509

413510
children: Sequence[BaseOption]
511+
512+
def _to_json(self) -> JSON:
513+
return {
514+
option.identifier: child_json
515+
for option in self.children
516+
if (child_json := option._to_json()) is not ...
517+
}
518+
519+
def _from_json(self, value: JSON) -> None:
520+
if isinstance(value, Mapping):
521+
for option in self.children:
522+
if option.identifier not in value:
523+
continue
524+
option._from_json(value[option.identifier])
525+
else:
526+
logging.error(
527+
f"'{value}' is not a valid value for option '{self.identifier}', sticking"
528+
f" with the default",
529+
)

settings.py

+9-92
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,13 @@
22

33
import json
44
from collections.abc import Mapping, Sequence
5-
from typing import TYPE_CHECKING, TypedDict, cast
6-
7-
from unrealsdk import logging
5+
from typing import TYPE_CHECKING, TypedDict
86

97
from . import MODS_DIR
10-
from .options import (
11-
BaseOption,
12-
BoolOption,
13-
ButtonOption,
14-
DropdownOption,
15-
GroupedOption,
16-
HiddenOption,
17-
KeybindOption,
18-
NestedOption,
19-
SliderOption,
20-
SpinnerOption,
21-
ValueOption,
22-
)
238

249
if TYPE_CHECKING:
2510
from .mod import Mod
11+
from .options import BaseOption
2612

2713
type JSON = Mapping[str, JSON] | Sequence[JSON] | str | int | float | bool | None
2814

@@ -36,7 +22,7 @@ class BasicModSettings(TypedDict, total=False):
3622
keybinds: dict[str, str | None]
3723

3824

39-
def load_options_dict( # noqa: C901 - imo the match is rated too highly, but it's getting there
25+
def load_options_dict(
4026
options: Sequence[BaseOption],
4127
settings: Mapping[str, JSON],
4228
) -> None:
@@ -53,58 +39,7 @@ def load_options_dict( # noqa: C901 - imo the match is rated too highly, but it
5339

5440
value = settings[option.identifier]
5541

56-
match option:
57-
case HiddenOption():
58-
option.value = value
59-
60-
# For all other option types, try validate the type before setting it, we don't want
61-
# a "malicious" settings file to corrupt the types at runtime
62-
63-
case BoolOption():
64-
# Special case a false string
65-
if isinstance(value, str) and value.strip().lower() == "false":
66-
value = False
67-
68-
option.value = bool(value)
69-
case SliderOption():
70-
try:
71-
# Some of the JSON types won't support float conversion suppress the type
72-
# error and catch the exception instead
73-
option.value = float(value) # type: ignore
74-
if option.is_integer:
75-
option.value = round(option.value)
76-
except ValueError:
77-
logging.error(
78-
f"'{value}' is not a valid value for option '{option.identifier}', sticking"
79-
f" with the default",
80-
)
81-
case DropdownOption() | SpinnerOption():
82-
value = str(value)
83-
if value in option.choices:
84-
option.value = value
85-
else:
86-
logging.error(
87-
f"'{value}' is not a valid value for option '{option.identifier}', sticking"
88-
f" with the default",
89-
)
90-
case GroupedOption() | NestedOption():
91-
if isinstance(value, Mapping):
92-
load_options_dict(option.children, value)
93-
else:
94-
logging.error(
95-
f"'{value}' is not a valid value for option '{option.identifier}', sticking"
96-
f" with the default",
97-
)
98-
case KeybindOption():
99-
if value is None:
100-
option.value = None
101-
else:
102-
option.value = str(value)
103-
104-
case _:
105-
logging.error(
106-
f"Couldn't load settings for unknown option type {type(option).__name__}",
107-
)
42+
option._from_json(value) # type: ignore
10843

10944

11045
def default_load_mod_settings(self: Mod) -> None:
@@ -146,29 +81,11 @@ def create_options_dict(options: Sequence[BaseOption]) -> dict[str, JSON]:
14681
Returns:
14782
The options' values in dict form.
14883
"""
149-
settings: dict[str, JSON] = {}
150-
for option in options:
151-
match option:
152-
case ValueOption():
153-
# The generics mean the type of value is technically unknown here
154-
value = cast(JSON, option.value) # type: ignore
155-
settings[option.identifier] = value
156-
157-
case GroupedOption() | NestedOption():
158-
settings[option.identifier] = create_options_dict(option.children)
159-
160-
# Button option is the only standard option which is not abstract, but also not a value,
161-
# and doesn't have children.
162-
# Just no-op it so that it doesn't show an error
163-
case ButtonOption():
164-
pass
165-
166-
case _:
167-
logging.error(
168-
f"Couldn't save settings for unknown option type {type(option).__name__}",
169-
)
170-
171-
return settings
84+
return {
85+
option.identifier: child_json
86+
for option in options
87+
if (child_json := option._to_json()) is not ... # pyright: ignore[reportPrivateUsage]
88+
}
17289

17390

17491
def default_save_mod_settings(self: Mod) -> None:

0 commit comments

Comments
 (0)