Skip to content

Commit 638fef6

Browse files
authored
Merge pull request #38 from apple1417/master
merge mods base + console mod menu with willow + switch to submodules, upgrade to python 3.13
2 parents 4048c8d + f3e1f10 commit 638fef6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+149
-3873
lines changed

.cruft.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"template": "[email protected]:bl-sdk/common_dotfiles.git",
3-
"commit": "6b31480199099e9957b18918373a75d979951919",
3+
"commit": "d03eee713ad436d20033d0598eb88f1529c56ca8",
44
"checkout": null,
55
"context": {
66
"cookiecutter": {

.gitmodules

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
[submodule "pyunrealsdk"]
1+
[submodule "libs/pyunrealsdk"]
22
path = libs/pyunrealsdk
3-
url = [email protected]:bl-sdk/pyunrealsdk.git
3+
url = ../../bl-sdk/pyunrealsdk.git
44
[submodule "libs/pluginloader"]
55
path = libs/pluginloader
6-
url = [email protected]:bl-sdk/pluginloader.git
6+
url = ../../bl-sdk/pluginloader.git
7+
[submodule "src/mods_base"]
8+
path = src/mods_base
9+
url = ../../bl-sdk/mods_base.git
10+
[submodule "src/console_mod_menu"]
11+
path = src/console_mod_menu
12+
url = ../../bl-sdk/console_mod_menu.git

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ project(oak_mod_manager)
55
set(UNREALSDK_ARCH x64)
66
set(UNREALSDK_UE_VERSION UE4)
77
set(EXPLICIT_PYTHON_ARCH amd64)
8-
set(EXPLICIT_PYTHON_VERSION 3.12.3)
8+
set(EXPLICIT_PYTHON_VERSION 3.13.0)
99

1010
add_subdirectory(libs/pyunrealsdk)
1111

prepare_release.py

+16-16
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import re
33
import shutil
44
import subprocess
5-
import textwrap
65
import tomllib
76
from collections.abc import Iterator, Sequence
87
from functools import cache
@@ -24,6 +23,12 @@
2423

2524
LICENSE = THIS_FOLDER / "LICENSE"
2625

26+
MODS_WITH_EXISTING_LICENSE = {
27+
# These have their own due to being submodules
28+
BASE_MOD,
29+
CONSOLE_MENU,
30+
}
31+
2732
BUILD_DIR_BASE = THIS_FOLDER / "out" / "build"
2833
INSTALL_DIR_BASE = THIS_FOLDER / "out" / "install"
2934

@@ -33,6 +38,11 @@
3338
PYPROJECT_FILE = THIS_FOLDER / "manager_pyproject.toml"
3439

3540

41+
# Primarily to skip over all the dotfiles in mods which are submodules
42+
VALID_MOD_FILE_SUFFIXES = {".py", ".pyi", ".pyd", ".md"}
43+
44+
45+
# Regex to extract presets from a `cmake --list-presets` command
3646
LIST_PRESETS_RE = re.compile(' "(.+)"')
3747

3848

@@ -106,7 +116,7 @@ def iter_mod_files(mod_folder: Path, debug: bool) -> Iterator[Path]:
106116
if file.parent.name == "__pycache__":
107117
continue
108118

109-
if file.suffix == ".cpp":
119+
if file.suffix not in VALID_MOD_FILE_SUFFIXES:
110120
continue
111121
if file.suffix == ".pyd" and file.stem.endswith("_d") != debug:
112122
continue
@@ -151,7 +161,8 @@ def _zip_mod_folders(zip_file: ZipFile, mod_folders: Sequence[Path], debug: bool
151161
)
152162

153163
# Add the license
154-
zip_file.write(LICENSE, ZIP_MODS_FOLDER / mod.name / LICENSE.name)
164+
license_file = mod / "LICENSE" if mod in MODS_WITH_EXISTING_LICENSE else LICENSE
165+
zip_file.write(license_file, ZIP_MODS_FOLDER / mod.name / LICENSE.name)
155166
else:
156167
# Otherwise, we can add it as a .sdkmod
157168
buffer = BytesIO()
@@ -163,7 +174,8 @@ def _zip_mod_folders(zip_file: ZipFile, mod_folders: Sequence[Path], debug: bool
163174
)
164175

165176
# Add the license
166-
sdkmod_zip.write(LICENSE, Path(mod.name) / LICENSE.name)
177+
license_file = mod / "LICENSE" if mod in MODS_WITH_EXISTING_LICENSE else LICENSE
178+
sdkmod_zip.write(license_file, Path(mod.name) / LICENSE.name)
167179

168180
buffer.seek(0)
169181
zip_file.writestr(
@@ -212,18 +224,6 @@ def _zip_dlls(zip_file: ZipFile, install_dir: Path) -> None:
212224

213225
zip_file.write(file, dest)
214226

215-
py_stem = next(install_dir.glob("python*.zip")).stem
216-
zip_file.writestr(
217-
str(ZIP_PLUGINS_FOLDER / (py_stem + "._pth")),
218-
textwrap.dedent(
219-
f"""
220-
{path.relpath(ZIP_MODS_FOLDER, ZIP_PLUGINS_FOLDER)}
221-
{py_stem}.zip
222-
DLLs
223-
""",
224-
)[1:-1],
225-
)
226-
227227

228228
def zip_release(
229229
output: Path,

pyproject.toml

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
[tool.pyright]
2-
pythonVersion = "3.12"
2+
pythonVersion = "3.13"
33
typeCheckingMode = "strict"
4+
pythonPlatform = "Windows"
45

56
include = ["src"]
67
stubPath = "libs/pyunrealsdk/stubs"
78
reportMissingModuleSource = false
89

910
[tool.ruff]
10-
target-version = "py312"
11+
target-version = "py313"
1112
line-length = 100
1213

1314
[tool.ruff.lint]
15+
# Last time rules scrutinised: ruff 0.6.9 / 2024-10-08
1416
select = [
1517
"F",
1618
"W",

src/bl3_mod_menu/dialog_box.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def setup_callback(struct: WrappedStruct) -> None:
146146
_dialog_stack.append(self)
147147
show_dialog_box(ENGINE.GameInstance, setup_callback)
148148

149-
@hook("/Script/OakGame.OakGameInstance:OnNATHelpChoiceMade", Type.PRE, auto_enable=True)
149+
@hook("/Script/OakGame.OakGameInstance:OnNATHelpChoiceMade", Type.PRE, immediately_enable=True)
150150
@staticmethod
151151
def _on_dialog_closed_hook(
152152
_1: UObject,

src/bl3_mod_menu/native/dialog_box.cpp

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
using namespace unrealsdk::unreal;
1515
using namespace unrealsdk::memory;
1616

17+
namespace {
18+
1719
bool injecting_next_call = false;
1820
pyunrealsdk::StaticPyObject configure_callback{};
1921

@@ -67,7 +69,7 @@ UGbxGFxDialogBox* show_dialog_hook(UGbxPlayerController* player_controller,
6769
// To avoid this, swap it out with an empty array for this call
6870
auto arr = info_struct.get<UArrayProperty>(choices_prop);
6971
const TArray<void> arr_backup = *arr.base;
70-
*arr.base = TArray<void>{nullptr, 0, 0};
72+
*arr.base = TArray<void>{.data = nullptr, .count = 0, .max = 0};
7173

7274
try {
7375
const py::gil_scoped_acquire gil{};
@@ -89,6 +91,8 @@ UGbxGFxDialogBox* show_dialog_hook(UGbxPlayerController* player_controller,
8991
return show_dialog_ptr(player_controller, info);
9092
}
9193

94+
} // namespace
95+
9296
// NOLINTNEXTLINE(readability-identifier-length)
9397
PYBIND11_MODULE(dialog_box, m) {
9498
detour(SHOW_DIALOG, show_dialog_hook, &show_dialog_ptr,

src/bl3_mod_menu/native/options_getters.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
using namespace unrealsdk::unreal;
1212
using namespace unrealsdk::memory;
1313

14+
namespace {
15+
1416
const constinit Pattern<25> COMBO_BOX_GET_SELECTED_INDEX{
1517
"48 8B 81 ????????" // mov rax, [rcx+00000318]
1618
"48 85 C0" // test rax, rax
@@ -66,6 +68,8 @@ spinner_get_current_selection_index_func spinner_get_current_selection_index_ptr
6668
SPINNER_GET_CURRENT_SELECTION_INDEX.sigscan(
6769
"UGbxGFxListItemSpinner::GetCurrentSelectionIndex"));
6870

71+
} // namespace
72+
6973
// NOLINTNEXTLINE(readability-identifier-length)
7074
PYBIND11_MODULE(options_getters, m) {
7175
m.def(

src/bl3_mod_menu/native/options_setup.cpp

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
using namespace unrealsdk::unreal;
1919
using namespace unrealsdk::memory;
2020

21+
namespace {
22+
2123
using UGFxOptionBase = UObject;
2224
using UOptionDescriptionItem = UObject;
2325

24-
namespace {
25-
2626
const FName OPTION_CALLBACK = L"OnUnimplementedOptionClicked"_fn;
2727
const auto OPTION_DESCRIPTION_ITEM = unrealsdk::unreal::find_class(L"OptionDescriptionItem"_fn);
2828

@@ -56,8 +56,6 @@ UOptionDescriptionItem* create_description_item(
5656
return obj;
5757
}
5858

59-
} // namespace
60-
6159
namespace title {
6260

6361
// UGFxEchoCastMenu::SetupTitleItem and UGFxOptionBase::SetupTitleItem are essentially identical
@@ -435,6 +433,8 @@ void add_binding(UGFxOptionBase* self,
435433

436434
} // namespace controls
437435

436+
} // namespace
437+
438438
// NOLINTNEXTLINE(readability-identifier-length)
439439
PYBIND11_MODULE(options_setup, m) {
440440
slider::setup();

src/bl3_mod_menu/native/options_transition.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
using namespace unrealsdk::unreal;
2020
using namespace unrealsdk::memory;
2121

22+
namespace {
23+
2224
namespace transition {
2325

2426
// This signature matches all 6 callbacks for the different option menu entries - they're identical
@@ -297,6 +299,8 @@ void setup(void) {
297299

298300
} // namespace scroll
299301

302+
} // namespace
303+
300304
// NOLINTNEXTLINE(readability-identifier-length)
301305
PYBIND11_MODULE(options_transition, m) {
302306
transition::setup();

src/bl3_mod_menu/native/outer_menu.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
using namespace unrealsdk::unreal;
1414
using namespace unrealsdk::memory;
1515

16+
namespace {
17+
1618
pyunrealsdk::StaticPyObject add_menu_item_callback{};
1719

1820
#pragma region UGFxMainAndPauseBaseMenu::AddMenuItem
@@ -110,6 +112,8 @@ int32_t menu_state_offset = *reinterpret_cast<int32_t*>(
110112

111113
#pragma endregion
112114

115+
} // namespace
116+
113117
// NOLINTNEXTLINE(readability-identifier-length)
114118
PYBIND11_MODULE(outer_menu, m) {
115119
detour(ADD_MENU_ITEM_PATTERN, add_menu_item_hook, &add_menu_item_ptr,

src/bl3_mod_menu/options_callbacks.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828
)
2929

3030

31-
@hook("/Script/OakGame.GFxOptionBase:OnUnimplementedOptionClicked", Type.PRE, auto_enable=True)
31+
@hook(
32+
"/Script/OakGame.GFxOptionBase:OnUnimplementedOptionClicked",
33+
Type.PRE,
34+
immediately_enable=True,
35+
)
3236
def unimplemented_option_clicked( # noqa: C901 - imo the match is rated too highly
3337
obj: UObject,
3438
args: WrappedStruct,

src/bl3_mod_menu/options_setup.py

+71-21
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,75 @@ def get_displayed_option_at_idx(idx: int) -> BaseOption:
7474
return option_stack[-1].drawn_options[idx]
7575

7676

77+
def any_option_visible(options: Sequence[BaseOption]) -> bool:
78+
"""
79+
Recursively checks if any option in a sequence is visible.
80+
81+
Recurses into grouped options, but not nested ones. A grouped option which is not explicitly
82+
hidden, but contains no visible children, does not count as visible.
83+
84+
Args:
85+
options: The sequence of options to check.
86+
"""
87+
return any(
88+
(
89+
isinstance(option, GroupedOption)
90+
and not option.is_hidden
91+
and any_option_visible(option.children)
92+
)
93+
or (not option.is_hidden)
94+
for option in options
95+
)
96+
97+
98+
def draw_grouped_option(
99+
self: UObject,
100+
options: Sequence[BaseOption],
101+
group_stack: list[GroupedOption],
102+
option: GroupedOption,
103+
options_idx: int,
104+
) -> None:
105+
"""
106+
Draws a grouped option and it's children.
107+
108+
Args:
109+
self: The options menu being drawn.
110+
options: The full options list this group is part of.
111+
group_stack: The stack of currently open grouped options.
112+
option: The specific grouped option to add.
113+
options_idx: The index of the specific grouped option being added.
114+
"""
115+
if not any_option_visible(option.children):
116+
return
117+
118+
group_stack.append(option)
119+
120+
# If the first entry of the group is another group, don't draw a title, let the nested call do
121+
# it, so the first title is the most nested
122+
# If we're empty, or a different type, draw our own header
123+
if len(option.children) == 0 or not isinstance(option.children[0], GroupedOption):
124+
add_title(self, " - ".join(g.display_name for g in group_stack))
125+
option_stack[-1].drawn_options.append(option)
126+
127+
draw_options(self, option.children, group_stack)
128+
129+
group_stack.pop()
130+
131+
# If we didn't just close the outermost group, the group above us still has extra visible
132+
# options, and the next one of those options is not another group, re-draw the outer group's
133+
# header
134+
if (
135+
group_stack
136+
and options_idx != len(options) - 1
137+
and any_option_visible(options[options_idx + 1 :])
138+
and not isinstance(options[options_idx + 1], GroupedOption)
139+
):
140+
# This will print an empty string if we're on the last stack - which is about
141+
# the best we can do, we still want a gap
142+
add_title(self, " - ".join(g.display_name for g in group_stack))
143+
option_stack[-1].drawn_options.append(option)
144+
145+
77146
def draw_options( # noqa: C901 - imo the match is rated too highly
78147
self: UObject,
79148
options: Sequence[BaseOption],
@@ -152,26 +221,7 @@ def draw_options( # noqa: C901 - imo the match is rated too highly
152221
case GroupedOption() if option in group_stack:
153222
logging.dev_warning(f"Found recursive options group, not drawing: {option}")
154223
case GroupedOption():
155-
group_stack.append(option)
156-
157-
# If the first entry of the group is another group, don't draw a title, let the
158-
# nested call do it, so the first title is the most nested
159-
# If we're empty, or a different type, draw our own header
160-
if len(option.children) == 0 or not isinstance(option.children[0], GroupedOption):
161-
add_title(self, " - ".join(g.display_name for g in group_stack))
162-
option_stack[-1].drawn_options.append(option)
163-
164-
draw_options(self, option.children, group_stack)
165-
166-
group_stack.pop()
167-
168-
# If we have more options left in this group, and we're not immediately followed by
169-
# another group, re-draw the base header
170-
if idx != len(options) - 1 and not isinstance(options[idx + 1], GroupedOption):
171-
# This will print an empty string if we're on the last stack - which is about
172-
# the best we can do, we still want a gap
173-
add_title(self, " - ".join(g.display_name for g in group_stack))
174-
option_stack[-1].drawn_options.append(option)
224+
draw_grouped_option(self, options, group_stack, option, idx)
175225

176226
case _:
177227
logging.dev_warning(f"Encountered unknown option type {type(option)}")
@@ -357,7 +407,7 @@ def open_nested_options_menu(nested: NestedOption) -> None:
357407
)
358408

359409

360-
@hook("/Script/OakGame.GFxFrontendMenu:OnMenuStackChanged", Type.POST, auto_enable=True)
410+
@hook("/Script/OakGame.GFxFrontendMenu:OnMenuStackChanged", Type.POST, immediately_enable=True)
361411
def frontend_menu_change_hook(
362412
_1: UObject,
363413
args: WrappedStruct,

0 commit comments

Comments
 (0)