1
1
from abc import ABC , abstractmethod
2
2
from collections .abc import Callable , Mapping , Sequence
3
3
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
5
6
6
7
from unrealsdk import logging
7
8
@@ -45,6 +46,27 @@ class BaseOption(ABC):
45
46
def __init__ (self ) -> None :
46
47
raise NotImplementedError
47
48
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
+
48
70
def __post_init__ (self ) -> None :
49
71
if self .display_name is None : # type: ignore
50
72
self .display_name = self .identifier
@@ -81,6 +103,9 @@ class ValueOption[J: JSON](BaseOption):
81
103
def __init__ (self ) -> None :
82
104
raise NotImplementedError
83
105
106
+ def _to_json (self ) -> J :
107
+ return self .value
108
+
84
109
def __post_init__ (self ) -> None :
85
110
super ().__post_init__ ()
86
111
self .default_value = self .value
@@ -147,6 +172,9 @@ class HiddenOption[J: JSON](ValueOption[J]):
147
172
init = False ,
148
173
)
149
174
175
+ def _from_json (self , value : JSON ) -> None :
176
+ self .value = cast (J , value )
177
+
150
178
def save (self ) -> None :
151
179
"""Saves the settings of the mod this option is associated with."""
152
180
if self .mod is None :
@@ -186,6 +214,17 @@ class SliderOption(ValueOption[float]):
186
214
step : float = 1
187
215
is_integer : bool = True
188
216
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
+
189
228
190
229
@dataclass
191
230
class SpinnerOption (ValueOption [str ]):
@@ -214,6 +253,16 @@ class SpinnerOption(ValueOption[str]):
214
253
choices : list [str ]
215
254
wrap_enabled : bool = False
216
255
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
+
217
266
218
267
@dataclass
219
268
class BoolOption (ValueOption [bool ]):
@@ -240,6 +289,13 @@ class BoolOption(ValueOption[bool]):
240
289
true_text : str | None = None
241
290
false_text : str | None = None
242
291
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
+
243
299
244
300
@dataclass
245
301
class DropdownOption (ValueOption [str ]):
@@ -266,6 +322,16 @@ class DropdownOption(ValueOption[str]):
266
322
267
323
choices : list [str ]
268
324
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
+
269
335
270
336
@dataclass
271
337
class ButtonOption (BaseOption ):
@@ -291,6 +357,12 @@ class ButtonOption(BaseOption):
291
357
_ : KW_ONLY
292
358
on_press : Callable [[Self ], None ] | None = None
293
359
360
+ def _to_json (self ) -> EllipsisType :
361
+ return ...
362
+
363
+ def _from_json (self , value : JSON ) -> None :
364
+ pass
365
+
294
366
def __call__ (self , on_press : Callable [[Self ], None ]) -> Self :
295
367
"""
296
368
Sets the on press callback.
@@ -339,6 +411,12 @@ class KeybindOption(ValueOption[str | None]):
339
411
340
412
is_rebindable : bool = True
341
413
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
+
342
420
@classmethod
343
421
def from_keybind (cls , bind : KeybindType ) -> Self :
344
422
"""
@@ -388,6 +466,25 @@ class GroupedOption(BaseOption):
388
466
389
467
children : Sequence [BaseOption ]
390
468
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
+
391
488
392
489
@dataclass
393
490
class NestedOption (BaseOption ):
@@ -411,3 +508,22 @@ class NestedOption(BaseOption):
411
508
"""
412
509
413
510
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
+ )
0 commit comments