├── .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 | --------------------------------------------------------------------------------