├── .gitattributes
├── .gitignore
├── README.md
└── reframework
└── autorun
├── ModOptionsMenu
└── ModMenuApi.lua
├── ModUI_ExampleTest.lua
└── OptionsMenu.lua
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | *.log
3 | Build
4 | Intermediate
5 | .vs
6 | *.zip
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ModOptionsMenu
2 |
3 |
4 |
5 |
6 |
7 |
8 | ## Example
9 |
10 | Look at the `ModUI_ExampleTest.lua` file for a more detailed example to get your feet wet.
11 |
12 | ```lua
13 |
14 | local modUI = require("ModOptionsMenu.ModMenuApi");
15 |
16 | local someSettingValue = false;
17 | local optionIdx = 1;
18 |
19 | local options = {"Option1", "Option2"};
20 |
21 | local name = "Example Mod";
22 | local description = "It's just a test mod.";
23 | local modObj = modUI.OnMenu(name, description, function()
24 |
25 | local changed;
26 |
27 | modUI.Header("Header");
28 |
29 | changed, someSettingValue = modUI.CheckBox("CheckBox", someSettingValue, "Some optional toolip style message here.");
30 |
31 | changed, optionIdx = modUI.Options("Options", optionIdx, options);
32 |
33 | --and so much more
34 |
35 | end)
36 | ```
37 |
38 |
39 |
40 | ## API
41 |
42 | ### `local ModUI = require("ModOptionsMenu.ModMenuApi")`
43 | Do something like this to import the api into your script.
44 | It can be called anything you'd like.
45 |
46 | ---
47 |
48 |
49 | ### `ModUI.OnMenu(name, description, uiCallback)`
50 | Register your mod to the options menu.
51 |
52 | #### Params:
53 | * `name` name of your mod that will be displayed in the menu
54 | * `description` will be displayed in the system message box to describe your mod in the mod list
55 | * `uiCallback` called every frame while your mod's menu is open. put your mod ui code in here
56 |
57 | #### Returns: `an object containing the mod's data`
58 |
59 | #### Notes:
60 | Technically you can register multiple mods through this but I would advise against it.
61 | Make sure to only call this once for the menu you want to add and NOT inside of some kind of update function.
62 |
63 |
64 | ---
65 |
66 |
67 |
68 | ### `ModUI.Header(text)`
69 | Displays a non-interactable header message to divide your ui sections.
70 | #### Notes:
71 | Displaying two headers right next to each other allows one to be selectable with a gamepad.
72 |
73 | ---
74 |
75 |
76 |
77 | ### `ModUI.FloatSlider(label, curValue, min, max, toolTip, isImmediateUpdate)`
78 | Draws a float slider.
79 |
80 | #### Params:
81 | * `label` displayed name of this setting
82 | * `curValue` the current/starting value that will be modified by the slider
83 | * `min` minimum value the slider can go to
84 | * `max` maximum value the slider can go to
85 | * `(optional) toolTip` message displayed in the system message box while hovering this element
86 | * `(optional) isImmediateUpdate`if true, the value will update immediately rather than waiting for the user to accept the change
87 |
88 | #### Returns: `(tuple of) wasChanged, newValue`
89 |
90 | #### Notes:
91 | Keep in mind this value only has precision to the nearest hundreth due to the game's limitations.
92 |
93 | ---
94 |
95 |
96 |
97 | ### `ModUI.Slider(label, curValue, min, max, toolTip, isImmediateUpdate)`
98 | Draws an integer slider.
99 |
100 | #### Params:
101 | * `label` displayed name of this setting
102 | * `curValue` the current/starting value that will be modified by the slider
103 | * `min` minimum value the slider can go to
104 | * `max` maximum value the slider can go to
105 | * `(optional) toolTip` message displayed in the system message box while hovering this element
106 | * `(optional) isImmediateUpdate` if true, the value will update immediately rather than waiting for the user to accept the change
107 |
108 | #### Returns: `(tuple of) wasChanged, newValue`
109 |
110 | ---
111 |
112 |
113 |
114 | ### `ModUI.Options(label, curValue, optionNames, optionMessages, toolTip, isImmediateUpdate)`
115 | Draws a cycle-able set of options for the user to choose between.
116 |
117 | #### Params:
118 | * `label` displayed name of this setting
119 | * `curValue` the current/starting index
120 | * `optionNames` a lua table of the displayed names for each selectable option e.g. `{"Option1", "Option2"}`
121 | * `(optional) optionMessages` a lua table of tooltips to go along with each option
122 | * `(optional) toolTip` message displayed in the system message box while hovering this element
123 | * `(optional) isImmediateUpdate` if true, the value will update immediately rather than waiting for the user to accept the change
124 |
125 | #### Returns: `(tuple of) wasChanged, newIndex`
126 |
127 | #### Notes:
128 | lua is NOT zero indexed, and neither is the input/output index of this function.
129 | The tables you give should be declared as variables OUTSIDE the scope of your UI callback.
130 | This is to avoid creating a new table every frame causing the UI to redraw every frame which breaks things.
131 |
132 | ---
133 |
134 |
135 |
136 | ### `ModUI.CheckBox(label, curValue, toolTip)`
137 | An easily clickable checkbox useful for on/off values where the user doesn't have to manually select on or off
138 |
139 | #### Params:
140 | * `label` displayed name of this setting
141 | * `curValue` the current/starting value
142 | * `(optional) toolTip` message displayed in the system message box while hovering this element
143 |
144 | #### Returns: `(tuple of) wasChanged, onOffValue`
145 |
146 | ---
147 |
148 |
149 |
150 | ### `ModUI.Toggle(label, curValue, toolTip, (optional)togNames[2], (optional)togMsgs[2], isImmediateUpdate)`
151 | Basically a wrapper around ModUI.Options that only takes two options and returns the result as a boolean instead of an index
152 |
153 | #### Params:
154 | * `label` displayed name of this setting
155 | * `curValue` the current/starting index
156 | * `(optional) toolTip` message displayed in the system message box while hovering this element
157 | * `(optional) togNames[2]` a lua table of the displayed names for each selectable option e.g. `{"Option1", "Option2"}`
158 | * `(optional) togMsgs[2]` a lua table of tooltips to go along with each option
159 | * `(optional) isImmediateUpdate` if true, the value will update immediately rather than waiting for the user to accept the change
160 |
161 | #### Returns: `(tuple of) wasChanged, onOffValue`
162 |
163 | #### Notes:
164 | The tables you give should be declared as variables OUTSIDE the scope of your UI callback.
165 | This is to avoid creating a new table every frame causing the UI to redraw every frame which breaks things.
166 |
167 | ---
168 |
169 |
170 |
171 | ### `ModUI.Button(label, prompt, isHighlight, toolTip)`
172 | Draws clickable element in the GUI
173 |
174 | #### Params:
175 | * `label` displayed name of this setting
176 | * `(optional) prompt` additional text to the right of the button
177 | * `(optional) isHighlight` if true, the label will be highlighted in yellow to make it more apparent it's a button
178 | * `(optional) toolTip` message displayed in the system message box while hovering this element
179 |
180 | #### Returns: `(boolean) wasClicked`
181 |
182 | ---
183 |
184 |
185 |
186 | ### `ModUI.Label(label, displayValue, toolTip)`
187 | Just draws some text
188 |
189 | #### Params:
190 | * `label` displayed name of this setting
191 | * `(optional) displayValue` additional text to the right of the label, useful to display values and such
192 | * `(optional) toolTip` message displayed in the system message box while hovering this element
193 |
194 | ---
195 |
196 |
197 |
198 |
199 | ### `ModUI.PromptYN(promptMessage, callback(result))`
200 | Displays a system message prompt with an option to select yes or no.
201 |
202 | #### Params:
203 | * `promptMessage` text displayed within the prompt
204 | * `callback(result)` function called when the user has selected their choice
205 |
206 | #### Notes:
207 | The result in the callback will be true if the user selected `Yes`, and false if `No`.
208 | ---
209 |
210 |
211 |
212 |
213 | ### `ModUI.PromptMsg(promptMessage, callback)`
214 | Displays a system message prompt.
215 |
216 | #### Params:
217 | * `promptMessage` text displayed within the prompt
218 | * `callback` function called when the user has closed the prompt
219 |
220 | #### Notes:
221 | The normal UI is not updated while the prompt is open.
222 | ---
223 |
224 |
225 |
226 |
227 | ## Rich Text:
228 |
229 | The game has its own sort of 'rich text' functionality, currently I only really know how to use colors.
230 | I think there's a system for displaying button icons through text but you'd have to figure that out yourself.
231 |
232 | ### Built-in Colors:
233 | * `YEL`
234 | * `RED`
235 | * `GRAY`
236 | * More colors can be added (see `AddTextColor` below)
237 |
238 | ---
239 | ### `ModUI.AddTextColor(colName, colHexStr)`
240 | #### Params:
241 | * `colName` name of the color you wish to add, should be distinct
242 | * `colHexStr` a string representing the color's hex code WITHOUT '#' symbol e.g. `"9F2B68"`
243 | #### Notes:
244 | Call this BEFORE your UI code, otherwise will add the color every frame which would be BAD.
245 | Keep in mind these are shared across mods so use descriptive names.
246 | Use the name like the built in color codes e.g. if you added a color called 'purple' use `
text`
247 | ---
248 |
249 |
250 |
251 |
252 | ## Layout Functions:
253 | * `ModUI.IncreaseIndent()`
254 | * `ModUI.DecreaseIndent()`
255 | * `ModUI.SetIndent(indentLevel)`
256 |
257 | #### Notes:
258 | You can have fairly dynamic UI layouts, but keep in mind every time something changes the entire UI needs to be rebuilt. Also the practical limits of how many elements you can have in one menu is untested currently.
259 | ---
260 |
261 |
262 |
263 |
264 | ## Rare Functions:
265 |
266 | * ### `ModUI.Repaint()`
267 | Forces game to re-show the data and show changes.
268 | You probably don't need to use this anymore as almost any change should be automatically detected.
269 |
270 | * ### `ModUI.ForceDeselect()`
271 | Forces game to deselect current option.
272 |
273 | * ### `modObj.regenOptions`
274 | Can be set to true to force the API to regenerate the UI layout, but you probably dont need this.
275 |
276 | ---
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
--------------------------------------------------------------------------------
/reframework/autorun/ModOptionsMenu/ModMenuApi.lua:
--------------------------------------------------------------------------------
1 | -----------------------------------------------------------------------------------------------------------------------------------
2 | ----------------------------------------------------------API--------------------------------------------------------------------
3 | -----------------------------------------------------------------------------------------------------------------------------------
4 |
5 |
6 | --Global mod variables
7 | if not _CModUiList then
8 | _CModUiList = {};
9 | _CModUiCurMod = nil;
10 | _CModUiPromptCoRo = nil;
11 | _CmodUiColors = {};
12 | end
13 |
14 | local ModUI = {
15 | version = 1.65;
16 | };
17 |
18 | local ENUM = 0;
19 | local SLIDER = 1;
20 | local OTHERWINDOW = 2; --not used as this basically opens sub windows for graphics and stuff
21 | local WATCHITEM = 3; --no idea what this is for really
22 | local HEADER = 4;
23 | local BUTTON = 5; --custom type
24 |
25 |
26 |
27 |
28 |
29 |
30 | local lineBreakPattern = "(" .. ('.'):rep(40) .. ('.?'):rep(16) .. ") " -- in regex: /(.{40,56}) /
31 |
32 | local function WrapText(text)
33 | if not text then
34 | return text;
35 | end
36 | local newlinePos = text:find("\n");
37 | if newlinePos then
38 | return text; -- assume the mod author wants to place newlines themselves
39 | end
40 | if #text <= 56 then
41 | return text;
42 | end
43 |
44 | text = text:gsub(lineBreakPattern, "%1\n");
45 | return text;
46 | end
47 |
48 | local function WrapTextTable(textTable)
49 |
50 | if not textTable then
51 | return nil;
52 | end
53 |
54 | local newTextTable = {};
55 |
56 | for _, text in ipairs(textTable) do
57 | table.insert(newTextTable, WrapText(text));
58 | end
59 |
60 | return newTextTable;
61 | end
62 |
63 |
64 |
65 |
66 |
67 | function ModUI.OnMenu(name, descript, uiCallback)
68 |
69 | if not name then name = ""; end
70 | if not descript then descript = ""; end
71 |
72 | local mod = {
73 | originalName = name;
74 | modName = name;
75 | modNameSuid = 1;
76 | originalDescription = descript;
77 | description = WrapText(descript);
78 | optionsOrdered = {};
79 | guiCallback = uiCallback;
80 | created = false;
81 | curOptIdx = 0;
82 | indent = 0;
83 | };
84 |
85 | mod.UpdateGui = (function()
86 |
87 | mod.indent = 0;
88 | mod.curOptIdx = 0;
89 |
90 | mod.guiCallback();
91 |
92 | if mod.curOptIdx ~= mod.optionsCount then
93 | mod.regenOptions = true;
94 | end
95 | end)
96 |
97 | table.insert(_CModUiList, mod);
98 |
99 | return mod;
100 | end
101 |
102 | function ModUI.Repaint()
103 | _CModUiRepaint();
104 | end
105 |
106 | local ColorStringType = sdk.find_type_definition("snow.gui.MessageManager.ColorString");
107 | function ModUI.AddTextColor(colName, colHexStr)
108 |
109 | if not _CmodUiColors then _CmodUiColors = {}; end
110 |
111 | local newCol = ColorStringType:create_instance():add_ref();
112 | newCol.ColorName = colName;
113 | newCol.ColorValueStr = colHexStr;
114 | table.insert(_CmodUiColors, newCol);
115 | end
116 |
117 | function ModUI.ForceDeselect()
118 | local guiManager = sdk.get_managed_singleton("snow.gui.GuiManager");
119 | local optionWindow = guiManager:get_refGuiOptionWindow();
120 |
121 | optionWindow._State = 2;
122 | optionWindow:setIsEditValue(false);
123 | end
124 |
125 | function ModUI.IncreaseIndent(val)
126 | _CModUiCurMod.indent = _CModUiCurMod.indent + (val and val or 1);
127 | end
128 |
129 | function ModUI.DecreaseIndent(val)
130 | _CModUiCurMod.indent = _CModUiCurMod.indent - (val and val or 1);
131 | end
132 |
133 |
134 | function ModUI.SetIndent(val)
135 | _CModUiCurMod.indent = val;
136 | end
137 |
138 | local function GetIndent(level)
139 | local pad = " ";
140 | for i = 2, level do
141 | pad = pad .. " ";
142 | end
143 | return pad;
144 | end
145 |
146 | local function GetFormattedName(name)
147 | if _CModUiCurMod.indent > 0 then
148 | return GetIndent(_CModUiCurMod.indent) .. name;
149 | else
150 | return name;
151 | end
152 | end
153 |
154 | local function GetOptionData(mod, optType, label, toolTip, defaultValue, immediate)
155 |
156 | mod.curOptIdx = mod.curOptIdx + 1;
157 | local data = mod.optionsOrdered[mod.curOptIdx];
158 |
159 | if not data then
160 |
161 | if not defaultValue then defaultValue = 0; end
162 |
163 | data = {
164 | parentMod = mod;
165 | type = optType;
166 | value = defaultValue;
167 | desiredValue = defaultValue;
168 | oldValue = defaultValue;
169 | name = label;
170 | displayName = (optType == HEADER) and label or GetFormattedName(label);
171 | message = toolTip;
172 | displayMessage = WrapText(toolTip);
173 | min = 0;
174 | max = 0;
175 | enumCount = 1;
176 | optionIdx = mod.curOptIdx;
177 | immediate = immediate;
178 | };
179 |
180 | mod.regenOptions = true;
181 | mod.optionsOrdered[mod.curOptIdx] = data;
182 |
183 | return data, true;
184 | else
185 |
186 | if data.name ~= label
187 | or data.message ~= toolTip
188 | or mod.regenOptions then
189 |
190 | mod.regenOptions = true;
191 | return;
192 | end
193 |
194 | return data, false;
195 | end
196 | end
197 |
198 |
199 |
200 | function ModUI.Slider(label, curValue, min, max, toolTip, immediate, isFloat)
201 |
202 | local mod = _CModUiCurMod;
203 | local optData, new = GetOptionData(mod, SLIDER, label, toolTip, curValue, immediate);
204 | if new then
205 | optData.min = min;
206 | optData.max = max;
207 | optData.float = isFloat;
208 | end
209 | if mod.regenOptions then return false, curValue; end
210 |
211 | local changed = optData.oldValue ~= optData.value;
212 | if not optData.wasChanged then
213 | --having to round this value is really dumb but otherwise it starts to bork things because of floating point precision issues
214 | optData.desiredValue = math.floor(curValue + 0.5);
215 | end
216 | optData.wasChanged = false;
217 | optData.oldValue = optData.value;
218 | return changed, optData.value;
219 | end
220 |
221 | -- the game legit internally represents float sliders as integers but scaled by 100 and then adds a decimal point...
222 | -- this is why i initially thought the game didnt even support float sliders
223 | -- this implementation is just so wack
224 | function ModUI.FloatSlider(label, curValue, min, max, toolTip, immediate)
225 |
226 | if not curValue then curValue = 0; end
227 |
228 | local changed, val = ModUI.Slider(label, curValue * 100, min * 100, max * 100, toolTip, immediate, true);
229 | return changed, val * 0.01;
230 | end
231 |
232 |
233 | function ModUI.Header(label)
234 | local mod = _CModUiCurMod;
235 | local optData = GetOptionData(mod, HEADER, label);
236 | end
237 |
238 |
239 | function ModUI.Button(label, prompt, isHighlight, toolTip)
240 |
241 | prompt = prompt or "Go";
242 |
243 | local mod = _CModUiCurMod;
244 | local optData, new = GetOptionData(mod, ENUM, label, toolTip);
245 |
246 |
247 | if new then
248 | optData.isBtn = true;
249 | optData.value = false;
250 | optData.enumNames = {prompt};
251 | optData.prompt = prompt;
252 |
253 | if isHighlight then
254 | optData.displayName = "" .. optData.displayName .. "";
255 | end
256 | end
257 | if mod.regenOptions then return false; end
258 |
259 |
260 | if optData.prompt ~= prompt then
261 | mod.regenOptions = true;
262 | end
263 |
264 | if optData.value then
265 | optData.value = false;
266 | return true;
267 | end
268 |
269 | return false;
270 | end
271 |
272 |
273 | local checkLabels = {"☐","☒"};
274 | function ModUI.CheckBox(label, curValue, toolTip)
275 |
276 | local mod = _CModUiCurMod;
277 | local optData, new = GetOptionData(mod, ENUM, label, toolTip);
278 |
279 | local idxValue = curValue and 1 or 0;
280 | if new then
281 | optData.isBtn = true;
282 | optData.value = false;
283 | optData.enumNames = checkLabels;
284 | optData.enumCount = 2;
285 | optData.max = 2;
286 | optData.desiredValue = idxValue;
287 | end
288 | if mod.regenOptions then return false, curValue; end
289 |
290 | local changed = false;
291 | if optData.value then
292 | changed = true;
293 | elseif optData.desiredValue ~= idxValue then
294 | changed = true;
295 | end
296 |
297 | if changed then
298 | optData.value = false;
299 | curValue = not curValue;
300 | optData.desiredValue = curValue and 1 or 0;
301 | optData.data._SelectValue = optData.desiredValue;
302 | optData.data._OldSelectValue = optData.desiredValue;
303 | ModUI.Repaint();
304 | end
305 |
306 | return changed, curValue;
307 | end
308 |
309 |
310 | function ModUI.Label(label, displayValue, toolTip)
311 |
312 | local mod = _CModUiCurMod;
313 | local opt, new = GetOptionData(mod, WATCHITEM, label, toolTip);
314 |
315 | if new then
316 | opt.prompt = displayValue;
317 | opt.enumNames = {displayValue};
318 | end
319 | if mod.regenOptions then return; end
320 |
321 | if opt.prompt ~= displayValue then
322 | mod.regenOptions = true;
323 | end
324 | end
325 |
326 |
327 | function ModUI.Options(label, curValue, optionNames, optionMessages, toolTip, immediate)
328 |
329 | local mod = _CModUiCurMod;
330 | local opt, new = GetOptionData(mod, ENUM, label, toolTip, curValue - 1, immediate);
331 |
332 | if new then
333 |
334 | local count = 0;
335 | for i, t in ipairs(optionNames) do
336 | count = count + 1;
337 | end
338 |
339 | opt.enumCount = count;
340 | opt.max = count;
341 | opt.enumNames = optionNames;
342 | opt.originalEnumMessages = optionMessages;
343 | opt.enumMessages = WrapTextTable(optionMessages);
344 | end
345 | if mod.regenOptions then
346 | return false, curValue;
347 | end
348 |
349 | if optionNames ~= opt.enumNames or optionMessages ~= opt.originalEnumMessages then
350 | mod.regenOptions = true;
351 | end
352 |
353 | local changed = opt.oldValue ~= opt.value;
354 | if not opt.wasChanged then
355 | opt.desiredValue = curValue - 1;
356 | end
357 | opt.wasChanged = false;
358 | opt.oldValue = opt.value;
359 | return changed, opt.value + 1;
360 | end
361 |
362 |
363 | --not entirely sure how i feel about these symbols but its neat
364 | local offOn = {"✖","√"};
365 | local offOnMsg = {"Disabled.","Enabled."};
366 | function ModUI.Toggle(label, curValue, toolTip, togNames, togMsgs, immediate)
367 | local idx = curValue and 2 or 1;
368 | if not togNames then togNames = offOn; end
369 | if not togMsgs then togMsgs = offOnMsg; end
370 | local changed, optSel = ModUI.Options(label, idx, togNames, togMsgs, toolTip, immediate);
371 | return changed, (optSel == 2);
372 | end
373 |
374 |
375 |
376 | function ModUI.PromptMsg(promptMessage, callback)
377 |
378 | _CModUiPromptCoRo = coroutine.create(function()
379 |
380 | local gui_mgr = sdk.get_managed_singleton("snow.gui.GuiManager")
381 | gui_mgr:call(
382 | "setOpenInfo(System.String, snow.gui.GuiCommonInfoBase.Type, snow.gui.SnowGuiCommonUtility.Segment, System.Boolean, System.Boolean, snow.gui.GuiRootBaseBehavior)"
383 | , promptMessage, 0x1, 0x32, false, false, nil)
384 |
385 | coroutine.yield();
386 |
387 | while not gui_mgr:updateInfoWindow() do
388 | coroutine.yield();
389 | end
390 |
391 | if callback then callback(); end
392 | end);
393 | end
394 |
395 | function ModUI.PromptYN(promptMessage, callback)
396 |
397 | _CModUiPromptCoRo = coroutine.create(function()
398 |
399 | local result = 2;
400 |
401 | local guiMgr = sdk.get_managed_singleton("snow.gui.GuiManager");
402 | guiMgr:call(
403 | "setOpenYNInfo(System.String, snow.gui.GuiManager.YNInfoUIState, snow.gui.SnowGuiCommonUtility.Segment, System.Boolean, System.Boolean)"
404 | ,
405 | promptMessage, 0, 0x32, false, false
406 | )
407 |
408 | coroutine.yield();
409 | while result == 2 do
410 | local uiConfirmSoundID = 0xaa66032d;
411 | result = guiMgr:updateYNInfoWindow(uiConfirmSoundID);
412 | coroutine.yield();
413 | end
414 |
415 | guiMgr:closeYNInfo();
416 |
417 | if callback then callback(result == 0); end
418 | end);
419 | end
420 |
421 |
422 | return ModUI;
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
--------------------------------------------------------------------------------
/reframework/autorun/ModUI_ExampleTest.lua:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | local apiPackageName = "ModOptionsMenu.ModMenuApi";
8 |
9 | local settings;
10 | local function CreateNewSettings()
11 | settings = {
12 | slide1 = 42;
13 | slide2 = 314;
14 | select1 = 1;
15 | check1 = false;
16 | toggle1 = false;
17 | hide = false;
18 | }
19 | end
20 | CreateNewSettings();
21 |
22 |
23 | local function LoadSettings()
24 | local loadedSettings = json.load_file("TestModSettings.json");
25 | if loadedSettings then
26 | settings = loadedSettings;
27 | end
28 | end
29 | LoadSettings();
30 | settings.select1 = 1;
31 |
32 | --no idea how this works but google to the rescue
33 | --can use this to check if the api is available and do an alternative to avoid complaints from users
34 | function IsModuleAvailable(name)
35 | if package.loaded[name] then
36 | return true
37 | else
38 | for _, searcher in ipairs(package.searchers or package.loaders) do
39 | local loader = searcher(name)
40 | if type(loader) == 'function' then
41 | package.preload[name] = loader
42 | return true
43 | end
44 | end
45 | return false
46 | end
47 | end
48 |
49 |
50 | local modUI = nil;
51 |
52 | if IsModuleAvailable(apiPackageName) then
53 | modUI = require(apiPackageName);
54 | end
55 |
56 |
57 | if not modUI then
58 | re.msg("No ModUI API package found. \nYou may need to download it or something.");
59 | return;
60 | end
61 |
62 |
63 | --[[
64 | Known colors for "rich text": There really arent that many
65 | But you can add more with ModUI.AddTextColor
66 | YEL
67 | RED
68 | GRAY
69 | --]]
70 |
71 | --Technicallyyyy I think theres some weird formatting to get key icons to display but eh
72 |
73 |
74 | --[[
75 | Here's a List of all the available api functions:
76 | all tooltip type things should be optional
77 |
78 | ModUI.OnMenu(name, descript, uiCallback)
79 | ModUI.FloatSlider(label, curValue, min, max, toolTip, isImmediateUpdate) -- keep in mind this value only has precision to the nearest hundreth
80 | ModUI.Slider(label, curValue, min, max, toolTip, isImmediateUpdate)
81 | ModUI.Button(label, prompt, isHighlight, toolTip)
82 | ModUI.CheckBox(label, curValue, toolTip)
83 | ModUI.Toggle(label, curValue, toolTip, (optional)togNames[2], (optional)togMsgs[2], isImmediateUpdate)
84 | ModUI.Label(label, displayValue, toolTip)
85 | ModUI.Options(label, curValue, optionNames, optionMessages, toolTip, isImmediateUpdate)
86 | ModUI.PromptYN(promptMessage, callback(result))
87 | ModUI.PromptMsg(promptMessage, callback)
88 |
89 | ModUI.Repaint() -- forces game to re-show the data and show changes
90 | ModUI.ForceDeselect() -- forces game to deselect current option
91 | modObj.regenOptions -- can be set to true to force the API to regenerate the UI layout, but you probably dont need this
92 |
93 | --call this BEFORE your UI code
94 | --keep in mind these are shared across mods so use descriptive names
95 | --do NOT include # in your hex color code string
96 | ModUI.AddTextColor(colName, colHexStr)
97 |
98 | ModUI.IncreaseIndent()
99 | ModUI.DecreaseIndent()
100 | ModUI.SetIndent(indentLevel)
101 | ]]--
102 |
103 |
104 |
105 |
106 | local buttonTxt = "Press Me";
107 | local buttonPressed = false;
108 | local labelValue = tostring(settings.slide1);
109 |
110 |
111 | --Colors
112 | modUI.AddTextColor("purp", "9F2B68");
113 |
114 |
115 | local optionNames = {
116 | "Basic Option",
117 | "Neat Option",
118 | "Epic Option",
119 | };
120 |
121 | local optionDescriptions = {
122 | "It's a basic, run-of-the-mill, option.",
123 | "Neato.",
124 | "Epic gamer moments only.",
125 | };
126 |
127 |
128 | local name = "Rad Example Mod";
129 | local description = "It's just a test mod. What more do you want?\nAuthored by: Bolt";
130 | local modObj = modUI.OnMenu(name, description, function()
131 |
132 | local changed = false;
133 |
134 | modUI.Header("Wow Custom Mod Settings This Is Crazy");
135 | changed, settings.slide1 = modUI.Slider("Nice Slider", settings.slide1, 0, 69, "Weeeee.");
136 |
137 | if changed then
138 | --do something with slider value here
139 |
140 | labelValue = (settings.slide1 == 69) and "Nice" or tostring(settings.slide1);
141 |
142 | if (settings.slide1 == 69) then
143 |
144 | modUI.PromptMsg("That's Nice.", function()
145 | --optional callback
146 | modUI.ForceDeselect();
147 | modUI.Repaint();
148 | end);
149 | end
150 | end
151 |
152 | if modUI.Button("This is a Button", buttonTxt, false, "It's just a button, really...") then
153 |
154 | if buttonTxt == "Cool it." then
155 | modUI.PromptYN("Did you mean to do that?", function(result)
156 | buttonTxt = (result and "Rude." or "It's Okay.");
157 | modUI.Repaint();
158 | end);
159 | else
160 | buttonPressed = not buttonPressed;
161 | buttonTxt = buttonPressed and "おめでとうね" or "Cool it.";
162 | end
163 |
164 | --need to repaint if text changes or something so it updates responsively
165 | modUI.Repaint();
166 | end
167 |
168 | if modUI.version >= 1.2 then
169 | changed, settings.slide2 = modUI.FloatSlider("Precise Slider", settings.slide2, 69, 420, "Well, it's only really accurate to 2 decimal places...");
170 |
171 |
172 | if modUI.Button("[Hide Section 2]", "", true, "Crazy.") then
173 | settings.hide = not settings.hide;
174 | end
175 | end
176 |
177 |
178 | if not settings.hide then
179 | modUI.Header("Another Header Just Because");
180 |
181 | changed, settings.check1 = modUI.CheckBox("Hey, A CheckBox!", settings.check1, "Why didn't I think of this sooner?!");
182 |
183 | modUI.Label("It's a label I Guess", labelValue, "Exciting, right?");
184 |
185 | changed, settings.select1 = modUI.Options("My Option Set", settings.select1, optionNames, optionDescriptions,
186 | "Check out my cool options, half-off.");
187 |
188 | if changed then
189 | --do something with the selected index here
190 | log.debug("Selected: " .. settings.select1);
191 | end
192 |
193 | changed, settings.toggle1 = modUI.Toggle("Toggle me, senpai!", settings.toggle1, "OwO");
194 | if changed and settings.toggle1 then
195 | modUI.ForceDeselect();
196 | modUI.PromptMsg("Pervert...");
197 | settings.toggle1 = false;
198 | end
199 | end
200 |
201 | end);
202 |
203 |
204 | --add a callback here in order to hook when the user resets all settings
205 | modObj.OnResetAllSettings = (function()
206 | CreateNewSettings();
207 | end)
208 |
209 |
210 |
211 |
212 |
213 | -------------------------------SAVE DATA STUFF---------------------
214 |
215 | local function SaveSettings()
216 | json.dump_file("TestModSettings.json", settings);
217 | end
218 |
219 |
220 | re.on_config_save(function()
221 | SaveSettings();
222 | end)
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
--------------------------------------------------------------------------------
/reframework/autorun/OptionsMenu.lua:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | -----------------------------------------------------------------------------------------------------------------------------------
7 | ----------------------------------------------------------UTILITY--------------------------------------------------------------------
8 | -----------------------------------------------------------------------------------------------------------------------------------
9 |
10 |
11 | local SAVE_DATA_IDX = 4;
12 | local DISPLAY_IDX = 5;
13 | local ADV_GPU_OPT_IDX = 7;
14 | local MOD_TAB_IDX = 6;
15 |
16 | local ENUM = 0;
17 | local SLIDER = 1;
18 | local OTHERWINDOW = 2;
19 | local WATCHITEM = 3; --no idea what this is for really
20 | local HEADER = 4;
21 | local BUTTON = 5; --custom type
22 |
23 | local optionBaseDataType = sdk.find_type_definition("snow.StmGuiOptionData.StmOptionBaseData");
24 | local optionDataType = sdk.find_type_definition("snow.StmOptionData");
25 |
26 | local OptionName_OFFSET = optionBaseDataType:get_field("_OptionName"):get_offset_from_base();
27 | local OptionMessage_OFFSET = optionBaseDataType:get_field("_OptionSystemMessage"):get_offset_from_base();
28 | local SAVE_DATA_SUID = 789582228;
29 |
30 | local function SaveData()
31 | if reframework.save_config then
32 | reframework:save_config();
33 | end
34 | end
35 |
36 |
37 | local suidCounter = 1;
38 | local modStrings = {};
39 | local modStringsToSuids = {};
40 |
41 | modStrings[1] = sdk.to_ptr(sdk.create_managed_string(""):add_ref_permanent());
42 | modStringsToSuids[""] = 1;
43 |
44 | local function GetNewId()
45 | suidCounter = suidCounter + 1;
46 | return suidCounter;
47 | end
48 |
49 | local function StringToSuid(str)
50 |
51 | if not str then return 1; end
52 |
53 | local suid = modStringsToSuids[str];
54 | if suid then
55 | return suid;
56 | end
57 |
58 | --not entirely sure why these strings need to be permanent ref but the game crashes otherwise so whatever
59 | suid = GetNewId();
60 | modStrings[suid] = sdk.to_ptr(sdk.create_managed_string(str):add_ref_permanent());
61 | modStringsToSuids[str] = suid;
62 | return suid;
63 | end
64 |
65 |
66 |
67 | --I dont like having to use the "write" functions but it simply doesnt work to set the guid values normally using set_field or anything
68 | local function SetBaseDataOptionName(baseData, str)
69 | local suid = StringToSuid(str);
70 | baseData:write_dword(OptionName_OFFSET, suid);
71 | return suid;
72 | end
73 |
74 | local function SetBaseDataOptionMessage(baseData, str)
75 | local suid = StringToSuid(str);
76 | baseData:write_dword(OptionMessage_OFFSET, suid);
77 | return suid;
78 | end
79 |
80 |
81 | local guidType = sdk.find_type_definition("System.Guid");
82 | local guidTypeSystem = sdk.typeof("System.Guid");
83 |
84 | local function GetManualGuid(suid)
85 | local guid = ValueType.new(guidType);
86 | guid:set_field("mData1", suid);
87 | return guid;
88 | end
89 |
90 | local function CreateGuidArray(count, stringTable)
91 |
92 | local arr = sdk.create_managed_array(guidTypeSystem, count):add_ref_permanent();
93 |
94 | for idx, str in ipairs(stringTable) do
95 |
96 | local suid = StringToSuid(str);
97 | local guid = GetManualGuid(suid);
98 |
99 | --no idea why but calling this "Set" method works while "set_Item" doesnt and its very annoying
100 | arr:call("Set", idx - 1, guid);
101 | end
102 |
103 | return arr;
104 | end
105 |
106 |
107 | --Default Strings
108 | local ModsListName_Str = sdk.create_managed_string("Mods"):add_ref_permanent();
109 | local ModsListName_Ptr = sdk.to_ptr(ModsListName_Str);
110 | local ModsListDesc_Ptr = sdk.to_ptr(sdk.create_managed_string("Adjust settings for mods using the custom mod menu."):add_ref_permanent());
111 | local Go_STRING = ("Go");
112 | local OpenMenu_STRING = ("Open Menu");
113 | local Back_SUID = StringToSuid("Back To Mod List");
114 | local Null_SUID = StringToSuid("Null");
115 | local Return_Str = "Return to the list of mods.";
116 | local Return_SUID = StringToSuid(Return_Str);
117 | local OpenMenu_ARR = CreateGuidArray(1, {OpenMenu_STRING});
118 | local Go_ARR = CreateGuidArray(2, {Go_STRING, Go_STRING});
119 |
120 |
121 |
122 |
123 | local GuiOptionWindowTypeSystem = sdk.typeof("snow.gui.GuiOptionWindow");
124 | local viaGuiType = sdk.find_type_definition("via.gui.GUI");
125 | local get_GameObject = viaGuiType:get_method("get_GameObject");
126 | local goType = sdk.find_type_definition("via.GameObject");
127 | local get_Components = goType:get_method("get_Components");
128 | local get_Name = goType:get_method("get_Name");
129 | local getComponent = goType:get_method("getComponent(System.Type)");
130 |
131 | local guiUtilityType = sdk.find_type_definition("snow.gui.SnowGuiCommonUtility");
132 | local playSound = guiUtilityType:get_method("reqSe(System.UInt32)");
133 | local uiConfirmSoundID = 0xaa66032d;
134 |
135 | local msgManagerType = sdk.find_type_definition("snow.gui.MessageManager");
136 | local ColTagUserData = msgManagerType:get_field("ColTagUserData");
137 |
138 |
139 | local uiOpen = false;
140 | local mainBaseDataList;
141 | local mainDataList;
142 | local modBaseDataList;
143 | local modDataList;
144 | local displayedList;
145 | local defaultSelMsgGuidArr;
146 |
147 | local guiManager;
148 | local optionWindow;
149 | local messageWindow;
150 | local mainScrollList;
151 | local subHeadingTxt;
152 | local unifier;
153 |
154 | local function GetUnifier()
155 | if not unifier then
156 | unifier = optionWindow:call("get_OptionDataUnifier");
157 | end
158 |
159 | return unifier;
160 | end
161 |
162 | local function SetOptionWindow(optWin)
163 |
164 | if optWin then
165 | optionWindow = optWin;
166 | else
167 | guiManager = sdk.get_managed_singleton("snow.gui.GuiManager");
168 | if not guiManager then return end
169 | optionWindow = guiManager:get_refGuiOptionWindow();
170 | messageWindow = guiManager:get_refGuiCommonMessageWindow();
171 | end
172 |
173 |
174 | mainScrollList = optionWindow._scrL_MainOption;
175 | subHeadingTxt = optionWindow._txt_SubHeading;
176 | end
177 |
178 | local ignoreSetSysMsg = false;
179 | local function SetSystemMessage(str)
180 | messageWindow:setSystemMessageText(str, 40);
181 | ignoreSetSysMsg = true;
182 | end
183 |
184 |
185 | local function AppendArray(inArr, arrType, addItem)
186 |
187 |
188 | local count = 0;
189 | if inArr then
190 | count = inArr:get_size();
191 | end
192 |
193 | local newArr = sdk.create_managed_array(arrType, count + 1);
194 | newArr:add_ref_permanent();
195 |
196 | for i = 0, count - 1 do
197 | newArr[i] = inArr[i];
198 | end
199 |
200 | newArr[count] = addItem;
201 |
202 | return newArr;
203 |
204 | end
205 |
206 | local function ArrayFirstElements(inArr, arrType, numElements)
207 |
208 | local newArr = sdk.create_managed_array(arrType, numElements);
209 | newArr:add_ref_permanent();
210 |
211 | for i = 0, numElements - 1 do
212 | newArr[i] = inArr[i];
213 | end
214 |
215 | return newArr;
216 | end
217 |
218 |
219 | local modGuids = {};
220 |
221 |
222 |
223 |
224 | local OptionBaseDataType = sdk.find_type_definition("snow.gui.userdata.GuiOptionData.OptionBaseData");
225 | local OptionNameField = OptionBaseDataType:get_field("OptionName");
226 |
227 |
228 | local function AddNewTopMenuCategory(catList)
229 |
230 | if not catList then
231 | catList = optionWindow:get_OptionCategoryTypeList();
232 | end
233 |
234 |
235 | local catListCount = catList:get_Count();
236 |
237 | --prob shouldnt hardcode this but i dont exactly see them adding new options menu categories any time soon
238 | if catListCount > 6 then
239 | --mod entry already exists
240 | return;
241 | end
242 |
243 | catList:Add(SAVE_DATA_IDX);
244 | end
245 |
246 |
247 | local function GetUnifiedOptionArrays(idx)
248 |
249 | local catBaseDict = GetUnifier()._SortedUnifiedOptionBaseDataMap;
250 | local catDict = GetUnifier()._SortedUnifiedOptionDataMap;
251 |
252 | local baseList = catBaseDict:get_Item(idx);
253 | local dataList = catDict:get_Item(idx);
254 |
255 | return baseList, dataList, catBaseDict, catDict;
256 | end
257 |
258 | local function SetUnifiedOptionArrays(idx, baseDatas, datas, shouldAppend, shouldReset)
259 |
260 | displayedList = baseDatas;
261 |
262 | local baseList, dataList, catBaseDict, catDict = GetUnifiedOptionArrays(idx);
263 |
264 | if shouldAppend then
265 |
266 | if shouldReset then
267 |
268 | baseList = ArrayFirstElements(baseList, sdk.typeof("snow.StmUnifiedOptionBaseData"), 1);
269 | dataList = ArrayFirstElements(dataList, sdk.typeof("snow.StmUnifiedOptionData"), 1);
270 |
271 | catBaseDict:set_Item(idx, baseList);
272 | catDict:set_Item(idx, dataList);
273 |
274 | else
275 | catBaseDict:set_Item(idx, AppendArray(baseList, sdk.typeof("snow.StmUnifiedOptionBaseData"), baseDatas));
276 | catDict:set_Item(idx, AppendArray(dataList, sdk.typeof("snow.StmUnifiedOptionData"), datas));
277 | end
278 | else
279 | catBaseDict:set_Item(idx, baseDatas);
280 | catDict:set_Item(idx, datas);
281 | end
282 | end
283 |
284 |
285 | local function SetOptStrings(opt)
286 |
287 | SetBaseDataOptionName(opt.baseData, opt.displayName);
288 | SetBaseDataOptionMessage(opt.baseData, opt.displayMessage);
289 |
290 | if opt.baseData._OptionItemName then
291 | opt.baseData._OptionItemName:force_release();
292 | end
293 |
294 | if opt.baseData._OptionItemSelectMessage then
295 | opt.baseData._OptionItemSelectMessage:force_release();
296 | end
297 |
298 | --for some reason the game will crash if its a header type with empty OptionItemName[]
299 | --even though its a dang header that doesnt need them jeez
300 | if opt.enumNames then
301 | opt.baseData._OptionItemName = CreateGuidArray(opt.enumCount, opt.enumNames);
302 | else
303 | opt.baseData._OptionItemName = defaultSelMsgGuidArr;
304 | end
305 |
306 | if opt.enumMessages then
307 | opt.baseData._OptionItemSelectMessage = CreateGuidArray(opt.enumCount, opt.enumMessages);
308 | else
309 | opt.baseData._OptionItemSelectMessage = defaultSelMsgGuidArr;
310 | end
311 | end
312 |
313 | local function PrintObj(obj)
314 |
315 | local output = "{\n";
316 | for key, value in pairs(obj) do
317 | output = output .. " " .. tostring(key) .. " = " .. tostring(value) .. ",\n";
318 | end
319 |
320 | output = output .. "}";
321 | return output;
322 | end
323 |
324 | local function GetNewBaseData(opt)
325 |
326 | local unifiedData = sdk.create_instance("snow.StmUnifiedOptionBaseData", true):add_ref();
327 | local newBaseData = sdk.create_instance("snow.StmGuiOptionData.StmOptionBaseData"):add_ref();
328 |
329 | if opt then
330 |
331 | --log.debug(PrintObj(opt));
332 |
333 | if opt.float then
334 | --setting this to 10 is mouse sensitivity and will make it appear as a float
335 | newBaseData._OptionType = 10;
336 | end
337 |
338 | newBaseData._PartsType = opt.type;
339 | newBaseData._SliderFloatMin = opt.min;
340 | newBaseData._SliderFloatMax = opt.max;
341 | opt.baseData = newBaseData;
342 | SetOptStrings(opt);
343 | end
344 |
345 | unifiedData:call(".ctor", 0, nil, newBaseData);
346 |
347 | return unifiedData, newBaseData;
348 | end
349 |
350 | local function GetNewData(opt)
351 |
352 | local unifiedData = sdk.create_instance("snow.StmUnifiedOptionData", true):add_ref();
353 | local newData = sdk.create_instance("snow.StmOptionData", true):add_ref();
354 |
355 | if opt then
356 | newData._PartsType = opt.type;
357 | newData._MinSliderValue = opt.min;
358 | newData._MaxSliderValue = opt.max;
359 | newData._SelectNum = opt.max - 1;
360 |
361 | newData._SliderValue = opt.desiredValue;
362 | newData._OldSliderValue = opt.desiredValue;
363 | newData._SelectValue = opt.desiredValue;
364 | newData._OldSelectValue = opt.desiredValue;
365 | opt.data = newData;
366 | end
367 |
368 | unifiedData:call(".ctor", 0, nil, newData);
369 | return unifiedData, newData;
370 | end
371 |
372 |
373 |
374 | local function AddNewModOptionButton(mod)
375 |
376 | local unifiedBaseData, newBaseData = GetNewBaseData();
377 | local unifiedData, newData = GetNewData();
378 |
379 |
380 | mod.modNameSuid = SetBaseDataOptionName(newBaseData, mod.modName);
381 | SetBaseDataOptionMessage(newBaseData, mod.description);
382 |
383 | newData._SelectNum = 0;
384 | newBaseData._OptionItemName = OpenMenu_ARR;
385 | newBaseData._OptionItemSelectMessage = newBaseData._OptionItemName;
386 |
387 | modBaseDataList = AppendArray(modBaseDataList, sdk.typeof("snow.StmUnifiedOptionBaseData"), unifiedBaseData);
388 | modDataList = AppendArray(modDataList, sdk.typeof("snow.StmUnifiedOptionData"), unifiedData);
389 | end
390 |
391 | local function AddCreditsEntry()
392 |
393 |
394 | local unifiedBaseData, newBaseData = GetNewBaseData();
395 | local unifiedData, newData = GetNewData();
396 |
397 |
398 | SetBaseDataOptionName(newBaseData, "Created By: Bolt");
399 | SetBaseDataOptionMessage(newBaseData, "Hi, it's me.\nI made the mod menu ツ\nRemember to endorse the mods you like!");
400 |
401 | newBaseData._PartsType = WATCHITEM;
402 | newData._PartsType = WATCHITEM;
403 |
404 | newData._SelectNum = 0;
405 | newBaseData._OptionItemName = defaultSelMsgGuidArr;
406 | newBaseData._OptionItemSelectMessage = newBaseData._OptionItemName;
407 |
408 | modBaseDataList = AppendArray(modBaseDataList, sdk.typeof("snow.StmUnifiedOptionBaseData"), unifiedBaseData);
409 | modDataList = AppendArray(modDataList, sdk.typeof("snow.StmUnifiedOptionData"), unifiedData);
410 | end
411 |
412 | local function GetBackButtonData()
413 |
414 | local unifiedBaseData, newBaseData = GetNewBaseData();
415 | local unifiedData, newData = GetNewData();
416 |
417 | newBaseData:write_dword(OptionName_OFFSET, Back_SUID);
418 | newBaseData:write_dword(OptionMessage_OFFSET, Return_SUID);
419 |
420 | newData._SelectNum = 1;
421 | newData._SelectValue = 1;
422 | newBaseData._OptionItemName = Go_ARR;
423 | newBaseData._OptionItemSelectMessage = newBaseData._OptionItemName;
424 |
425 | return unifiedBaseData, unifiedData;
426 | end
427 |
428 |
429 | local function GetSelectedModIndex()
430 | return mainScrollList:get_SelectedIndex() + 1;
431 | end
432 |
433 | local function GetIsModsTabSelected()
434 | if not optionWindow then return false end
435 | return (optionWindow._scrL_TopMenu:get_SelectedIndex() == MOD_TAB_IDX) and optionWindow:isOpenOption();
436 | end
437 |
438 |
439 | local function CreateOptionDataArrays(mod)
440 |
441 | if mod.unifiedBaseArray then mod.unifiedBaseArray:force_release(); end
442 | if mod.unifiedArray then mod.unifiedArray:force_release(); end
443 |
444 | local count = mod.optionsCount + 1;
445 | local baseDataArray = sdk.create_managed_array(sdk.typeof("snow.StmUnifiedOptionBaseData"), count):add_ref_permanent();
446 | local dataArray = sdk.create_managed_array(sdk.typeof("snow.StmUnifiedOptionData"), count):add_ref_permanent();
447 |
448 |
449 | local backBaseData, backData = GetBackButtonData();
450 | baseDataArray[0] = backBaseData;
451 | dataArray[0] = backData;
452 |
453 |
454 | for idx, opt in ipairs(mod.optionsOrdered) do
455 | local unifiedBaseData, baseData = GetNewBaseData(opt);
456 | local unifiedData, data = GetNewData(opt);
457 | baseDataArray[idx] = unifiedBaseData;
458 | dataArray[idx] = unifiedData;
459 | end
460 |
461 |
462 |
463 | mod.unifiedBaseArray = baseDataArray;
464 | mod.unifiedArray = dataArray;
465 | mod.backBtnData = backData._StmOptionData;
466 | end
467 |
468 |
469 | local desiredSelectIdx = -1
470 | local desiredCursorIdx = -1;
471 | local desiredScrollIdx = -1;
472 | local function SetDesiredScrollIndexes(maintainIndex, itemCount)
473 | if maintainIndex then
474 | desiredSelectIdx = mainScrollList:get_SelectedIndex();
475 | desiredCursorIdx = mainScrollList:get_CursorIndex();
476 | desiredScrollIdx = mainScrollList:get_ScrollIndex();
477 |
478 | --more logic to clamp these values since the game will absolutely not hesitate to crash if any of this goes past the limit
479 | local maxDispItems = 10;
480 | local maxScroll = itemCount - maxDispItems;
481 | if maxScroll < 0 then maxScroll = 0; end
482 | if desiredScrollIdx > maxScroll then desiredScrollIdx = maxScroll; end
483 |
484 | if desiredSelectIdx >= itemCount then desiredSelectIdx = itemCount - 1; end
485 | if desiredCursorIdx >= maxDispItems then desiredCursorIdx = maxDispItems - 1; end
486 | else
487 | desiredSelectIdx = 0;
488 | desiredCursorIdx = 0;
489 | desiredScrollIdx = 0;
490 | end
491 | end
492 |
493 |
494 | local function UpdateScrollIndex(clear)
495 |
496 | if desiredScrollIdx < 0 then return end
497 |
498 | if optionWindow._State > 1 then
499 | --desiredScrollIdx is also used later and used to replace the scroll index on setOptionList bc of course it has to be used there too thats not confusing or anything
500 | --not sure if all of this is necessary or not but at least it makes sense now and works
501 | local menuCursor = optionWindow:get_OptionMenuListCursor();
502 | menuCursor:set_scrollIndex(desiredScrollIdx);
503 | menuCursor:set_cursorIndex(desiredCursorIdx);
504 | menuCursor:setIndex(desiredSelectIdx, true);
505 | optionWindow:updateOptionCursor(menuCursor, true);
506 | end
507 |
508 | if clear then
509 | --reset this so it doesnt overrite the value in setOptionList anymore
510 | desiredScrollIdx = -1;
511 | end
512 | end
513 |
514 |
515 | local function SwapOptionArray(toBaseArray, toDataArray, maintainCursorPos)
516 |
517 | SetDesiredScrollIndexes(maintainCursorPos, toBaseArray:get_size());
518 |
519 | ignoreSetSysMsg = true;
520 |
521 | SetUnifiedOptionArrays(SAVE_DATA_IDX, toBaseArray, toDataArray);
522 | optionWindow:setOpenOption(SAVE_DATA_IDX);
523 | --optionWindow:setOptionList(optionWindow._DataList, 0); --not sure if this is really necessary
524 |
525 | UpdateScrollIndex();
526 | end
527 |
528 |
529 |
530 |
531 | local needsRepaint = false;
532 | function _CModUiRepaint()
533 | needsRepaint = true;
534 | end
535 |
536 | local textType = sdk.find_type_definition("via.gui.Text");
537 | local function FindItemText(em)
538 |
539 | local next = em:get_Next();
540 |
541 | --prob a better way to iterate these but eh
542 | if next then
543 | if next:get_type_definition() == textType then
544 | next:set_Message(ModsListName_Str);
545 | else
546 | FindItemText(next);
547 | end
548 | end
549 |
550 | end
551 |
552 | --for whatever reason the top menu text doesnt seem to go through the same message ID stuff or something so I just did this instead /shrug
553 | local function ReplaceTopMenuText()
554 | local elements = optionWindow._scrL_TopMenu:get_Items();
555 | FindItemText(elements[MOD_TAB_IDX]:get_Child());
556 | end
557 |
558 |
559 | local colList;
560 | local function HandleCustomColors()
561 | if _CmodUiColors then
562 | for idx, col in ipairs(_CmodUiColors) do
563 | colList:Add(col);
564 | end
565 |
566 | _CmodUiColors = nil;
567 | end
568 | end
569 |
570 | local function InitCustomColors()
571 | --clear the custom colors from the list so we dont create duplicates
572 | colList = ColTagUserData:get_data(nil).DataList;
573 | colList.mSize = 3;
574 | end
575 |
576 | local function InitMods()
577 |
578 | end
579 |
580 |
581 | local function FirstOpen()
582 |
583 | log.debug("first open")
584 |
585 | defaultSelMsgGuidArr = CreateGuidArray(1, {""});
586 |
587 | --need to store this here so we can swap between arrays later
588 | mainBaseDataList, mainDataList = GetUnifiedOptionArrays(SAVE_DATA_IDX);
589 | mainBaseDataList:add_ref_permanent();
590 | mainDataList:add_ref_permanent();
591 |
592 | InitCustomColors();
593 |
594 | if not _CModUiList then _CModUiList = {}; end
595 |
596 | for idx, mod in ipairs(_CModUiList) do
597 |
598 | --pre run the callback once on init to pre fill the mod's optionsList
599 | --should be fine since the ui functions will simply return the initial values anyway
600 | _CModUiCurMod = mod;
601 |
602 | local guiResult, error = pcall(mod.guiCallback);
603 | if not guiResult then
604 | log.debug("ModGui Error in " .. mod.originalName .. ": " .. error);
605 | log.error("ModGui Error in " .. mod.originalName .. ": " .. error);
606 | mod.modName = "Error: " .. mod.originalName;
607 | mod.description = "This mod threw an error on initialization:\n" .. error;
608 | mod.optionsCount = 0;
609 | mod.optionsOrdered = {};
610 | mod.curOptIdx = 0;
611 | else
612 | mod.regenOptions = false;
613 | mod.optionsCount = mod.curOptIdx;
614 | end
615 |
616 |
617 | CreateOptionDataArrays(mod);
618 | AddNewModOptionButton(mod);
619 | end
620 |
621 | AddCreditsEntry();
622 |
623 | uiOpen = true;
624 | end
625 |
626 |
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 | -----------------------------------------------------------------------------------------------------------------------------------
635 | ----------------------------------------------------------HOOKS--------------------------------------------------------------------
636 | -----------------------------------------------------------------------------------------------------------------------------------
637 |
638 |
639 | local modMenuIsOpen = false;
640 |
641 | local function PreDef(args)
642 | end
643 | local function PostDef(retval)
644 | return retval;
645 | end
646 |
647 | local function PreOpt(args)
648 | --local str = args[3];
649 | --local type = sdk.to_int64(args[4]);
650 | --log.debug("Str: " .. sdk.to_managed_object(str):call("ToString()") .. " : " .. type);
651 |
652 | if ignoreSetSysMsg then
653 | ignoreSetSysMsg = false;
654 | return sdk.PreHookResult.SKIP_ORIGINAL;
655 | end
656 |
657 | if (sdk.to_int64(args[4]) == 40) and GetIsModsTabSelected() then
658 | if not modMenuIsOpen and optionWindow._State == 1 then
659 | args[3] = ModsListDesc_Ptr;
660 | end
661 | else
662 | modMenuIsOpen = false;
663 | end
664 | end
665 |
666 |
667 | local guidData1Field = guidType:get_field("mData1");
668 | local suidArg;
669 | local function PreMsg(args)
670 | suidArg = guidData1Field:get_data(args[2]);
671 | end
672 |
673 | local function PostMsg(retval)
674 |
675 | --log.debug(suidArg .. " : " .. sdk.to_managed_object(retval):call("ToString()"));
676 |
677 | local modString = modStrings[suidArg];
678 | if modString then
679 | return modString;
680 | end
681 |
682 | if suidArg == SAVE_DATA_SUID and GetIsModsTabSelected() then
683 | --log.debug("save data suid: " .. suidArg);
684 | if modMenuIsOpen and _CModUiCurMod then
685 | return modStrings[_CModUiCurMod.modNameSuid];
686 | else
687 | return ModsListName_Ptr;
688 | end
689 | end
690 |
691 |
692 | return retval;
693 | end
694 |
695 |
696 |
697 | local function PreSelect(args)
698 |
699 | if (not GetIsModsTabSelected()) then
700 | return;
701 | end
702 |
703 | if modMenuIsOpen then
704 |
705 | local pressIdx = optionWindow._scrL_MainOption:get_SelectedIndex();
706 | local mod = _CModUiCurMod;
707 |
708 | --back button is at index 0 so handle returning to main mod list
709 | if pressIdx == 0 then
710 | modMenuIsOpen = false;
711 | playSound(nil, uiConfirmSoundID);
712 | SwapOptionArray(modBaseDataList, modDataList);
713 | SaveData(); --force a config save when we exit a mod menu
714 | return sdk.PreHookResult.SKIP_ORIGINAL;
715 |
716 | elseif mod.optionsOrdered[pressIdx].isBtn then
717 | playSound(nil, uiConfirmSoundID);
718 | mod.optionsOrdered[pressIdx].value = true;
719 | return sdk.PreHookResult.SKIP_ORIGINAL;
720 | end
721 |
722 | --return if we clicked an option that wasnt a button
723 | return;
724 | end
725 |
726 |
727 | --go into a mod menu
728 | local selectedMod = _CModUiList[GetSelectedModIndex()];
729 | if not selectedMod then
730 | return;
731 | end
732 |
733 |
734 | _CModUiCurMod = selectedMod;
735 | modMenuIsOpen = true;
736 |
737 | --this prevents the message text showing the save data message if the cursor hovers a header after the swap operation, 40 is options segment
738 | SetSystemMessage(Return_Str);
739 | playSound(nil, uiConfirmSoundID);
740 |
741 | SwapOptionArray(selectedMod.unifiedBaseArray, selectedMod.unifiedArray);
742 |
743 |
744 |
745 | return sdk.PreHookResult.SKIP_ORIGINAL;
746 | end
747 |
748 | local function PreSkipIfOpen(args)
749 | if modMenuIsOpen then
750 | return sdk.PreHookResult.SKIP_ORIGINAL;
751 | end
752 | end
753 |
754 |
755 | local function PreInitTopMenu(args)
756 |
757 | log.debug("Mod Menu Init");
758 |
759 | SetOptionWindow();
760 | AddNewTopMenuCategory(sdk.to_managed_object(args[3]));
761 | topInitialized = true;
762 | end
763 |
764 | local function PostInitTopMenu(retval)
765 | ReplaceTopMenuText();
766 |
767 | if not uiOpen then
768 | FirstOpen();
769 | end
770 |
771 | return retval;
772 | end
773 |
774 |
775 | local function PreOptionChange(args)
776 |
777 | log.debug("pre opt: ".."");
778 |
779 | if GetIsModsTabSelected() then
780 | if displayedList ~= modBaseDataList and (not modMenuIsOpen) then
781 |
782 | --cant believe this worked but need to do a proper -reswap or else for some reason some of the data isnt fully reloaded
783 | --it feels kinda like its caching the list count somewhere before this so only the first item updates properly
784 | SwapOptionArray(modBaseDataList, modDataList);
785 | return sdk.PreHookResult.SKIP_ORIGINAL;
786 | end
787 | else
788 | modMenuIsOpen = false;
789 | SetUnifiedOptionArrays(SAVE_DATA_IDX, mainBaseDataList, mainDataList);
790 | end
791 |
792 | end
793 |
794 |
795 | local function PreSetList(args)
796 |
797 | --noooo idea why or whats going on but it seems snow.gui.GuiOptionWindow.changeOptionState no longer gets called after a game update so i guessssss this works toooo
798 | PreOptionChange();
799 |
800 | if desiredScrollIdx >= 0 then
801 | --need to override select index here
802 | args[4] = sdk.to_ptr(desiredScrollIdx);
803 | end
804 |
805 | --handle backing out of sub menu
806 | --2 is in the state of selecting settings
807 | if modMenuIsOpen and optionWindow._State == 1 then
808 | --i kinda cant believe this actually works
809 | --closes the mod menu but returns the state to selecting to emulate backing out of the sub menu
810 | optionWindow._State = 2;
811 | modMenuIsOpen = false;
812 | SwapOptionArray(modBaseDataList, modDataList);
813 | SetSystemMessage(_CModUiList[1].description);
814 | SaveData(); --force a config save when we exit a mod menu
815 | return sdk.PreHookResult.SKIP_ORIGINAL;
816 | end
817 | end
818 |
819 | local function PostSwitchState(retval)
820 | UpdateSelectedIdx();
821 | return retval;
822 | end
823 |
824 |
825 | local ignoreJmp = true;
826 |
827 | sdk.hook(sdk.find_type_definition("snow.gui.GuiCommonMessageWindow"):get_method("setSystemMessageText(System.String, snow.gui.SnowGuiCommonUtility.Segment)"), PreOpt, PostDef, ignoreJmp);
828 | --sdk.hook(sdk.find_type_definition("snow.gui.StmGuiInput"):get_method("convertIconTag_replaceOptionId(via.gui.Text, System.Guid)"), PreReplace, PostDef, ignoreJmp);
829 | sdk.hook(sdk.find_type_definition("snow.gui.StmGuiInput"):get_method("convertIconTag_replaceOptionId(System.Guid)"), PreMsg, PostMsg, ignoreJmp);
830 |
831 | local optionWindowType = sdk.find_type_definition("snow.gui.GuiOptionWindow");
832 | sdk.hook(optionWindowType:get_method("ItemSelectDecideAction()"), PreSelect, PostDef, ignoreJmp);
833 | sdk.hook(optionWindowType:get_method("setOpenOptionWindow(System.Collections.Generic.List`1, snow.gui.GuiOptionWindow._void_OptionFunction, snow.gui.SnowGuiCommonUtility.Segment, System.Boolean)"), PreInitTopMenu, PostDef, ignoreJmp); --what a mouthfull
834 | sdk.hook(optionWindowType:get_method("initTopMenu"), PreDef, PostInitTopMenu, ignoreJmp);
835 | -- sdk.hook(optionWindowType:get_method("changeOptionState(snow.gui.GuiOptionWindow.OptionState)"), PreOptionChange, PostDef, ignoreJmp);
836 | sdk.hook(optionWindowType:get_method("setOptionList(System.Collections.Generic.List`1, System.Int32)"), PreSetList, PostDef, ignoreJmp);
837 | --ItemSelectDecideAction
838 | --updateSelectValueSelect
839 | --updateCategorySelect()
840 | --changeOptionState(snow.gui.GuiOptionWindow.OptionState)
841 |
842 |
843 |
844 |
845 |
846 |
847 | -----------------------------------------------------------------------------------------------------------------------------------
848 | ----------------------------------------------------------HANDLE GUI--------------------------------------------------------------------
849 | -----------------------------------------------------------------------------------------------------------------------------------
850 |
851 | local function RegenModOpts(mod)
852 |
853 | if optionWindow._State > 2 then
854 | --prevent options from being regenerated while user is editing something
855 | return;
856 | end
857 |
858 | mod.optionsOrdered = {};
859 | mod.curOptIdx = 0;
860 | mod.indent = 0;
861 | mod.guiCallback();
862 | mod.optionsCount = mod.curOptIdx;
863 | mod.regenOptions = false;
864 |
865 | CreateOptionDataArrays(mod);
866 | SwapOptionArray(mod.unifiedBaseArray, mod.unifiedArray, true);
867 | return sdk.PreHookResult.SKIP_ORIGINAL;
868 | end
869 |
870 | local function Options(mod)
871 |
872 |
873 | if mod.regenOptions then
874 | return RegenModOpts(mod);
875 | end
876 |
877 | local wasReset = false;
878 |
879 | --this is a really goofy way of detecting if the options were reset but there wasnt a function to hook for it
880 | --so this is a clever way i think
881 | if mod.backBtnData._SelectValue == 0 then
882 |
883 | mod.backBtnData._SelectValue = 1;
884 |
885 | if _CModUiCurMod.OnResetAllSettings then
886 | _CModUiCurMod.OnResetAllSettings();
887 | end
888 |
889 | for idx, opt in ipairs(mod.optionsOrdered) do
890 | opt.wasChanged = false;
891 | end
892 |
893 | mod.guiCallback();
894 |
895 | needsRepaint = true;
896 | wasReset = true;
897 | end
898 |
899 |
900 | for idx, opt in ipairs(mod.optionsOrdered) do
901 |
902 | local data = opt.data;
903 |
904 | if wasReset then
905 | opt.value = opt.desiredValue;
906 | data._SelectValue = opt.desiredValue;
907 | data._SliderValue = opt.desiredValue;
908 | data._OldSliderValue = opt.desiredValue;
909 | data._OldSelectValue = opt.desiredValue;
910 | opt.wasChanged = true;
911 |
912 | if opt.isBtn then
913 | opt.value = false;
914 | end
915 |
916 | elseif opt.type == SLIDER then
917 |
918 | local checkValue = opt.immediate and data._SliderValue or data._OldSliderValue;
919 | if checkValue ~= opt.value then
920 | opt.value = data._SliderValue;
921 | data._OldSliderValue = opt.value;
922 | opt.desiredValue = opt.value;
923 | opt.wasChanged = true;
924 |
925 | elseif opt.value ~= opt.desiredValue then
926 | data._OldSliderValue = opt.desiredValue;
927 | data._SliderValue = opt.desiredValue;
928 | opt.value = opt.desiredValue;
929 | opt.wasChanged = true;
930 | end
931 |
932 | elseif opt.type == ENUM and not opt.isBtn then
933 |
934 | local checkValue = opt.immediate and data._SelectValue or data._OldSelectValue;
935 | if checkValue ~= opt.value then
936 | opt.value = data._SelectValue;
937 | data._OldSelectValue = opt.value;
938 | opt.desiredValue = opt.value;
939 | opt.wasChanged = true;
940 |
941 | elseif opt.value ~= opt.desiredValue then
942 | data._OldSelectValue = opt.desiredValue;
943 | data._SelectValue = opt.desiredValue;
944 | opt.value = opt.desiredValue;
945 | opt.wasChanged = true;
946 | end
947 | end
948 |
949 | end
950 |
951 | mod.UpdateGui();
952 | end
953 |
954 |
955 | local function PreOptWindowUpdate(args)
956 |
957 | if not optionWindow then
958 | SetOptionWindow();
959 | end
960 |
961 | if not uiOpen then
962 | FirstOpen();
963 | uiOpen = true;
964 |
965 | if GetIsModsTabSelected() then
966 | SwapOptionArray(modBaseDataList, modDataList);
967 | return sdk.PreHookResult.SKIP_ORIGINAL;
968 | end
969 | end
970 |
971 | if _CModUiPromptCoRo then
972 | if not coroutine.resume(_CModUiPromptCoRo) then
973 | _CModUiPromptCoRo = nil;
974 | else
975 | return sdk.PreHookResult.SKIP_ORIGINAL;
976 | end
977 | end
978 |
979 | local mod = _CModUiCurMod;
980 | if not mod then
981 | return;
982 | end
983 |
984 | if needsRepaint then
985 | needsRepaint = false;
986 | SwapOptionArray(mod.unifiedBaseArray, mod.unifiedArray, true);
987 | return sdk.PreHookResult.SKIP_ORIGINAL;
988 | end
989 |
990 | HandleCustomColors();
991 | UpdateScrollIndex(true);
992 |
993 | if modMenuIsOpen then
994 | return Options(mod);
995 | end
996 | end
997 |
998 |
999 | sdk.hook(optionWindowType:get_method("updateOptionOperation()"), PreOptWindowUpdate, PostDef, ignoreJmp);
1000 |
1001 |
1002 |
1003 | re.on_script_reset(function()
1004 |
1005 | if mainBaseDataList then
1006 | SetUnifiedOptionArrays(SAVE_DATA_IDX, mainBaseDataList, mainDataList);
1007 | end
1008 |
1009 | end)
1010 |
1011 |
1012 |
1013 |
1014 |
1015 |
1016 |
1017 |
1018 |
1019 |
1020 |
1021 |
1022 |
1023 |
1024 |
1025 |
1026 |
1027 |
1028 |
1029 |
1030 |
1031 |
1032 |
1033 |
1034 |
1035 |
1036 |
1037 |
1038 |
1039 |
1040 |
1041 |
1042 |
1043 |
1044 |
1045 |
1046 |
1047 |
1048 |
1049 |
1050 |
1051 |
1052 |
1053 |
1054 |
1055 |
1056 |
1057 |
1058 |
1059 |
1060 |
1061 |
--------------------------------------------------------------------------------