Skip to content

Commit 65eb4df

Browse files
authored
Merge pull request #54 from apple1417/master
fix crash in constructor based mods, fix save options warning
2 parents ae63129 + 1b91773 commit 65eb4df

File tree

6 files changed

+48
-29
lines changed

6 files changed

+48
-29
lines changed

changelog.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## v3.6 (Upcoming)
4+
5+
### Legacy Compat v1.5
6+
- Fixed handling of _0 names, which caused crashes in most constructor-based mods.
7+
8+
### Save Options v1.1
9+
- Addressed a warning about assigning an array to itself, caused during handling save options. This
10+
had no runtime impact.
11+
312
## v3.5: Slagga
413
- Upgraded the Microsoft Visual C++ version the SDK is built with. This may cause some people to
514
crash immediately on launch, to fix this install the latest

manager_pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
[project]
77
name = "willow_mod_manager"
8-
version = "3.5"
8+
version = "3.6"
99
authors = [{ name = "bl-sdk" }]
1010

1111
[tool.sdkmod]

src/legacy_compat/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"legacy_compat",
1515
)
1616

17-
__version_info__: tuple[int, int] = (1, 4)
17+
__version_info__: tuple[int, int] = (1, 5)
1818
__version__: str = f"{__version_info__[0]}.{__version_info__[1]}"
1919
__author__: str = "bl-sdk"
2020

@@ -82,7 +82,17 @@ def add_compat_module(name: str, module: ModuleType) -> None: # pyright: ignore
8282

8383

8484
# Kill switch. May have to update this at some point if we decide to keep this around longer.
85-
if base_mod.version.partition(" ")[0] not in {"3.0", "3.1", "3.2", "3.3", "3.4", "3.5", "3.6"}:
85+
if base_mod.version.partition(" ")[0] not in {
86+
"3.0",
87+
"3.1",
88+
"3.2",
89+
"3.3",
90+
"3.4",
91+
"3.5",
92+
"3.6",
93+
"3.7",
94+
"3.8",
95+
}:
8696
from unrealsdk import logging
8797

8898
logging.warning("Legacy SDK Compatibility has been disabled")

src/legacy_compat/unrealsdk/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,8 @@ def _convert_struct_tuple_if_required(
353353
return value
354354

355355

356-
# Make sure not to allow leading zeros
357-
_RE_NAME_SUFFIX = re.compile(r"^(.+)_[1-9]\d*$")
356+
# For multi-digit numbers, make sure not to allow leading zeros
357+
_RE_NAME_SUFFIX = re.compile(r"^(.+)_(\d|[1-9]\d+)$")
358358

359359

360360
@overload

src/save_options/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"register_save_options",
2626
)
2727

28-
__version_info__: tuple[int, int] = (1, 0)
28+
__version_info__: tuple[int, int] = (1, 1)
2929
__version__: str = f"{__version_info__[0]}.{__version_info__[1]}"
3030
__author__: str = "bl-sdk"
3131

src/save_options/hooks.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
from json import JSONDecodeError
33
from typing import Any
44

5-
from unrealsdk import logging, make_struct
5+
from unrealsdk import logging
66
from unrealsdk.hooks import Type
7-
from unrealsdk.unreal import BoundFunction, UObject, WrappedStruct
7+
from unrealsdk.unreal import BoundFunction, UObject, WrappedArray, WrappedStruct
88

99
import save_options.options
1010
from mods_base import JSON, get_pc, hook
@@ -22,37 +22,28 @@
2222

2323

2424
def _extract_save_data(
25-
lockout_list: list[WrappedStruct],
26-
) -> tuple[dict[str, dict[str, JSON]], list[WrappedStruct]]:
25+
lockout_list: WrappedArray[WrappedStruct],
26+
) -> dict[str, dict[str, JSON]]:
2727
"""
2828
Extracts custom save data from an UnloadableDlcLockoutList.
2929
3030
This function searches through the list for an entry matching the global `_PACKAGE_ID`.
3131
If found, it attempts to parse the `LockoutDefName` field as a JSON string into a dictionary.
3232
Invalid or malformed JSON will result in an empty dictionary and an error being logged.
33-
Any entries not matching the `_PACKAGE_ID` are preserved and returned as the second element
34-
of the tuple to avoid overwriting unrelated data.
33+
All entries matching the `_PACKAGE_ID` are removed in place.
3534
3635
Args:
3736
lockout_list: List of LockoutData structs from the character save file.
38-
3937
Returns:
40-
A tuple with two elements:
41-
- A dictionary of extracted save data (empty if none found or invalid)
42-
- A list of LockoutData structs for any entries not associated with our custom saves.
38+
A dictionary of extracted save data (empty if not found or invalid).
4339
"""
4440

4541
matching_lockout_data = next(
4642
(lockout_data for lockout_data in lockout_list if lockout_data.DlcPackageId == _PACKAGE_ID),
4743
None,
4844
)
4945
if not matching_lockout_data:
50-
return {}, lockout_list
51-
52-
# Preserve other package Ids on the off chance someone else uses this.
53-
lockout_list_other = [
54-
lockout_data for lockout_data in lockout_list if lockout_data.DlcPackageId != _PACKAGE_ID
55-
]
46+
return {}
5647

5748
extracted_save_data: dict[str, dict[str, JSON]] = {}
5849
if matching_lockout_data.LockoutDefName:
@@ -70,7 +61,18 @@ def _extract_save_data(
7061
f" {matching_lockout_data.LockoutDefName}",
7162
)
7263
extracted_save_data = {}
73-
return extracted_save_data, lockout_list_other
64+
65+
# Remove all our entries from the list
66+
# This is done in a bit of a weird, unpythonic way, to be extra safe with regards to the
67+
# structs. Structs are reference types, so removing one shifts all other references.
68+
i = 0
69+
while i < len(lockout_list):
70+
if lockout_list[i].DlcPackageId == _PACKAGE_ID:
71+
del lockout_list[i]
72+
continue
73+
i += 1
74+
75+
return extracted_save_data
7476

7577

7678
@hook("WillowGame.WillowSaveGameManager:SaveGame", immediately_enable=True)
@@ -90,7 +92,8 @@ def save_game(_1: UObject, args: WrappedStruct, _3: Any, _4: BoundFunction) -> N
9092

9193
# For saving, we'll overwrite existing mod data for enabled mods. Any disabled/uninstalled mods
9294
# will have their data left alone.
93-
json_save_data, lockout_list = _extract_save_data(args.SaveGame.UnloadableDlcLockoutList)
95+
lockout_list = args.SaveGame.UnloadableDlcLockoutList
96+
json_save_data = _extract_save_data(lockout_list)
9497
for mod_id, mod_data in registered_save_options.items():
9598
if mod_id in enabled_mods:
9699
mod_save_data = {
@@ -108,13 +111,10 @@ def save_game(_1: UObject, args: WrappedStruct, _3: Any, _4: BoundFunction) -> N
108111
logging.dev_warning(f"Data is not json encodable: {mod_save_data}")
109112

110113
str_save_data = json.dumps(json_save_data)
111-
custom_lockout = make_struct(
112-
"UnloadableDlcLockoutData",
114+
lockout_list.emplace_struct(
113115
LockoutDefName=str_save_data,
114116
DlcPackageId=_PACKAGE_ID,
115117
)
116-
lockout_list.append(custom_lockout)
117-
args.SaveGame.UnloadableDlcLockoutList = lockout_list
118118

119119
# Reset our var tracking whether any options have changed since last save.
120120
save_options.options.any_option_changed = False
@@ -130,7 +130,7 @@ def end_load_game(_1: UObject, _2: WrappedStruct, ret: Any, _4: BoundFunction) -
130130
save_game = ret
131131
if not save_game:
132132
return
133-
extracted_save_data, _ = _extract_save_data(save_game.UnloadableDlcLockoutList)
133+
extracted_save_data = _extract_save_data(save_game.UnloadableDlcLockoutList)
134134
if not extracted_save_data:
135135
return
136136

0 commit comments

Comments
 (0)