├── .editorconfig ├── .gitignore ├── LICENSE ├── ModMenu.sln ├── ModMenu ├── Helpers.cs ├── Info.json ├── Main.cs ├── ModMenu.cs ├── ModMenu.csproj ├── NewTypes │ ├── ISettingsChanged.cs │ ├── SettingsEntityButton.cs │ ├── SettingsEntityCollapsibleHeader.cs │ ├── SettingsEntityDropdownButton.cs │ ├── SettingsEntityImage.cs │ ├── SettingsEntityPatches.cs │ └── SettingsEntitySubHeader.cs ├── Settings │ ├── HeaderFix.cs │ ├── KeyBinding.cs │ ├── ModsMenuEntity.cs │ ├── SettingsBuilder.cs │ └── TestSettings.cs └── WittleWolfie.png ├── README.md ├── more_settings.png └── test_settings.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_size = 2 11 | indent_style = space 12 | tab_width = 2 13 | 14 | # New line preferences 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET Coding Conventions #### 19 | 20 | # Organize usings 21 | dotnet_separate_import_directive_groups = false 22 | dotnet_sort_system_directives_first = false 23 | file_header_template = unset 24 | 25 | # this. and Me. preferences 26 | dotnet_style_qualification_for_event = false 27 | dotnet_style_qualification_for_field = false 28 | dotnet_style_qualification_for_method = false 29 | dotnet_style_qualification_for_property = false 30 | 31 | # Language keywords vs BCL types preferences 32 | dotnet_style_predefined_type_for_locals_parameters_members = true 33 | dotnet_style_predefined_type_for_member_access = true 34 | 35 | # Parentheses preferences 36 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity 37 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity 38 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary 39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity 40 | 41 | # Modifier preferences 42 | dotnet_style_require_accessibility_modifiers = for_non_interface_members 43 | 44 | # Expression-level preferences 45 | dotnet_style_coalesce_expression = true 46 | dotnet_style_collection_initializer = true 47 | dotnet_style_explicit_tuple_names = true 48 | dotnet_style_namespace_match_folder = true 49 | dotnet_style_null_propagation = true 50 | dotnet_style_object_initializer = true 51 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 52 | dotnet_style_prefer_auto_properties = true 53 | dotnet_style_prefer_compound_assignment = true 54 | dotnet_style_prefer_conditional_expression_over_assignment = true 55 | dotnet_style_prefer_conditional_expression_over_return = true 56 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed 57 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 58 | dotnet_style_prefer_inferred_tuple_names = true 59 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true 60 | dotnet_style_prefer_simplified_boolean_expressions = true 61 | dotnet_style_prefer_simplified_interpolation = true 62 | 63 | # Field preferences 64 | dotnet_style_readonly_field = true 65 | 66 | # Parameter preferences 67 | dotnet_code_quality_unused_parameters = all 68 | 69 | # Suppression preferences 70 | dotnet_remove_unnecessary_suppression_exclusions = none 71 | 72 | # New line preferences 73 | dotnet_style_allow_multiple_blank_lines_experimental = false 74 | dotnet_style_allow_statement_immediately_after_block_experimental = true 75 | 76 | #### C# Coding Conventions #### 77 | 78 | # var preferences 79 | csharp_style_var_elsewhere = true 80 | csharp_style_var_for_built_in_types = true 81 | csharp_style_var_when_type_is_apparent = true 82 | 83 | # Expression-bodied members 84 | csharp_style_expression_bodied_accessors = true 85 | csharp_style_expression_bodied_constructors = false 86 | csharp_style_expression_bodied_indexers = true 87 | csharp_style_expression_bodied_lambdas = true 88 | csharp_style_expression_bodied_local_functions = false 89 | csharp_style_expression_bodied_methods = false 90 | csharp_style_expression_bodied_operators = false 91 | csharp_style_expression_bodied_properties = true 92 | 93 | # Pattern matching preferences 94 | csharp_style_pattern_matching_over_as_with_null_check = true 95 | csharp_style_pattern_matching_over_is_with_cast_check = true 96 | csharp_style_prefer_extended_property_pattern = true 97 | csharp_style_prefer_not_pattern = true 98 | csharp_style_prefer_pattern_matching = true 99 | csharp_style_prefer_switch_expression = true 100 | 101 | # Null-checking preferences 102 | csharp_style_conditional_delegate_call = true 103 | 104 | # Modifier preferences 105 | csharp_prefer_static_local_function = true 106 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async 107 | 108 | # Code-block preferences 109 | csharp_prefer_braces = true 110 | csharp_prefer_simple_using_statement = false 111 | csharp_style_namespace_declarations = block_scoped 112 | csharp_style_prefer_method_group_conversion = true 113 | csharp_style_prefer_top_level_statements = true 114 | 115 | # Expression-level preferences 116 | csharp_prefer_simple_default_expression = true 117 | csharp_style_deconstructed_variable_declaration = true 118 | csharp_style_implicit_object_creation_when_type_is_apparent = true 119 | csharp_style_inlined_variable_declaration = true 120 | csharp_style_prefer_index_operator = true 121 | csharp_style_prefer_local_over_anonymous_function = true 122 | csharp_style_prefer_null_check_over_type_check = true 123 | csharp_style_prefer_range_operator = true 124 | csharp_style_prefer_tuple_swap = true 125 | csharp_style_prefer_utf8_string_literals = true 126 | csharp_style_throw_expression = true 127 | csharp_style_unused_value_assignment_preference = discard_variable 128 | csharp_style_unused_value_expression_statement_preference = discard_variable 129 | 130 | # 'using' directive preferences 131 | csharp_using_directive_placement = outside_namespace 132 | 133 | # New line preferences 134 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true 135 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false 136 | csharp_style_allow_embedded_statements_on_same_line_experimental = true 137 | 138 | #### C# Formatting Rules #### 139 | 140 | # New line preferences 141 | csharp_new_line_before_catch = true 142 | csharp_new_line_before_else = true 143 | csharp_new_line_before_finally = true 144 | csharp_new_line_before_members_in_anonymous_types = true 145 | csharp_new_line_before_members_in_object_initializers = true 146 | csharp_new_line_before_open_brace = all 147 | csharp_new_line_between_query_expression_clauses = true 148 | 149 | # Indentation preferences 150 | csharp_indent_block_contents = true 151 | csharp_indent_braces = false 152 | csharp_indent_case_contents = true 153 | csharp_indent_case_contents_when_block = true 154 | csharp_indent_labels = one_less_than_current 155 | csharp_indent_switch_labels = true 156 | 157 | # Space preferences 158 | csharp_space_after_cast = false 159 | csharp_space_after_colon_in_inheritance_clause = true 160 | csharp_space_after_comma = true 161 | csharp_space_after_dot = false 162 | csharp_space_after_keywords_in_control_flow_statements = true 163 | csharp_space_after_semicolon_in_for_statement = true 164 | csharp_space_around_binary_operators = before_and_after 165 | csharp_space_around_declaration_statements = false 166 | csharp_space_before_colon_in_inheritance_clause = true 167 | csharp_space_before_comma = false 168 | csharp_space_before_dot = false 169 | csharp_space_before_open_square_brackets = false 170 | csharp_space_before_semicolon_in_for_statement = false 171 | csharp_space_between_empty_square_brackets = false 172 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 173 | csharp_space_between_method_call_name_and_opening_parenthesis = false 174 | csharp_space_between_method_call_parameter_list_parentheses = false 175 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 176 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 177 | csharp_space_between_method_declaration_parameter_list_parentheses = false 178 | csharp_space_between_parentheses = false 179 | csharp_space_between_square_brackets = false 180 | 181 | # Wrapping preferences 182 | csharp_preserve_single_line_blocks = true 183 | csharp_preserve_single_line_statements = true 184 | 185 | #### Naming styles #### 186 | 187 | # Naming rules 188 | 189 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 190 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 191 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 192 | 193 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 194 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 195 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 196 | 197 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 198 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 199 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 200 | 201 | # Symbol specifications 202 | 203 | dotnet_naming_symbols.interface.applicable_kinds = interface 204 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 205 | dotnet_naming_symbols.interface.required_modifiers = 206 | 207 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 208 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 209 | dotnet_naming_symbols.types.required_modifiers = 210 | 211 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 212 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 213 | dotnet_naming_symbols.non_field_members.required_modifiers = 214 | 215 | # Naming styles 216 | 217 | dotnet_naming_style.pascal_case.required_prefix = 218 | dotnet_naming_style.pascal_case.required_suffix = 219 | dotnet_naming_style.pascal_case.word_separator = 220 | dotnet_naming_style.pascal_case.capitalization = pascal_case 221 | 222 | dotnet_naming_style.begins_with_i.required_prefix = I 223 | dotnet_naming_style.begins_with_i.required_suffix = 224 | dotnet_naming_style.begins_with_i.word_separator = 225 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 226 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | # Local Files 353 | lib/** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 WittleWolfie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ModMenu.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32811.315 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModMenu", "ModMenu\ModMenu.csproj", "{583CA9AD-39F0-4245-8FA1-B9F591B51FCC}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{53B2EE73-3351-4DAC-A5C1-134CC761E7BD}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitignore = .gitignore 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {583CA9AD-39F0-4245-8FA1-B9F591B51FCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {583CA9AD-39F0-4245-8FA1-B9F591B51FCC}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {583CA9AD-39F0-4245-8FA1-B9F591B51FCC}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {583CA9AD-39F0-4245-8FA1-B9F591B51FCC}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {564B862C-5D65-4928-8993-2344812BBD9B} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /ModMenu/Helpers.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker; 3 | using Kingmaker.Localization; 4 | using Kingmaker.Localization.Shared; 5 | using Kingmaker.UI.MVVM._PCView.Settings.Entities; 6 | using Kingmaker.UI.MVVM._PCView.Settings; 7 | using Kingmaker.UI.MVVM._VM.Settings.Entities; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Reflection; 12 | using UnityEngine; 13 | 14 | namespace ModMenu 15 | { 16 | /// 17 | /// Generic utils for simple operations. 18 | /// 19 | internal static class Helpers 20 | { 21 | private static readonly List Strings = new(); 22 | 23 | internal static LocalizedString CreateString(string key, string enGB, string ruRU = "") 24 | { 25 | var localString = new LocalString(key, enGB, ruRU); 26 | Strings.Add(localString); 27 | localString.Register(); 28 | return localString.LocalizedString; 29 | } 30 | 31 | internal static Sprite CreateSprite(string embeddedImage) 32 | { 33 | var assembly = Assembly.GetExecutingAssembly(); 34 | using var stream = assembly.GetManifestResourceStream(embeddedImage); 35 | byte[] bytes = new byte[stream.Length]; 36 | stream.Read(bytes, 0, bytes.Length); 37 | var texture = new Texture2D(128, 128, TextureFormat.RGBA32, false); 38 | _ = texture.LoadImage(bytes); 39 | var sprite = Sprite.Create(texture, new(0, 0, texture.width, texture.height), Vector2.zero); 40 | return sprite; 41 | } 42 | 43 | private class LocalString 44 | { 45 | public readonly LocalizedString LocalizedString; 46 | private readonly string enGB; 47 | private readonly string ruRU; 48 | 49 | public LocalString(string key, string enGB, string ruRU) 50 | { 51 | LocalizedString = new LocalizedString() { m_Key = key }; 52 | this.enGB = enGB; 53 | this.ruRU = ruRU; 54 | } 55 | 56 | public void Register() 57 | { 58 | var localized = enGB; 59 | switch (LocalizationManager.CurrentPack.Locale) 60 | { 61 | case Locale.ruRU: 62 | if (!string.IsNullOrEmpty(ruRU)) 63 | localized = ruRU; 64 | break; 65 | } 66 | LocalizationManager.CurrentPack.PutString(LocalizedString.m_Key, localized); 67 | } 68 | } 69 | 70 | /// 71 | /// Updated the Description field of a setting that is set with WithLongDescription(). The update works by finding the Title of the setting. 72 | /// Titles that you wish to update must be unique inorder to update the correct setting. 73 | /// 74 | /// 75 | /// This needs to be a class of the type that inherits from SettingsEntityWithValueVM that you wish to update. Such as if you are updating a slider the typeparam 76 | /// must be SettingsEntitySliderVM 77 | /// 78 | public class SettingsDescriptionUpdater 79 | where T : SettingsEntityWithValueVM 80 | { 81 | private readonly string pathMainUi; 82 | private readonly string pathDescriptionUi; 83 | 84 | private Transform mainUI; 85 | private Transform settingsUI; 86 | private Transform descriptionUI; 87 | 88 | private List> settingViews; 89 | private SettingsDescriptionPCView descriptionView; 90 | 91 | /// 92 | /// Expected path as of 2.1.5r 93 | /// 94 | public const string PATH_MAIN_UI = "Canvas/SettingsView/ContentWrapper/VirtualListVertical/Viewport/Content"; 95 | 96 | /// 97 | /// Expected path as of 2.1.5r 98 | /// 99 | public const string PATH_DESCRIPTION_UI = "Canvas/SettingsView/ContentWrapper/DescriptionView"; 100 | 101 | /// 102 | /// Constuctor for SettingsDescriptionUpdater. Sets up the paths for where the UI gameobjects at located 103 | /// 104 | /// 105 | /// Optional. This is the path to main UI where setting GameOjects are located are located. 106 | /// Defaults to PATH_MAIN_UI which should work in 2.1.5r. 107 | /// 108 | /// 109 | /// This is the path to the Description UI where the Description GameOject SettingsDescriptionPCView is located. 110 | /// Defaults to PATH_DESCRIPTION_UI which should work in 2.1.5r. 111 | /// 112 | public SettingsDescriptionUpdater(string pathMainUI = PATH_MAIN_UI, string pathDesriptionUI = PATH_DESCRIPTION_UI) 113 | { 114 | pathMainUi = pathMainUI; 115 | pathDescriptionUi = pathDesriptionUI; 116 | } 117 | 118 | private bool Ensure() 119 | { 120 | // UI tends to change frequently, ensure that eveything is up to date. 121 | 122 | if ((mainUI = Game.Instance.RootUiContext.m_CommonView.transform) == null) 123 | return false; 124 | 125 | settingsUI = mainUI.Find(pathMainUi); 126 | descriptionUI = mainUI.Find(pathDescriptionUi); 127 | if (settingsUI == null || descriptionUI == null) 128 | return false; 129 | 130 | settingViews = settingsUI.gameObject.GetComponentsInChildren>().ToList(); 131 | descriptionView = descriptionUI.GetComponent(); 132 | 133 | if (settingViews == null || descriptionView == null || settingViews.Count == 0) 134 | return false; 135 | 136 | return true; 137 | } 138 | 139 | /// 140 | /// This is the method that updates the Description of the SettingsEntityWithValueVM 141 | /// 142 | /// 143 | /// This is the UNIQUE Title of the setting that you wish to edit. If the Title is 144 | /// not unique then the incorrect setting may be updated. 145 | /// 146 | /// 147 | /// The text you wish to set the Description to. 148 | /// 149 | /// 150 | /// Will return true if the update was successfull. 151 | /// 152 | 153 | public bool TryUpdate(string title, string description) 154 | { 155 | if (!Ensure()) return false; 156 | 157 | T svm = null; 158 | 159 | foreach (var settingView in settingViews) 160 | { 161 | var test = (T)settingView.GetViewModel(); 162 | if (test.Title.Equals(title)) 163 | { 164 | svm = test; 165 | break; 166 | } 167 | } 168 | 169 | if (svm == null) 170 | return false; 171 | 172 | svm.GetType().GetField("Description").SetValue(svm, description); 173 | 174 | descriptionView.m_DescriptionText.text = description; 175 | 176 | return true; 177 | } 178 | } 179 | 180 | 181 | [HarmonyPatch(typeof(LocalizationManager))] 182 | static class LocalizationManager_Patch 183 | { 184 | [HarmonyPatch(nameof(LocalizationManager.OnLocaleChanged)), HarmonyPostfix] 185 | static void Postfix() 186 | { 187 | try 188 | { 189 | Strings.ForEach(str => str.Register()); 190 | } 191 | catch (Exception e) 192 | { 193 | Main.Logger.LogException("Failed to handle locale change.", e); 194 | } 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /ModMenu/Info.json: -------------------------------------------------------------------------------- 1 | { 2 | "Id": "ModMenu", 3 | "DisplayName": "ModMenu", 4 | "Author": "WittleWolfie", 5 | "Version": "1.3.2", 6 | "ManagerVersion": "0.21.3", 7 | "AssemblyName": "ModMenu.dll", 8 | "EntryMethod": "ModMenu.Main.Load", 9 | "Requirements": [], 10 | "LoadAfter": [ "MewsiferConsole.Mod" ] 11 | } 12 | -------------------------------------------------------------------------------- /ModMenu/Main.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.Blueprints.JsonSystem; 3 | using ModMenu.Settings; 4 | using System; 5 | using static UnityModManagerNet.UnityModManager; 6 | using static UnityModManagerNet.UnityModManager.ModEntry; 7 | 8 | namespace ModMenu 9 | { 10 | public static class Main 11 | { 12 | internal static ModLogger Logger; 13 | private static Harmony Harmony; 14 | 15 | public static bool Load(ModEntry modEntry) 16 | { 17 | try 18 | { 19 | Logger = modEntry.Logger; 20 | modEntry.OnUnload = OnUnload; 21 | 22 | Harmony = new(modEntry.Info.Id); 23 | Harmony.PatchAll(); 24 | 25 | Logger.Log("Finished loading."); 26 | } 27 | catch (Exception e) 28 | { 29 | Logger.LogException(e); 30 | return false; 31 | } 32 | return true; 33 | } 34 | 35 | private static bool OnUnload(ModEntry modEntry) 36 | { 37 | Logger.Log("Unloading."); 38 | Harmony?.UnpatchAll(); 39 | return true; 40 | } 41 | 42 | #if DEBUG 43 | [HarmonyPatch(typeof(BlueprintsCache))] 44 | static class BlueprintsCache_Patches 45 | { 46 | [HarmonyPriority(Priority.First)] 47 | [HarmonyPatch(nameof(BlueprintsCache.Init)), HarmonyPostfix] 48 | static void Postfix() 49 | { 50 | try 51 | { 52 | new TestSettings().Initialize(); 53 | } 54 | catch (Exception e) 55 | { 56 | Logger.LogException("BlueprintsCache.Init", e); 57 | } 58 | } 59 | } 60 | #endif 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ModMenu/ModMenu.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.Settings; 2 | using Kingmaker.UI.SettingsUI; 3 | using ModMenu.Settings; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace ModMenu 8 | { 9 | /// 10 | /// API mods can use to add settings to the Mods menu page. 11 | /// 12 | public static class ModMenu 13 | { 14 | /// 15 | /// Stores all settings entities in the mod menu. 16 | /// 17 | private readonly static Dictionary Settings = new(); 18 | 19 | /// 20 | /// Adds a new group of settings to the Mods menu page. 21 | /// 22 | /// 23 | /// 24 | /// Thrown when contains a setting with a key that already exists. 25 | /// 26 | public static void AddSettings(SettingsBuilder settings) 27 | { 28 | var settingsGroup = settings.Build(); 29 | foreach (var setting in settingsGroup.settings) 30 | { 31 | if (Settings.ContainsKey(setting.Key)) 32 | { 33 | throw new ArgumentException( 34 | $"Attempt to add settings failed: a setting with key {setting.Key} already exists."); 35 | } 36 | Settings.Add(setting.Key, setting.Value); 37 | } 38 | ModsMenuEntity.Add(settingsGroup.group); 39 | } 40 | 41 | /// 42 | /// Adds a new group of settings to the Mods menu page. 43 | /// 44 | /// 45 | /// 46 | /// 47 | /// Using is recommended. If you prefer to construct the settings 48 | /// on your own you can use this method. 49 | /// 50 | /// 51 | /// 52 | /// Settings added in this way cannot be retrieved using or 53 | /// . 54 | /// 55 | /// 56 | public static void AddSettings(UISettingsGroup settingsGroup) 57 | { 58 | ModsMenuEntity.Add(settingsGroup); 59 | } 60 | 61 | /// 62 | /// The setting with the specified , or null if it does not exist or has the wrong type. 63 | /// 64 | public static T GetSetting(string key) where T : SettingsEntity 65 | { 66 | if (!Settings.ContainsKey(key)) 67 | { 68 | Main.Logger.Error($"No setting found with key {key}"); 69 | return null; 70 | } 71 | 72 | var setting = Settings[key] as T; 73 | if (setting is null) 74 | { 75 | Main.Logger.Error($"Type mismatch. Setting {key} is a {Settings[key].GetType()}, but {typeof(T)} was expected."); 76 | return null; 77 | } 78 | return setting; 79 | } 80 | 81 | /// 82 | /// The value of the setting with the specified , or default if it does not exist or 83 | /// has the wrong type. 84 | /// 85 | public static T GetSettingValue(string key) 86 | { 87 | var setting = GetSetting, T>(key); 88 | return setting is null ? default : setting.GetValue(); 89 | } 90 | 91 | /// 92 | /// Attempts to set the value of a setting. 93 | /// 94 | /// 95 | /// Added in v1.1.0 96 | /// 97 | /// True if the setting was set, false otherwise. 98 | public static bool SetSetting(string key, TValue value) where T : SettingsEntity 99 | { 100 | Main.Logger.Log($"Attempting to set {key} to {value}"); 101 | var setting = GetSetting(key); 102 | if (setting is null) 103 | return false; 104 | 105 | setting.SetValueAndConfirm(value); 106 | return true; 107 | } 108 | 109 | /// 110 | /// Convenience method for with 111 | /// <SettingsEntityBool, bool>. 112 | /// 113 | /// 114 | /// 115 | public static bool SetSetting(string key, bool value) 116 | { 117 | return SetSetting(key, value); 118 | } 119 | 120 | /// 121 | /// Convenience method for with 122 | /// <SettingsEntityEnum<T>, T>. 123 | /// 124 | /// 125 | /// 126 | public static bool SetSetting(string key, T value) where T : Enum 127 | { 128 | return SetSetting, T>(key, value); 129 | } 130 | 131 | /// 132 | /// Convenience method for with 133 | /// <SettingsEntityFloat, float>. 134 | /// 135 | /// 136 | /// 137 | public static bool SetSetting(string key, float value) 138 | { 139 | return SetSetting(key, value); 140 | } 141 | 142 | /// 143 | /// Convenience method for with 144 | /// <SettingsEntityInt, int>. 145 | /// 146 | /// 147 | /// 148 | public static bool SetSetting(string key, int value) 149 | { 150 | return SetSetting(key, value); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /ModMenu/ModMenu.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 8 | latest 9 | 10 | net481 11 | 12 | 13 | true 14 | 15 | ModMenu 16 | ModMenu 17 | 18 | True 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | all 41 | runtime; build; native; contentfiles; analyzers; buildtransitive 42 | 43 | 44 | 45 | 46 | 47 | 48 | $(SolutionDir)lib\Assembly-CSharp.dll 49 | 50 | 51 | 52 | 53 | $(WrathPath)\Wrath_Data\Managed\Assembly-CSharp-firstpass.dll 54 | 55 | 56 | $(WrathPath)\Wrath_Data\Managed\Owlcat.Runtime.UI.dll 57 | 58 | 59 | $(WrathPath)\Wrath_Data\Managed\Owlcat.Runtime.Validation.dll 60 | 61 | 62 | $(WrathPath)\Wrath_Data\Managed\Unity.TextMeshPro.dll 63 | 64 | 65 | $(WrathPath)\Wrath_Data\Managed\UnityEngine.CoreModule.dll 66 | 67 | 68 | $(WrathPath)\Wrath_Data\Managed\UnityEngine.ImageConversionModule.dll 69 | 70 | 71 | $(WrathPath)\Wrath_Data\Managed\UnityEngine.UI.dll 72 | 73 | 74 | $(WrathPath)\Wrath_Data\Managed\UniRx.dll 75 | 76 | 77 | 78 | 79 | $(WrathPath)\Wrath_Data\Managed\0Harmony.dll 80 | 81 | 82 | $(WrathPath)\Wrath_Data\Managed\UnityModManager\UnityModManager.dll 83 | 84 | 85 | 86 | 87 | 88 | PreserveNewest 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /ModMenu/NewTypes/ISettingsChanged.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.PubSubSystem; 2 | 3 | namespace ModMenu.NewTypes 4 | { 5 | /// 6 | /// Event interface that can be subscribed to on the EventBus that handles when Apply button is pressed in the Settings UI. 7 | /// 8 | /// 9 | /// Your class must subscribe to the EventBus for this to trigger via EventBus.Subscribe(object subscriber) 10 | /// 11 | public interface ISettingsChanged : ISubscriber, IGlobalSubscriber 12 | { 13 | /// 14 | /// Method triggered when with SettingsVM.ApplySettings() is called. 15 | /// 16 | void HandleApplySettings(); 17 | } 18 | } -------------------------------------------------------------------------------- /ModMenu/NewTypes/SettingsEntityButton.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.Localization; 2 | using Kingmaker.PubSubSystem; 3 | using Kingmaker.UI.MVVM._PCView.Settings.Entities; 4 | using Kingmaker.UI.MVVM._VM.Settings.Entities; 5 | using Kingmaker.UI.SettingsUI; 6 | using Owlcat.Runtime.UI.Controls.Button; 7 | using Owlcat.Runtime.UI.VirtualListSystem.ElementSettings; 8 | using System; 9 | using TMPro; 10 | using UnityEngine; 11 | using UnityEngine.EventSystems; 12 | using UnityEngine.UI; 13 | 14 | namespace ModMenu.NewTypes 15 | { 16 | public class UISettingsEntityButton : UISettingsEntityBase 17 | { 18 | internal LocalizedString ButtonText; 19 | internal Action OnClick; 20 | 21 | internal static UISettingsEntityButton Create( 22 | LocalizedString description, LocalizedString longDescription, LocalizedString buttonText, Action onClick) 23 | { 24 | var button = ScriptableObject.CreateInstance(); 25 | button.m_Description = description; 26 | button.m_TooltipDescription = longDescription; 27 | 28 | button.ButtonText = buttonText; 29 | button.OnClick = onClick; 30 | return button; 31 | } 32 | 33 | public override SettingsListItemType? Type => SettingsListItemType.Custom; 34 | } 35 | 36 | internal class SettingsEntityButtonVM : SettingsEntityVM 37 | { 38 | private readonly UISettingsEntityButton buttonEntity; 39 | 40 | public string Text => buttonEntity.ButtonText; 41 | 42 | internal SettingsEntityButtonVM(UISettingsEntityButton buttonEntity) : base(buttonEntity) 43 | { 44 | this.buttonEntity = buttonEntity; 45 | } 46 | 47 | public void PerformClick() 48 | { 49 | buttonEntity.OnClick?.Invoke(); 50 | } 51 | } 52 | 53 | internal class SettingsEntityButtonView 54 | : SettingsEntityView, IPointerEnterHandler, IPointerExitHandler 55 | { 56 | public override VirtualListLayoutElementSettings LayoutSettings 57 | { 58 | get 59 | { 60 | bool set_mOverrideType = m_LayoutSettings == null; 61 | m_LayoutSettings ??= new(); 62 | if (set_mOverrideType) 63 | { 64 | SettingsEntityPatches.OverrideType.SetValue( 65 | m_LayoutSettings, VirtualListLayoutElementSettings.LayoutOverrideType.UnityLayout); 66 | } 67 | 68 | return m_LayoutSettings; 69 | } 70 | } 71 | 72 | private VirtualListLayoutElementSettings m_LayoutSettings; 73 | 74 | public override void BindViewImplementation() 75 | { 76 | Title.text = ViewModel.Title; 77 | ButtonLabel.text = ViewModel.Text; 78 | Button.OnLeftClick.RemoveAllListeners(); 79 | Button.OnLeftClick.AddListener(() => 80 | { 81 | ViewModel.PerformClick(); 82 | }); 83 | 84 | SetupColor(isHighlighted: false); 85 | } 86 | 87 | private Color NormalColor = Color.clear; 88 | private Color HighlightedColor = new(0.52f, 0.52f, 0.52f, 0.29f); 89 | 90 | // These must be public or they'll be null 91 | public Image HighlightedImage; 92 | public TextMeshProUGUI Title; 93 | public OwlcatButton Button; 94 | public TextMeshProUGUI ButtonLabel; 95 | 96 | private void SetupColor(bool isHighlighted) 97 | { 98 | if (HighlightedImage != null) 99 | { 100 | HighlightedImage.color = isHighlighted ? HighlightedColor : NormalColor; 101 | } 102 | } 103 | 104 | public void OnPointerEnter(PointerEventData eventData) 105 | { 106 | EventBus.RaiseEvent(delegate (ISettingsDescriptionUIHandler h) 107 | { 108 | h.HandleShowSettingsDescription(ViewModel.Title, ViewModel.Description); 109 | }, 110 | true); 111 | SetupColor(isHighlighted: true); 112 | } 113 | 114 | public void OnPointerExit(PointerEventData eventData) 115 | { 116 | EventBus.RaiseEvent(delegate (ISettingsDescriptionUIHandler h) 117 | { 118 | h.HandleHideSettingsDescription(); 119 | }, 120 | true); 121 | SetupColor(isHighlighted: false); 122 | } 123 | } 124 | } 125 | 126 | -------------------------------------------------------------------------------- /ModMenu/NewTypes/SettingsEntityCollapsibleHeader.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.UI.Common; 2 | using Kingmaker.UI.MVVM._PCView.ServiceWindows.Journal; 3 | using Kingmaker.UI.MVVM._VM.Settings.Entities.Decorative; 4 | using Owlcat.Runtime.UI.Controls.Button; 5 | using Owlcat.Runtime.UI.MVVM; 6 | using Owlcat.Runtime.UI.VirtualListSystem.ElementSettings; 7 | using System.Collections.Generic; 8 | using TMPro; 9 | 10 | namespace ModMenu.NewTypes 11 | { 12 | internal class SettingsEntityCollapsibleHeaderVM : SettingsEntityHeaderVM 13 | { 14 | internal readonly List SettingsInGroup = new(); 15 | 16 | private bool Initialized = false; 17 | internal bool Expanded { get; private set; } 18 | 19 | public SettingsEntityCollapsibleHeaderVM(string title, bool expanded = false) : base(title) 20 | { 21 | Expanded = expanded; 22 | } 23 | 24 | private void UpdateView() 25 | { 26 | if (Expanded) 27 | Expand(); 28 | else 29 | Collapse(); 30 | } 31 | 32 | internal void Collapse() 33 | { 34 | foreach (var entityVM in SettingsInGroup) 35 | { 36 | entityVM.Active.Value = false; 37 | } 38 | } 39 | 40 | internal void Expand() 41 | { 42 | bool expanded = true; 43 | foreach (var entityVM in SettingsInGroup) 44 | { 45 | if (entityVM is SettingsEntitySubHeaderVM subHeader) 46 | { 47 | subHeader.Active.Value = true; 48 | expanded = subHeader.Expanded; 49 | } 50 | else 51 | entityVM.Active.Value = expanded; 52 | } 53 | } 54 | 55 | internal void Init(ExpandableCollapseMultiButtonPC button) 56 | { 57 | button.SetValue(Expanded, true); 58 | if (Initialized) 59 | return; 60 | 61 | UpdateView(); 62 | Initialized = true; 63 | } 64 | 65 | internal void Toggle(ExpandableCollapseMultiButtonPC button) 66 | { 67 | Expanded = !Expanded; 68 | button.SetValue(Expanded, true); 69 | UpdateView(); 70 | } 71 | } 72 | 73 | internal class SettingsEntityCollapsibleHeaderView : VirtualListElementViewBase 74 | { 75 | public override VirtualListLayoutElementSettings LayoutSettings 76 | { 77 | get 78 | { 79 | bool set_mOverrideType = m_LayoutSettings == null; 80 | m_LayoutSettings ??= 81 | new() 82 | { 83 | // This is the typical header height 84 | Height = 65, 85 | OverrideHeight = true, 86 | Padding = new() { Top = 10 } 87 | }; 88 | if (set_mOverrideType) 89 | { 90 | SettingsEntityPatches.OverrideType.SetValue( 91 | m_LayoutSettings, VirtualListLayoutElementSettings.LayoutOverrideType.Custom); 92 | } 93 | 94 | return m_LayoutSettings; 95 | } 96 | } 97 | private VirtualListLayoutElementSettings m_LayoutSettings; 98 | 99 | public TextMeshProUGUI Title; 100 | public OwlcatMultiButton Button; 101 | public ExpandableCollapseMultiButtonPC ButtonPC; 102 | 103 | protected override void BindViewImplementation() 104 | { 105 | Title.text = UIUtility.GetSaberBookFormat(ViewModel.Tittle, size: GetFontSize()); 106 | Button.OnLeftClick.RemoveAllListeners(); 107 | Button.OnLeftClick.AddListener(() => ViewModel.Toggle(ButtonPC)); 108 | ViewModel.Init(ButtonPC); 109 | } 110 | 111 | protected virtual int GetFontSize() { return 140; } 112 | 113 | protected override void DestroyViewImplementation() { } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ModMenu/NewTypes/SettingsEntityDropdownButton.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.Localization; 2 | using Kingmaker.PubSubSystem; 3 | using Kingmaker.UI.MVVM._PCView.Settings.Entities; 4 | using Kingmaker.UI.MVVM._VM.Settings.Entities; 5 | using Kingmaker.UI.SettingsUI; 6 | using Owlcat.Runtime.UI.Controls.Button; 7 | using Owlcat.Runtime.UI.VirtualListSystem.ElementSettings; 8 | using System; 9 | using TMPro; 10 | using UnityEngine.EventSystems; 11 | using UnityEngine; 12 | using UnityEngine.UI; 13 | 14 | namespace ModMenu.NewTypes 15 | { 16 | public class UISettingsEntityDropdownButton : UISettingsEntityDropdownInt 17 | { 18 | internal LocalizedString ButtonText; 19 | internal Action OnClick; 20 | 21 | internal static UISettingsEntityDropdownButton Create( 22 | LocalizedString description, LocalizedString longDescription, LocalizedString buttonText, Action onClick) 23 | { 24 | var button = ScriptableObject.CreateInstance(); 25 | button.m_Description = description; 26 | button.m_TooltipDescription = longDescription; 27 | 28 | button.ButtonText = buttonText; 29 | button.OnClick = onClick; 30 | return button; 31 | } 32 | 33 | public override SettingsListItemType? Type => SettingsListItemType.Custom; 34 | } 35 | 36 | internal class SettingsEntityDropdownButtonVM : SettingsEntityDropdownVM 37 | { 38 | private readonly UISettingsEntityDropdownButton buttonEntity; 39 | 40 | public string Text => buttonEntity.ButtonText; 41 | 42 | internal SettingsEntityDropdownButtonVM(UISettingsEntityDropdownButton buttonEntity) : base(buttonEntity) 43 | { 44 | this.buttonEntity = buttonEntity; 45 | } 46 | 47 | public void PerformClick(int selectedIndex) 48 | { 49 | buttonEntity.OnClick?.Invoke(selectedIndex); 50 | } 51 | } 52 | 53 | internal class SettingsEntityDropdownButtonView 54 | : SettingsEntityDropdownPCView, IPointerEnterHandler, IPointerExitHandler 55 | { 56 | private SettingsEntityDropdownButtonVM VM => ViewModel as SettingsEntityDropdownButtonVM; 57 | 58 | public override VirtualListLayoutElementSettings LayoutSettings 59 | { 60 | get 61 | { 62 | bool set_mOverrideType = m_LayoutSettings == null; 63 | m_LayoutSettings ??= new(); 64 | if (set_mOverrideType) 65 | { 66 | SettingsEntityPatches.OverrideType.SetValue( 67 | m_LayoutSettings, VirtualListLayoutElementSettings.LayoutOverrideType.UnityLayout); 68 | } 69 | 70 | return m_LayoutSettings; 71 | } 72 | } 73 | 74 | private VirtualListLayoutElementSettings m_LayoutSettings; 75 | 76 | public override void BindViewImplementation() 77 | { 78 | base.BindViewImplementation(); 79 | Title.text = VM.Title; 80 | ButtonLabel.text = VM.Text; 81 | Button.OnLeftClick.RemoveAllListeners(); 82 | Button.OnLeftClick.AddListener(() => 83 | { 84 | VM.PerformClick(VM.GetTempValue()); 85 | }); 86 | 87 | SetupColor(isHighlighted: false); 88 | } 89 | 90 | private Color NormalColor = Color.clear; 91 | private Color HighlightedColor = new(0.52f, 0.52f, 0.52f, 0.29f); 92 | 93 | // These must be public or they'll be null 94 | public Image HighlightedImage; 95 | public TextMeshProUGUI Title; 96 | public OwlcatButton Button; 97 | public TextMeshProUGUI ButtonLabel; 98 | 99 | private void SetupColor(bool isHighlighted) 100 | { 101 | if (HighlightedImage != null) 102 | { 103 | HighlightedImage.color = isHighlighted ? HighlightedColor : NormalColor; 104 | } 105 | } 106 | 107 | public void OnPointerEnter(PointerEventData eventData) 108 | { 109 | EventBus.RaiseEvent(delegate (ISettingsDescriptionUIHandler h) 110 | { 111 | h.HandleShowSettingsDescription(ViewModel.Title, ViewModel.Description); 112 | }, 113 | true); 114 | SetupColor(isHighlighted: true); 115 | } 116 | 117 | public void OnPointerExit(PointerEventData eventData) 118 | { 119 | EventBus.RaiseEvent(delegate (ISettingsDescriptionUIHandler h) 120 | { 121 | h.HandleHideSettingsDescription(); 122 | }, 123 | true); 124 | SetupColor(isHighlighted: false); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /ModMenu/NewTypes/SettingsEntityImage.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.UI.SettingsUI; 2 | using Owlcat.Runtime.UI.MVVM; 3 | using Owlcat.Runtime.UI.VirtualListSystem.ElementSettings; 4 | using UnityEngine; 5 | using UnityEngine.UI; 6 | 7 | namespace ModMenu.NewTypes 8 | { 9 | internal class UISettingsEntityImage : UISettingsEntityBase 10 | { 11 | internal Sprite Sprite; 12 | internal int Height; 13 | internal float ImageScale; 14 | 15 | internal static UISettingsEntityImage Create(Sprite sprite, int height, float imageScale) 16 | { 17 | var image = ScriptableObject.CreateInstance(); 18 | image.Sprite = sprite; 19 | image.Height = height; 20 | image.ImageScale = imageScale; 21 | return image; 22 | } 23 | 24 | public override SettingsListItemType? Type => SettingsListItemType.Custom; 25 | } 26 | 27 | internal class SettingsEntityImageVM : VirtualListElementVMBase 28 | { 29 | internal Sprite Sprite; 30 | internal int Height; 31 | internal float ImageScale; 32 | 33 | internal SettingsEntityImageVM(UISettingsEntityImage imageEntity) 34 | { 35 | Sprite = imageEntity.Sprite; 36 | Height = imageEntity.Height; 37 | ImageScale = imageEntity.ImageScale; 38 | } 39 | 40 | protected override void DisposeImplementation() { } 41 | } 42 | 43 | internal class SettingsEntityImageView : VirtualListElementViewBase 44 | { 45 | public override VirtualListLayoutElementSettings LayoutSettings 46 | { 47 | get 48 | { 49 | if (m_LayoutSettings is null) 50 | { 51 | Main.Logger.NativeLog($"Instantiating layout settings."); 52 | m_LayoutSettings = new() 53 | { 54 | // For some reason it breaks if this isn't set. It doesn't work if you set the height without 55 | // LayoutOverrideType.Custom, but if this is false things are no good. 56 | OverrideHeight = true, 57 | }; 58 | SettingsEntityPatches.OverrideType.SetValue( 59 | m_LayoutSettings, VirtualListLayoutElementSettings.LayoutOverrideType.UnityLayout); 60 | } 61 | return m_LayoutSettings; 62 | } 63 | } 64 | 65 | private void SetHeight(float height) 66 | { 67 | Main.Logger.NativeLog($"Setting layout height: {height}"); 68 | m_LayoutSettings = new() 69 | { 70 | OverrideHeight = true, 71 | Height = height, 72 | }; 73 | 74 | // Without setting to custom the height is ignored. 75 | SettingsEntityPatches.OverrideType.SetValue( 76 | m_LayoutSettings, VirtualListLayoutElementSettings.LayoutOverrideType.Custom); 77 | } 78 | 79 | private VirtualListLayoutElementSettings m_LayoutSettings; 80 | 81 | public Image Icon; 82 | public GameObject TopBorder; 83 | 84 | protected override void BindViewImplementation() 85 | { 86 | Icon.sprite = ViewModel.Sprite; 87 | 88 | var spriteHeight = Icon.sprite.bounds.size.y * Icon.sprite.pixelsPerUnit; 89 | 90 | // You can't set height to pixels directly so instead you have to scale. 91 | float height, scaling; 92 | if (ViewModel.Height > 0) 93 | { 94 | height = ViewModel.Height; 95 | scaling = ViewModel.Height / spriteHeight; 96 | scaling *= ViewModel.ImageScale; 97 | } 98 | else 99 | { 100 | height = spriteHeight; 101 | scaling = 1; 102 | } 103 | 104 | // The height of the row is determined by the vertical height of the sprite, regardless of its scaling. To 105 | // prevent the row from being too-tall, scale the height of everything. 106 | gameObject.transform.localScale = new Vector3(gameObject.transform.localScale.x, scaling); 107 | // Just scaling the height of the image would break the aspect ratio, so scale its width. 108 | Icon.transform.localScale = new Vector3(scaling, Icon.transform.localScale.y); 109 | 110 | // Height scaling on the top bar changes its thickness, so invert it to counteract the row scaling. 111 | float inverseScaling = 1 / scaling; 112 | TopBorder.transform.localScale = new Vector3(TopBorder.transform.localScale.x, inverseScaling); 113 | 114 | SetHeight(height); 115 | } 116 | 117 | protected override void DestroyViewImplementation() { } 118 | } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /ModMenu/NewTypes/SettingsEntityPatches.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.PubSubSystem; 3 | using Kingmaker.UI.MVVM._PCView.ServiceWindows.Journal; 4 | using Kingmaker.UI.MVVM._PCView.Settings; 5 | using Kingmaker.UI.MVVM._PCView.Settings.Entities; 6 | using Kingmaker.UI.MVVM._PCView.Settings.Entities.Decorative; 7 | using Kingmaker.UI.MVVM._VM.Settings; 8 | using Kingmaker.UI.MVVM._VM.Settings.Entities; 9 | using Kingmaker.UI.MVVM._VM.Settings.Entities.Decorative; 10 | using Kingmaker.UI.MVVM._VM.Settings.Entities.Difficulty; 11 | using Kingmaker.UI.SettingsUI; 12 | using Kingmaker.Utility; 13 | using ModMenu.Settings; 14 | using Owlcat.Runtime.UI.Controls.Button; 15 | using Owlcat.Runtime.UI.MVVM; 16 | using Owlcat.Runtime.UI.VirtualListSystem; 17 | using Owlcat.Runtime.UI.VirtualListSystem.ElementSettings; 18 | using System; 19 | using System.Reflection; 20 | using TMPro; 21 | using UnityEngine; 22 | using UnityEngine.UI; 23 | using Object = UnityEngine.Object; 24 | 25 | namespace ModMenu.NewTypes 26 | { 27 | internal class SettingsEntityPatches 28 | { 29 | internal static readonly FieldInfo OverrideType = 30 | AccessTools.Field(typeof(VirtualListLayoutElementSettings), "m_OverrideType"); 31 | 32 | /// 33 | /// Patch to return the correct view model for 34 | /// 35 | [HarmonyPatch(typeof(SettingsVM))] 36 | static class SettingsVM_Patch 37 | { 38 | [HarmonyPatch(nameof(SettingsVM.GetVMForSettingsItem)), HarmonyPrefix] 39 | static bool Prefix( 40 | UISettingsEntityBase uiSettingsEntity, ref VirtualListElementVMBase __result) 41 | { 42 | try 43 | { 44 | if (uiSettingsEntity is UISettingsEntityImage imageEntity) 45 | { 46 | Main.Logger.NativeLog("Returning SettingsEntityImageVM."); 47 | __result = new SettingsEntityImageVM(imageEntity); 48 | return false; 49 | } 50 | if (uiSettingsEntity is UISettingsEntityButton buttonEntity) 51 | { 52 | Main.Logger.NativeLog("Returning SettingsEntityButtonVM."); 53 | __result = new SettingsEntityButtonVM(buttonEntity); 54 | return false; 55 | } 56 | if (uiSettingsEntity is UISettingsEntitySubHeader subHeaderEntity) 57 | { 58 | Main.Logger.NativeLog("Returning SettingsEntitySubHeaderVM."); 59 | __result = new SettingsEntitySubHeaderVM(subHeaderEntity); 60 | return false; 61 | } 62 | if (uiSettingsEntity is UISettingsEntityDropdownButton dropdownButton) 63 | { 64 | Main.Logger.NativeLog("Returning SettingsEntityDropdownButtonVM."); 65 | __result = new SettingsEntityDropdownButtonVM(dropdownButton); 66 | return false; 67 | } 68 | } 69 | catch (Exception e) 70 | { 71 | Main.Logger.LogException("SettingsVM.GetVMForSettingsItem", e); 72 | } 73 | return true; 74 | } 75 | 76 | [HarmonyPatch(nameof(SettingsVM.SwitchSettingsScreen)), HarmonyPostfix] 77 | static void Postfix(UISettingsManager.SettingsScreen settingsScreen, SettingsVM __instance) 78 | { 79 | try 80 | { 81 | if (settingsScreen != ModsMenuEntity.SettingsScreenId) { return; } 82 | Main.Logger.NativeLog("Configuring header buttons."); 83 | 84 | // Add all settings in each group to the corresponding expand/collapse button 85 | SettingsEntityCollapsibleHeaderVM headerVM = null; 86 | SettingsEntitySubHeaderVM subHeaderVM = null; 87 | for (int i = 0; i < __instance.m_SettingEntities.Count; i++) 88 | { 89 | var entity = __instance.m_SettingEntities[i]; 90 | if (entity is SettingsEntitySubHeaderVM subHeader) 91 | { 92 | subHeaderVM = subHeader; 93 | if (headerVM is not null) 94 | headerVM.SettingsInGroup.Add(subHeaderVM); // Sub headers are nested in headers 95 | continue; 96 | } 97 | else if (entity is SettingsEntityHeaderVM header) 98 | { 99 | headerVM = new SettingsEntityCollapsibleHeaderVM(header.Tittle); 100 | __instance.m_SettingEntities[i] = headerVM; 101 | subHeaderVM = null; // Make sure we stop counting sub header entries 102 | continue; 103 | } 104 | 105 | if (headerVM is not null) 106 | headerVM.SettingsInGroup.Add(entity); 107 | if (subHeaderVM is not null) 108 | subHeaderVM.SettingsInGroup.Add(entity); 109 | } 110 | } 111 | catch (Exception e) 112 | { 113 | Main.Logger.LogException("SettingsVM.SwitchSettingsScreen", e); 114 | } 115 | } 116 | } 117 | 118 | /// 119 | /// Patch to add new setting type prefabs. 120 | /// 121 | [HarmonyPatch(typeof(SettingsPCView.SettingsViews))] 122 | static class SettingsViews_Patch 123 | { 124 | [HarmonyPatch(nameof(SettingsPCView.SettingsViews.InitializeVirtualList)), HarmonyPrefix] 125 | static bool Prefix(SettingsPCView.SettingsViews __instance, VirtualListComponent virtualListComponent) 126 | { 127 | try 128 | { 129 | Main.Logger.NativeLog("Adding new type prefabs."); 130 | 131 | // Copy the bool settings 132 | var copyFrom = __instance.m_SettingsEntityBoolViewPrefab.gameObject; 133 | var imageTemplate = CreateImageTemplate(Object.Instantiate(copyFrom)); 134 | var buttonTemplate = 135 | CreateButtonTemplate(Object.Instantiate(copyFrom), 136 | __instance.m_SettingsEntitySliderVisualPerceptionViewPrefab?.m_ResetButton); 137 | 138 | var headerTemplate = 139 | CreateCollapsibleHeaderTemplate( 140 | Object.Instantiate(__instance.m_SettingsEntityHeaderViewPrefab.gameObject)); 141 | var subHeaderTemplate = CreateSubHeaderTemplate(Object.Instantiate(headerTemplate.gameObject)); 142 | 143 | // Copy dropdown since you know, it seems like close to dropdown button right? 144 | var dropdownButtonTemplate = 145 | CreateDropdownButtonTemplate( 146 | Object.Instantiate(__instance.m_SettingsEntityDropdownViewPrefab.gameObject), 147 | __instance.m_SettingsEntitySliderVisualPerceptionViewPrefab?.m_ResetButton); 148 | 149 | virtualListComponent.Initialize(new IVirtualListElementTemplate[] 150 | { 151 | new VirtualListElementTemplate(__instance.m_SettingsEntityHeaderViewPrefab), 152 | new VirtualListElementTemplate(__instance.m_SettingsEntityBoolViewPrefab), 153 | new VirtualListElementTemplate(__instance.m_SettingsEntityDropdownViewPrefab, 0), 154 | new VirtualListElementTemplate(__instance.m_SettingsEntitySliderViewPrefab, 0), 155 | new VirtualListElementTemplate(__instance.m_SettingEntityKeyBindingViewPrefab), 156 | new VirtualListElementTemplate(__instance.m_SettingsEntityDropdownDisplayModeViewPrefab, 1), 157 | new VirtualListElementTemplate(__instance.m_SettingsEntityDropdownGameDifficultyViewPrefab, 0), 158 | new VirtualListElementTemplate(__instance.m_SettingsEntitySliderVisualPerceptionViewPrefab, 1), 159 | new VirtualListElementTemplate(__instance.m_SettingsEntitySliderVisualPerceptionWithImagesViewPrefab, 2), 160 | new VirtualListElementTemplate(__instance.m_SettingsEntityStatisticsOptOutViewPrefab), 161 | new VirtualListElementTemplate(imageTemplate), 162 | new VirtualListElementTemplate(buttonTemplate), 163 | new VirtualListElementTemplate(headerTemplate), 164 | new VirtualListElementTemplate(subHeaderTemplate), 165 | new VirtualListElementTemplate(dropdownButtonTemplate, 0), 166 | }); 167 | } 168 | catch (Exception e) 169 | { 170 | Main.Logger.LogException("SettingsViews_Patch", e); 171 | } 172 | return false; 173 | } 174 | 175 | private static SettingsEntityButtonView CreateButtonTemplate(GameObject prefab, OwlcatButton buttonPrefab) 176 | { 177 | Main.Logger.NativeLog("Creating button template."); 178 | 179 | // Destroy the stuff we don't want from the source prefab 180 | Object.DestroyImmediate(prefab.GetComponent()); 181 | Object.DestroyImmediate(prefab.transform.Find("MultiButton").gameObject); 182 | Object.DontDestroyOnLoad(prefab); 183 | 184 | OwlcatButton buttonControl = null; 185 | TextMeshProUGUI buttonLabel = null; 186 | 187 | // Add in our own button 188 | if (buttonPrefab != null) 189 | { 190 | var button = Object.Instantiate(buttonPrefab.gameObject, prefab.transform); 191 | buttonControl = button.GetComponent(); 192 | buttonLabel = button.GetComponentInChildren(); 193 | 194 | var layout = button.AddComponent(); 195 | layout.ignoreLayout = true; 196 | 197 | var rect = button.transform as RectTransform; 198 | 199 | rect.anchorMin = new(1, 0.5f); 200 | rect.anchorMax = new(1, 0.5f); 201 | rect.pivot = new(1, 0.5f); 202 | 203 | rect.anchoredPosition = new(-55, 0); 204 | rect.sizeDelta = new(430, 45); 205 | } 206 | 207 | // Add our own View (after destroying the Bool one) 208 | var templatePrefab = prefab.AddComponent(); 209 | 210 | // Wire up the fields that would have been deserialized if coming from a bundle 211 | templatePrefab.HighlightedImage = 212 | prefab.transform.Find("HighlightedImage").gameObject.GetComponent(); 213 | templatePrefab.Title = 214 | prefab.transform.Find("HorizontalLayoutGroup/Text").gameObject.GetComponent(); 215 | templatePrefab.Button = buttonControl; 216 | templatePrefab.ButtonLabel = buttonLabel; 217 | 218 | return templatePrefab; 219 | } 220 | 221 | private static SettingsEntityImageView CreateImageTemplate(GameObject prefab) 222 | { 223 | Main.Logger.NativeLog("Creating image template."); 224 | 225 | //Destroy the stuff we don't want from the source prefab 226 | Object.DestroyImmediate(prefab.GetComponent()); 227 | Object.DestroyImmediate(prefab.transform.Find("MultiButton").gameObject); 228 | Object.DestroyImmediate(prefab.transform.Find("HorizontalLayoutGroup").gameObject); 229 | Object.DestroyImmediate(prefab.transform.Find("HighlightedImage").gameObject); 230 | Object.DontDestroyOnLoad(prefab); 231 | 232 | // Add our own View (after destroying the Bool one) 233 | var templatePrefab = prefab.AddComponent(); 234 | 235 | // Create an imagePrefab as a child of the view so it can be scaled independently 236 | var imagePrefab = new GameObject("banner", typeof(RectTransform)); 237 | imagePrefab.transform.SetParent(templatePrefab.transform, false); 238 | 239 | // Wire up the fields that would have been deserialized if coming from a bundle 240 | templatePrefab.Icon = imagePrefab.AddComponent(); 241 | templatePrefab.Icon.preserveAspect = true; 242 | templatePrefab.TopBorder = prefab.transform.Find("TopBorderImage").gameObject; 243 | 244 | return templatePrefab; 245 | } 246 | 247 | private static SettingsEntityCollapsibleHeaderView CreateCollapsibleHeaderTemplate(GameObject prefab) 248 | { 249 | Main.Logger.NativeLog("Creating collapsible header template."); 250 | 251 | // Destroy the stuff we don't want from the source prefab 252 | Object.DestroyImmediate(prefab.GetComponent()); 253 | Object.DontDestroyOnLoad(prefab); 254 | 255 | var buttonPC = prefab.GetComponentInChildren(); 256 | var buttonPrefab = buttonPC.gameObject; 257 | buttonPrefab.transform.Find("_CollapseArrowImage").gameObject.SetActive(true); 258 | var button = buttonPrefab.GetComponent(); 259 | button.Interactable = true; 260 | 261 | // Add our own View 262 | var templatePrefab = prefab.AddComponent(); 263 | templatePrefab.Title = prefab.transform.FindRecursive("Label").GetComponent(); 264 | templatePrefab.Button = button; 265 | templatePrefab.ButtonPC = buttonPC; 266 | return templatePrefab; 267 | } 268 | 269 | // Prefab from the SettingsEntityCollapsibleHeaderView 270 | private static SettingsEntitySubHeaderView CreateSubHeaderTemplate(GameObject prefab) 271 | { 272 | Main.Logger.NativeLog("Creating sub header template."); 273 | 274 | // Destroy the stuff we don't want from the source prefab 275 | Object.DestroyImmediate(prefab.GetComponent()); 276 | Object.DontDestroyOnLoad(prefab); 277 | 278 | // Add our own view 279 | var templatePrefab = prefab.AddComponent(); 280 | templatePrefab.Title = prefab.transform.FindRecursive("Label").GetComponent(); 281 | templatePrefab.Button = prefab.GetComponentInChildren(); 282 | templatePrefab.ButtonPC = prefab.GetComponentInChildren(); 283 | return templatePrefab; 284 | } 285 | 286 | private static SettingsEntityDropdownButtonView CreateDropdownButtonTemplate( 287 | GameObject prefab, OwlcatButton buttonPrefab) 288 | { 289 | Main.Logger.NativeLog("Creating dropdown button template."); 290 | 291 | // Destroy the stuff we don't want from the source prefab 292 | Object.DestroyImmediate(prefab.GetComponent()); 293 | Object.DestroyImmediate(prefab.transform.Find("SetConnectionMarkerIamSet").gameObject); 294 | Object.DontDestroyOnLoad(prefab); 295 | 296 | OwlcatButton buttonControl = null; 297 | TextMeshProUGUI buttonLabel = null; 298 | 299 | // Add in our own button 300 | if (buttonPrefab != null) 301 | { 302 | var button = Object.Instantiate(buttonPrefab.gameObject, prefab.transform); 303 | buttonControl = button.GetComponent(); 304 | buttonLabel = button.GetComponentInChildren(); 305 | 306 | var layout = button.AddComponent(); 307 | layout.ignoreLayout = true; 308 | 309 | var rect = button.transform as RectTransform; 310 | 311 | rect.anchorMin = new(1, 0.5f); 312 | rect.anchorMax = new(1, 0.5f); 313 | rect.pivot = new(1, 0.5f); 314 | 315 | rect.anchoredPosition = new(-510, 0); 316 | rect.sizeDelta = new(215, 45); 317 | } 318 | 319 | // Add our own View (after destroying the Bool one) 320 | var templatePrefab = prefab.AddComponent(); 321 | 322 | // Wire up the fields that would have been deserialized if coming from a bundle 323 | templatePrefab.HighlightedImage = 324 | prefab.transform.Find("HighlightedImage").gameObject.GetComponent(); 325 | templatePrefab.Title = 326 | prefab.transform.Find("HorizontalLayoutGroup/Text").gameObject.GetComponent(); 327 | templatePrefab.Dropdown = prefab.GetComponentInChildren(); 328 | templatePrefab.Button = buttonControl; 329 | templatePrefab.ButtonLabel = buttonLabel; 330 | 331 | return templatePrefab; 332 | } 333 | 334 | [HarmonyPatch(typeof(SettingsVM))] 335 | static class ApplySettings_Patch 336 | { 337 | [HarmonyPatch(nameof(SettingsVM.ApplySettings)), HarmonyPostfix] 338 | static void Postfix() 339 | { 340 | // Raise Event when Apply button is press in the settings UI 341 | EventBus.RaiseEvent(h => h.HandleApplySettings()); 342 | } 343 | } 344 | } 345 | } 346 | } -------------------------------------------------------------------------------- /ModMenu/NewTypes/SettingsEntitySubHeader.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.Localization; 2 | using Kingmaker.UI.SettingsUI; 3 | using Owlcat.Runtime.UI.VirtualListSystem.ElementSettings; 4 | using UnityEngine; 5 | 6 | namespace ModMenu.NewTypes 7 | { 8 | internal class UISettingsEntitySubHeader : UISettingsEntityBase 9 | { 10 | internal LocalizedString Title; 11 | internal bool Expanded; 12 | 13 | internal static UISettingsEntitySubHeader Create(LocalizedString title, bool expanded) 14 | { 15 | var subHeader = ScriptableObject.CreateInstance(); 16 | subHeader.Title = title; 17 | subHeader.Expanded = expanded; 18 | return subHeader; 19 | } 20 | 21 | public override SettingsListItemType? Type => SettingsListItemType.Custom; 22 | } 23 | 24 | internal class SettingsEntitySubHeaderVM : SettingsEntityCollapsibleHeaderVM 25 | { 26 | public SettingsEntitySubHeaderVM(UISettingsEntitySubHeader headerEntity) 27 | : base(headerEntity.Title, headerEntity.Expanded) { } 28 | } 29 | 30 | internal class SettingsEntitySubHeaderView : SettingsEntityCollapsibleHeaderView 31 | { 32 | public override VirtualListLayoutElementSettings LayoutSettings 33 | { 34 | get 35 | { 36 | bool set_mOverrideType = m_LayoutSettings == null; 37 | m_LayoutSettings ??= 38 | new() 39 | { 40 | Height = 45, 41 | OverrideHeight = true, 42 | }; 43 | if (set_mOverrideType) 44 | { 45 | SettingsEntityPatches.OverrideType.SetValue( 46 | m_LayoutSettings, VirtualListLayoutElementSettings.LayoutOverrideType.Custom); 47 | } 48 | 49 | return m_LayoutSettings; 50 | } 51 | } 52 | private VirtualListLayoutElementSettings m_LayoutSettings; 53 | 54 | protected override int GetFontSize() 55 | { 56 | return 110; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ModMenu/Settings/HeaderFix.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.PubSubSystem; 2 | using Kingmaker; 3 | using UnityEngine; 4 | using Kingmaker.Utility; 5 | using HarmonyLib; 6 | using Kingmaker.Blueprints.JsonSystem; 7 | 8 | namespace ModMenu.Settings 9 | { 10 | internal class HeaderFix 11 | { 12 | [HarmonyPatch(typeof(BlueprintsCache))] 13 | static class BlueprintsCache_Patch 14 | { 15 | [HarmonyPatch(nameof(BlueprintsCache.Init)), HarmonyPostfix] 16 | static void Postfix() 17 | { 18 | EventBus.Subscribe(new Fix()); 19 | } 20 | } 21 | } 22 | 23 | internal class Fix : ISettingsUIHandler 24 | { 25 | public void HandleOpenSettings(bool isMainMenu = false) 26 | { 27 | var menuSelectorView = 28 | Game.Instance.RootUiContext.m_CommonView?.transform.Find( 29 | "Canvas/SettingsView/ContentWrapper/MenuSelectorPCView"); 30 | if (menuSelectorView is null) 31 | return; 32 | 33 | foreach (RectTransform transform in menuSelectorView) 34 | transform.ResetScale(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ModMenu/Settings/KeyBinding.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.Localization; 2 | using Kingmaker.Settings; 3 | using Kingmaker.UI.SettingsUI; 4 | using System.Text; 5 | using static Kingmaker.UI.KeyboardAccess; 6 | using UnityEngine; 7 | using HarmonyLib; 8 | using Kingmaker.UI; 9 | using Kingmaker; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Security.Principal; 13 | using Kingmaker.GameModes; 14 | 15 | namespace ModMenu.Settings 16 | { 17 | public class KeyBinding 18 | : BaseSettingWithValue 19 | { 20 | private GameModesGroup GameModesGroup; 21 | private string PrimaryBinding = null; 22 | private string SecondaryBinding = null; 23 | private KeyBindingPair DefaultOverride; 24 | private bool IsHoldTrigger = false; 25 | 26 | /// 27 | public static KeyBinding New(string key, GameModesGroup gameModesGroup, LocalizedString description) 28 | { 29 | return new(key, gameModesGroup, description); 30 | } 31 | 32 | protected override SettingsEntityKeyBindingPair CreateEntity() 33 | { 34 | DefaultOverride = DefaultValue; 35 | if (!string.IsNullOrEmpty(PrimaryBinding)) 36 | { 37 | var bindingString = new StringBuilder($"!{PrimaryBinding}"); 38 | if (!string.IsNullOrEmpty(SecondaryBinding)) 39 | bindingString.Append($";{SecondaryBinding}"); 40 | 41 | DefaultOverride = new(bindingString.ToString(), GameModesGroup); 42 | } 43 | return new SettingsEntityKeyBindingPair(Key, DefaultOverride, SaveDependent, RebootRequired); 44 | } 45 | 46 | protected override UISettingsEntityKeyBinding CreateUIEntity() 47 | { 48 | var uiEntity = ScriptableObject.CreateInstance(); 49 | uiEntity.name = Key; 50 | uiEntity.IsHoldTrigger = IsHoldTrigger; 51 | return uiEntity; 52 | } 53 | 54 | /// 55 | /// If true, the key binding is activated only when held down rather than just pressed. 56 | /// 57 | public KeyBinding SetIsHoldTrigger(bool isHoldTrigger = true) 58 | { 59 | IsHoldTrigger = isHoldTrigger; 60 | return this; 61 | } 62 | 63 | /// 64 | /// Sets the default key binding. 65 | /// 66 | /// 67 | /// Unity's key code for the binding 68 | /// If true, the binding includes the Ctrl key 69 | /// If true, the binding includes the Alt key 70 | /// If true, the binding includes the Shift key 71 | public KeyBinding SetPrimaryBinding( 72 | KeyCode keyCode, bool withCtrl = false, bool withAlt = false, bool withShift = false) 73 | { 74 | PrimaryBinding = Create(keyCode, withCtrl, withAlt, withShift); 75 | return this; 76 | } 77 | 78 | /// 79 | /// Sets the default alternate binding. This is just a second key combination for the same binding. Ignored if 80 | /// there is no primary binding, see . 81 | /// 82 | /// 83 | /// Unity's key code for the binding 84 | /// If true, the binding includes the Ctrl key 85 | /// If true, the binding includes the Alt key 86 | /// If true, the binding includes the Shift key 87 | public KeyBinding SetSecondaryBinding( 88 | KeyCode keyCode, bool withCtrl = false, bool withAlt = false, bool withShift = false) 89 | { 90 | SecondaryBinding = Create(keyCode, withCtrl, withAlt, withShift); 91 | return this; 92 | } 93 | 94 | /// 95 | /// 96 | /// Indicates in which game modes the key binding functions 97 | public KeyBinding(string key, GameModesGroup gameModesGroup, LocalizedString description) 98 | : base(key, new("", gameModesGroup), description) 99 | { 100 | GameModesGroup = gameModesGroup; 101 | } 102 | 103 | private static string Create(KeyCode keyCode, bool withCtrl, bool withAlt, bool withShift) 104 | { 105 | var keyBinding = new StringBuilder(); 106 | if (withCtrl) 107 | keyBinding.Append("%"); 108 | if (withAlt) 109 | keyBinding.Append("&"); 110 | if (withShift) 111 | keyBinding.Append("#"); 112 | keyBinding.Append(keyCode.ToString()); 113 | 114 | return keyBinding.ToString(); 115 | } 116 | } 117 | 118 | [HarmonyPatch(typeof(KeyboardAccess))] 119 | internal static class KeyboardAccess_Patch 120 | { 121 | private static readonly List KeyBindings = new(); 122 | 123 | internal static void RegisterBinding( 124 | SettingsEntityKeyBindingPair entity, UISettingsEntityKeyBinding uiEntity, Action onPress) 125 | { 126 | var keyBinding = new KeyBinding(entity, uiEntity, onPress); 127 | KeyBindings.Add(keyBinding); 128 | RegisterBinding(keyBinding); 129 | } 130 | 131 | private static void RegisterBinding(KeyBinding keyBinding) 132 | { 133 | // First register the binding, then associate it with onPress 134 | var binding = keyBinding.Entity.GetValue(); 135 | if (!keyBinding.UiEntity.TrySetBinding(binding.Binding1, 0)) 136 | { 137 | Main.Logger.Warning($"Unable to set binding: {keyBinding.Entity.Key} - {binding.Binding1}"); 138 | keyBinding.Entity.SetKeyBindingDataAndConfirm(default, 0); 139 | } 140 | if (!keyBinding.UiEntity.TrySetBinding(binding.Binding2, 1)) 141 | { 142 | Main.Logger.Warning($"Unable to set binding: {keyBinding.Entity.Key} - {binding.Binding2}"); 143 | keyBinding.Entity.SetKeyBindingDataAndConfirm(default, 1); 144 | } 145 | 146 | if (Game.Instance.Keyboard.m_BindingCallbacks.TryGetValue(keyBinding.Entity.Key, out var callbacks) && callbacks.Count > 0) 147 | { 148 | #if DEBUG 149 | Main.Logger.Log($"Callback binding found: {keyBinding.Entity.Key}"); 150 | #endif 151 | return; 152 | } 153 | 154 | #if DEBUG 155 | Main.Logger.Log($"Binding callback: {keyBinding.Entity.Key}"); 156 | #endif 157 | Game.Instance.Keyboard.Bind(keyBinding.Entity.Key, keyBinding.OnPress); 158 | } 159 | 160 | // This patch is needed because all key bindings are cleared (and thus get disposed) every time you load to a 161 | // different mode (e.g. from in game back to main menu). This re-registers the settings, similar to the flow used 162 | // for base game key bindings. 163 | [HarmonyPatch(nameof(KeyboardAccess.RegisterBuiltinBindings)), HarmonyPrefix] 164 | static void RegisterBuiltinBindings() 165 | { 166 | try 167 | { 168 | #if DEBUG 169 | Main.Logger.Log("Re-registering key bindings."); 170 | #endif 171 | foreach (var keyBinding in KeyBindings) 172 | { 173 | keyBinding.UiEntity.RenewRegisteredBindings(); 174 | RegisterBinding(keyBinding); 175 | } 176 | } 177 | catch (Exception e) 178 | { 179 | Main.Logger.LogException("KeyboardAccess_Patch.RegisterBuiltinBindings", e); 180 | } 181 | } 182 | 183 | private struct KeyBinding 184 | { 185 | public readonly SettingsEntityKeyBindingPair Entity; 186 | public readonly UISettingsEntityKeyBinding UiEntity; 187 | public readonly Action OnPress; 188 | 189 | public KeyBinding(SettingsEntityKeyBindingPair entity, UISettingsEntityKeyBinding uiEntity, Action onPress) 190 | { 191 | Entity = entity; 192 | UiEntity = uiEntity; 193 | OnPress = onPress; 194 | } 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /ModMenu/Settings/ModsMenuEntity.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Kingmaker.Localization; 3 | using Kingmaker.UI.MVVM._PCView.Settings.Menu; 4 | using Kingmaker.UI.MVVM._VM.Settings; 5 | using Kingmaker.UI.SettingsUI; 6 | using Kingmaker.Utility; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Reflection; 10 | using System.Reflection.Emit; 11 | 12 | namespace ModMenu.Settings 13 | { 14 | /// 15 | /// Class containing patches necessary to inject an additional settings screen into the menu. 16 | /// 17 | internal class ModsMenuEntity 18 | { 19 | // Random magic number representing our fake enum for UiSettingsManager.SettingsScreen 20 | private const int SettingsScreenValue = 17; 21 | internal static readonly UISettingsManager.SettingsScreen SettingsScreenId = 22 | (UISettingsManager.SettingsScreen)SettingsScreenValue; 23 | 24 | private static LocalizedString _menuTitleString; 25 | private static LocalizedString MenuTitleString 26 | { 27 | get 28 | { 29 | _menuTitleString ??= Helpers.CreateString( 30 | "ModsMenu.Title", "Mods", ruRU: "Моды"); 31 | return _menuTitleString; 32 | } 33 | } 34 | 35 | private static readonly List ModSettings = new(); 36 | 37 | internal static void Add(UISettingsGroup uiSettingsGroup) 38 | { 39 | ModSettings.Add(uiSettingsGroup); 40 | } 41 | 42 | /// 43 | /// Patch to create the Mods Menu ViewModel. 44 | /// 45 | [HarmonyPatch] 46 | static class SettingsVM_Constructor 47 | { 48 | static MethodBase TargetMethod() 49 | { 50 | // There's only a single constructor so grab the first one and ignore the arguments. Maybe I'll try adding 51 | // back the args version later but right now this works. 52 | return AccessTools.FirstConstructor(typeof(SettingsVM), c => true); 53 | } 54 | 55 | private static readonly MethodInfo CreateMenuEntity = 56 | AccessTools.Method(typeof(SettingsVM), nameof(SettingsVM.CreateMenuEntity)); 57 | static IEnumerable Transpiler(IEnumerable instructions) 58 | { 59 | var code = new List(instructions); 60 | 61 | // Look for the last usage of CreateMenuEntity, we want to insert just after that. 62 | var insertionIndex = 0; 63 | for (int i = code.Count - 1; i > 0; i--) 64 | { 65 | if (code[i].Calls(CreateMenuEntity)) 66 | { 67 | insertionIndex = i + 1; // increment since inserting at i would actually be before the insertion point 68 | break; 69 | } 70 | } 71 | 72 | var newCode = 73 | new List() 74 | { 75 | new CodeInstruction(OpCodes.Ldarg_0), // Loads this 76 | CodeInstruction.Call(typeof(SettingsVM_Constructor), nameof(SettingsVM_Constructor.AddMenuEntity)), 77 | }; 78 | 79 | code.InsertRange(insertionIndex, newCode); 80 | return code; 81 | } 82 | 83 | private static void AddMenuEntity(SettingsVM settings) 84 | { 85 | try 86 | { 87 | settings.CreateMenuEntity(MenuTitleString, SettingsScreenId); 88 | Main.Logger.NativeLog("Added Mods Menu ViewModel."); 89 | } 90 | catch (Exception e) 91 | { 92 | Main.Logger.LogException(e); 93 | } 94 | } 95 | } 96 | 97 | /// 98 | /// Patch to create the Mods Menu View. Needed to show the menu in-game. 99 | /// 100 | [HarmonyPatch(typeof(SettingsMenuSelectorPCView))] 101 | static class SettingsMenuSelectorPCView_Patch 102 | { 103 | [HarmonyPatch(nameof(SettingsMenuSelectorPCView.Initialize)), HarmonyPrefix] 104 | static void Initialize_Prefix(SettingsMenuSelectorPCView __instance) 105 | { 106 | try 107 | { 108 | if (!__instance.m_MenuEntities.Any()) 109 | { 110 | __instance.m_MenuEntities = new List(); 111 | __instance.m_MenuEntities.AddRange(__instance.GetComponentsInChildren()); 112 | var existingEntity = __instance.m_MenuEntities.LastItem(); 113 | var newEntity = UnityEngine.Object.Instantiate(existingEntity); 114 | newEntity.transform.SetParent(existingEntity.transform.parent); 115 | __instance.m_MenuEntities.Add(newEntity); 116 | Main.Logger.NativeLog("Added Mods Menu View"); 117 | } 118 | } 119 | catch (Exception e) 120 | { 121 | Main.Logger.LogException(e); 122 | } 123 | } 124 | } 125 | 126 | /// 127 | /// Patch to return the Mods settings list 128 | /// 129 | [HarmonyPatch(typeof(UISettingsManager))] 130 | static class UISettingsManager_GetSettingsList 131 | { 132 | [HarmonyPatch(nameof(UISettingsManager.GetSettingsList)), HarmonyPostfix] 133 | static void Postfix(UISettingsManager.SettingsScreen? screenId, ref List __result) 134 | { 135 | try 136 | { 137 | if (screenId is not null && screenId == SettingsScreenId) 138 | { 139 | Main.Logger.NativeLog($"Returning mod settings for screen {screenId}."); 140 | __result = ModSettings; 141 | } 142 | } 143 | catch (Exception e) 144 | { 145 | Main.Logger.LogException(e); 146 | } 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /ModMenu/Settings/SettingsBuilder.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker; 2 | using Kingmaker.Localization; 3 | using Kingmaker.PubSubSystem; 4 | using Kingmaker.Settings; 5 | using Kingmaker.UI; 6 | using Kingmaker.UI.SettingsUI; 7 | using ModMenu.NewTypes; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using UnityEngine; 12 | 13 | namespace ModMenu.Settings 14 | { 15 | /// 16 | /// Builder API for constructing settings. 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// All the AddX methods return this to support builder style method chaining. Once your SettingsGroup 22 | /// is configured add it to the Mods menu page by calling . 23 | /// 24 | /// 25 | /// 26 | /// Entries are displayed in the order they are added. 27 | /// 28 | /// 29 | /// 30 | /// Creats a setting group with a single feature toggle: 31 | /// 32 | /// 33 | /// 34 | /// ModMenu.AddSettings( 35 | /// SettingsGroup.New("mymod.settingsgroup", MySettingsGroupTitle) 36 | /// .AddImage(MyModBanner) 37 | /// .AddToggle( 38 | /// new( 39 | /// "mymod.feature.toggle", 40 | /// defaultValue: false, 41 | /// MyFeatureToggleDescription))); 42 | /// 43 | /// 44 | /// 45 | /// 46 | /// To actually use the settings values you must either handle OnValueChanged events which you can do by 47 | /// passing in a with onValueChanged specified, or by storing the 48 | /// SettingsEntity: 49 | /// 50 | /// 51 | /// 52 | /// SettingsEntity<bool> featureToggle; 53 | /// SettingsGroup.New("mymod.settingsgroup", MySettingsGroupTitle) 54 | /// .AddToggle( 55 | /// new( 56 | /// "mymod.feature.toggle.using.event", 57 | /// defaultValue: false, 58 | /// MyFeatureToggleDescription, 59 | /// // When toggled this calls HandleMyFeatureToggle(value) where value is the new setting. 60 | /// onValueChanged: value => HandleMyFeatureToggle(value))) 61 | /// .AddToggle( 62 | /// new( 63 | /// "mymod.feature.toggle.using.entity", 64 | /// defaultValue: false, 65 | /// MyFeatureToggleDescription), 66 | /// // When toggled featureToggle updates its value which can be retrieved by calling featureToggle.GetValue() 67 | /// out featureToggle)); 68 | /// 69 | /// 70 | /// 71 | public class SettingsBuilder 72 | { 73 | private readonly UISettingsGroup Group = ScriptableObject.CreateInstance(); 74 | private readonly List Settings = new(); 75 | private readonly Dictionary SettingsEntities = new(); 76 | private Action OnDefaultsApplied; 77 | 78 | /// 79 | /// Globally unique key / name for the settings group. Use only lowercase letters, numbers, '-', and '.' 80 | /// 81 | /// Title of the settings group, displayed on the settings page 82 | public static SettingsBuilder New(string key, LocalizedString title) 83 | { 84 | return new(key, title); 85 | } 86 | 87 | public SettingsBuilder(string key, LocalizedString title) 88 | { 89 | Group.name = key.ToLower(); 90 | Group.Title = title; 91 | } 92 | 93 | /// 94 | /// Adds a row containing an image. There is no setting tied to this, it is just for decoration. 95 | /// 96 | /// 97 | /// Height added in v1.2.1 98 | /// 99 | /// 100 | /// Sets the row height. Keep in mind the scaling is relative to resolution; a standard row has a height of 40. The 101 | /// image width will be scaled to preserve the aspect ratio. 102 | /// 103 | /// 104 | /// Adjust the size of the image. Use this if the default logic doesn't get the size of the image correct. 105 | /// 106 | public SettingsBuilder AddImage(Sprite sprite, int height, float imageScale) 107 | { 108 | return AddImageInternal(sprite, height, imageScale); 109 | } 110 | 111 | /// 112 | /// Adds a row containing an image. There is no setting tied to this, it is just for decoration. 113 | /// 114 | /// 115 | /// Height added in v1.1.0 116 | /// 117 | /// 118 | /// Sets the row height. Keep in mind the scaling is relative to resolution; a standard row has a height of 40. The 119 | /// image width will be scaled to preserve the aspect ratio. 120 | /// 121 | public SettingsBuilder AddImage(Sprite sprite, int height) 122 | { 123 | return AddImageInternal(sprite, height); 124 | } 125 | 126 | /// 127 | public SettingsBuilder AddImage(Sprite sprite) 128 | { 129 | return AddImageInternal(sprite); 130 | } 131 | 132 | private SettingsBuilder AddImageInternal(Sprite sprite, int height = -1, float imageScale = 1.0f) 133 | { 134 | Settings.Add(UISettingsEntityImage.Create(sprite, height, imageScale)); 135 | return this; 136 | } 137 | 138 | /// 139 | /// Adds a row containing a button. There is no setting tied to this, only an event handler. 140 | /// 141 | public SettingsBuilder AddButton(Button button) 142 | { 143 | var uiEntity = button.Build(); 144 | Settings.Add(uiEntity); 145 | return this; 146 | } 147 | 148 | /// 149 | /// Adds a button which resets the value of each setting in this group to its default. Triggers a confirmation 150 | /// prompt before executing. 151 | /// 152 | /// 153 | /// Added in v1.1.0 154 | /// 155 | /// Invoked after default settings are applied. 156 | public SettingsBuilder AddDefaultButton(Action onDefaultsApplied = null) 157 | { 158 | // Make sure OnDefaultsApplied is not null or the dialog doesn't close 159 | OnDefaultsApplied = onDefaultsApplied ?? new(() => { }); 160 | Settings.Add( 161 | Button.New(DefaultDescription(), DefaultButtonLabel, OpenDefaultSettingsDialog) 162 | .WithLongDescription(DefaultDescriptionLong()) 163 | .Build()); 164 | return this; 165 | } 166 | 167 | /// 168 | /// Adds an On / Off setting toggle. 169 | /// 170 | public SettingsBuilder AddToggle(Toggle toggle) 171 | { 172 | var (entity, uiEntity) = toggle.Build(); 173 | return Add(entity.Key, entity, uiEntity); 174 | } 175 | 176 | /// 177 | /// Adds a dropdown setting populated using an enum. 178 | /// 179 | public SettingsBuilder AddDropdown(Dropdown dropdown) where T : Enum 180 | { 181 | var (entity, uiEntity) = dropdown.Build(); 182 | return Add(entity.Key, entity, uiEntity); 183 | } 184 | 185 | /// 186 | /// Adds a dropdown populated using a list of strings. The value of the setting is the index in the list. 187 | /// 188 | public SettingsBuilder AddDropdownList(DropdownList dropdown) 189 | { 190 | var (entity, uiEntity) = dropdown.Build(); 191 | return Add(entity.Key, entity, uiEntity); 192 | } 193 | 194 | /// 195 | /// Similar to DropdownList but includes a button on the setting which fires an event when clicked. 196 | /// 197 | public SettingsBuilder AddDropdownButton(DropdownButton dropdown) 198 | { 199 | var (entity, uiEntity) = dropdown.Build(); 200 | return Add(entity.Key, entity, uiEntity); 201 | } 202 | 203 | /// 204 | /// Adds a slider based on a float. 205 | /// 206 | public SettingsBuilder AddSliderFloat(SliderFloat sliderFloat) 207 | { 208 | var (entity, uiEntity) = sliderFloat.Build(); 209 | return Add(entity.Key, entity, uiEntity); 210 | } 211 | 212 | /// 213 | /// Adds a slider based on an int. 214 | /// 215 | public SettingsBuilder AddSliderInt(SliderInt sliderInt) 216 | { 217 | var (entity, uiEntity) = sliderInt.Build(); 218 | return Add(entity.Key, entity, uiEntity); 219 | } 220 | 221 | /// 222 | /// Adds a button which can be used to set key bindings. 223 | /// 224 | /// 225 | /// 226 | /// Keep in mind: 227 | /// 228 | /// 229 | /// The KeyBinding's Key must be a unique identifier for the setting as well as a unique identifier for the 230 | /// binding. If there's a conflict then will never trigger. 231 | /// 232 | /// 233 | /// If another key binding has the same mapping when the game loads, this key binding will be cleared and the user 234 | /// must set a new one. This safety check does not happen when resetting to defaults using the default button from 235 | /// . This is an owlcat limitation. 236 | /// 237 | /// 238 | /// 239 | /// 240 | /// Action invoked when the key binding is activated 241 | public SettingsBuilder AddKeyBinding(KeyBinding keyBinding, Action onPress) 242 | { 243 | var (entity, uiEntity) = keyBinding.Build(); 244 | KeyboardAccess_Patch.RegisterBinding(entity, uiEntity, onPress); 245 | return Add(entity.Key, entity, uiEntity); 246 | } 247 | 248 | /// 249 | /// Use for settings you construct on your own. 250 | /// 251 | /// 252 | /// 253 | /// Note that settings added this way cannot be retrieved using 254 | /// or . 255 | /// 256 | public SettingsBuilder AddSetting(UISettingsEntityBase setting) 257 | { 258 | Settings.Add(setting); 259 | return this; 260 | } 261 | 262 | /// 263 | /// Adds a sub-header marking the start of collapsible group of settings. 264 | /// 265 | /// 266 | /// 267 | /// The sub-header applies to every view following it until another sub-header is added. 268 | /// 269 | /// 270 | /// If true, the sub-header starts expanded. 271 | public SettingsBuilder AddSubHeader(LocalizedString title, bool startExpanded = false) 272 | { 273 | Settings.Add(UISettingsEntitySubHeader.Create(title, startExpanded)); 274 | return this; 275 | } 276 | 277 | private SettingsBuilder Add(string key, ISettingsEntity entity, UISettingsEntityBase uiEntity) 278 | { 279 | SettingsEntities.Add(key, entity); 280 | Settings.Add(uiEntity); 281 | return this; 282 | } 283 | 284 | internal (UISettingsGroup group, Dictionary settings) Build() 285 | { 286 | Group.SettingsList = Settings.ToArray(); 287 | return (Group, SettingsEntities); 288 | } 289 | 290 | private void OpenDefaultSettingsDialog() 291 | { 292 | string text = 293 | string.Format( 294 | Game.Instance.BlueprintRoot.LocalizedTexts.UserInterfacesText.SettingsUI.RestoreAllDefaultsMessage, 295 | Group.Title); 296 | EventBus.RaiseEvent(delegate (IMessageModalUIHandler w) 297 | { 298 | w.HandleOpen( 299 | text, 300 | MessageModalBase.ModalType.Dialog, 301 | new Action(OnDefaultDialogAnswer)); 302 | }, 303 | true); 304 | } 305 | 306 | public void OnDefaultDialogAnswer(MessageModalBase.ButtonType buttonType) 307 | { 308 | if (buttonType != MessageModalBase.ButtonType.Yes) 309 | return; 310 | 311 | foreach (var setting in SettingsEntities.Values) 312 | setting.ResetToDefault(true); 313 | 314 | OnDefaultsApplied(); 315 | } 316 | 317 | private LocalizedString DefaultDescription() 318 | { 319 | return 320 | Helpers.CreateString( 321 | $"mod-menu.default-description.{Group.name}", 322 | $"Restore all settings in {Group.Title} to their defaults", 323 | ruRU: $"Вернуть все настройки в группе {Group.Title} к значениям по умолчанию"); 324 | } 325 | 326 | private LocalizedString DefaultDescriptionLong() 327 | { 328 | return 329 | Helpers.CreateString( 330 | $"mod-menu.default-description-long.{Group.name}", 331 | $"Sets each settings under {Group.Title} to its default value. Your current settings will be lost." 332 | + $" Settings in other groups are not affected. Keep in mind this will apply to sub-groups under" 333 | + $" {Group.Title} as well (anything that is hidden when the group is collapsed).", 334 | ruRU: $"При нажатии на кнопку все настройки в группе {Group.Title} примут значения по умолчанию." + 335 | $" Ваши текущие настройки будут потеряны. Настройки из других групп затронуты не будут. Обратите внимание," + 336 | $" что изменения коснутся в том числе настроек из подгрупп, вложенных в {Group.Title}" + 337 | $" (т.е. все те настройки, которые оказываются скрыты, когда вы сворачиваете группу)."); 338 | } 339 | 340 | private static LocalizedString _defaultButtonLabel; 341 | private static LocalizedString DefaultButtonLabel 342 | { 343 | get 344 | { 345 | _defaultButtonLabel ??= Helpers.CreateString( 346 | "mod-menu.default-button-label", "Default", ruRU: "По умолчанию"); 347 | return _defaultButtonLabel; 348 | } 349 | } 350 | } 351 | 352 | public abstract class BaseSetting 353 | where TUIEntity : UISettingsEntityBase 354 | where TBuilder : BaseSetting 355 | { 356 | protected readonly TBuilder Self; 357 | 358 | protected readonly LocalizedString Description; 359 | 360 | protected LocalizedString LongDescription; 361 | protected bool VisualConnection = false; 362 | 363 | /// Short description displayed on the setting row. 364 | protected BaseSetting(LocalizedString description) 365 | { 366 | Self = (TBuilder)this; 367 | Description = description; 368 | LongDescription = description; 369 | } 370 | 371 | /// 372 | /// Changes the setting bullet point to a visual connection line. See the game's visual perception settings for an 373 | /// example. 374 | /// 375 | public TBuilder ShowVisualConnection() 376 | { 377 | VisualConnection = true; 378 | return Self; 379 | } 380 | 381 | /// 382 | /// Sets the long description displayed on the right side of the menu when the setting is highlighted. 383 | /// 384 | /// 385 | /// 386 | /// This sets UISettingsEntityBase.TooltipDescription. When not specified, Description is used. 387 | /// 388 | public TBuilder WithLongDescription(LocalizedString longDescription) 389 | { 390 | LongDescription = longDescription; 391 | return Self; 392 | } 393 | } 394 | 395 | public abstract class BaseSettingBuilder 396 | : BaseSetting 397 | where TUIEntity : UISettingsEntityBase 398 | where TBuilder : BaseSettingBuilder 399 | { 400 | private TUIEntity UIEntity; 401 | 402 | /// 403 | protected BaseSettingBuilder(LocalizedString description) : base(description) { } 404 | 405 | public TUIEntity Build() 406 | { 407 | UIEntity ??= CreateUIEntity(); 408 | UIEntity.m_ShowVisualConnection = VisualConnection; 409 | return UIEntity; 410 | } 411 | protected abstract TUIEntity CreateUIEntity(); 412 | } 413 | 414 | public class Button : BaseSettingBuilder 415 | { 416 | private readonly LocalizedString ButtonText; 417 | private readonly Action OnClick; 418 | 419 | /// 420 | public static Button New(LocalizedString description, LocalizedString buttonText, Action onClick) 421 | { 422 | return new(description, buttonText, onClick); 423 | } 424 | 425 | protected override UISettingsEntityButton CreateUIEntity() 426 | { 427 | return UISettingsEntityButton.Create(Description, LongDescription, ButtonText, OnClick); 428 | } 429 | 430 | /// 431 | /// Text displayed on the button 432 | /// Action invoked when the button is clicked 433 | public Button(LocalizedString description, LocalizedString buttonText, Action onClick) : base(description) 434 | { 435 | ButtonText = buttonText; 436 | OnClick = onClick; 437 | } 438 | } 439 | 440 | public abstract class BaseSettingWithValue 441 | : BaseSetting 442 | where TEntity : SettingsEntity 443 | where TUIEntity : UISettingsEntityWithValueBase 444 | where TBuilder : BaseSettingWithValue 445 | { 446 | private TEntity Entity; 447 | private TUIEntity UIEntity; 448 | 449 | protected readonly string Key; 450 | protected readonly T DefaultValue; 451 | /// 452 | /// Currently this is unused but I might add some kind of special handling later so the code is here. 453 | /// 454 | protected readonly bool RebootRequired = false; 455 | 456 | protected bool SaveDependent; 457 | protected Action ValueChanged; 458 | protected Action TempValueChanged; 459 | protected Func ModificationAllowed; 460 | 461 | /// 462 | /// 463 | /// Globally unique key / name for the setting. Use only lowercase letters, numbers, '-', and '.' 464 | /// 465 | /// Default value for the setting. 466 | protected BaseSettingWithValue(string key, T defaultValue, LocalizedString description) : base(description) 467 | { 468 | Key = key; 469 | DefaultValue = defaultValue; 470 | } 471 | 472 | /// 473 | /// Causes the setting to be associated with the current save. By default settings apply globally. 474 | /// 475 | public TBuilder DependsOnSave() 476 | { 477 | SaveDependent = true; 478 | return Self; 479 | } 480 | 481 | /// 482 | /// Invokes the provided action when the value is changed and applied. 483 | /// 484 | public TBuilder OnValueChanged(Action onValueChanged) 485 | { 486 | ValueChanged = onValueChanged; 487 | return Self; 488 | } 489 | 490 | /// 491 | /// Invokes the provided action when the value is changed, before the change is applied. 492 | /// 493 | public TBuilder OnTempValueChanged(Action onTempValueChanged) 494 | { 495 | TempValueChanged = onTempValueChanged; 496 | return Self; 497 | } 498 | 499 | /// 500 | /// When the menu is displayed, the provided function is checked to determine if the setting can be changed. 501 | /// 502 | /// 503 | /// 504 | /// This is only checked when the Mods menu page is opened. As a result you cannot use this to create dependencies 505 | /// between settings. 506 | /// 507 | public TBuilder IsModificationAllowed(Func isModificationAllowed) 508 | { 509 | ModificationAllowed = isModificationAllowed; 510 | return Self; 511 | } 512 | 513 | internal (TEntity entity, TUIEntity uiEntity) Build() 514 | { 515 | if (Entity is null || UIEntity is null) 516 | { 517 | Entity ??= CreateEntity(); 518 | if (ValueChanged is not null) 519 | { 520 | (Entity as IReadOnlySettingEntity).OnValueChanged += ValueChanged; 521 | } 522 | if (TempValueChanged is not null) 523 | { 524 | (Entity as IReadOnlySettingEntity).OnTempValueChanged += TempValueChanged; 525 | } 526 | 527 | UIEntity ??= CreateUIEntity(); 528 | UIEntity.m_Description = Description; 529 | UIEntity.m_TooltipDescription = LongDescription; 530 | UIEntity.ModificationAllowedCheck = ModificationAllowed; 531 | UIEntity.m_ShowVisualConnection = VisualConnection; 532 | UIEntity.LinkSetting(Entity); 533 | } 534 | return (Entity, UIEntity); 535 | } 536 | protected abstract TEntity CreateEntity(); 537 | protected abstract TUIEntity CreateUIEntity(); 538 | } 539 | 540 | public class Toggle : BaseSettingWithValue 541 | { 542 | /// 543 | public static Toggle New(string key, bool defaultValue, LocalizedString description) 544 | { 545 | return new(key, defaultValue, description); 546 | } 547 | 548 | protected override SettingsEntityBool CreateEntity() 549 | { 550 | return new SettingsEntityBool(Key, DefaultValue, SaveDependent, RebootRequired); 551 | } 552 | 553 | protected override UISettingsEntityBool CreateUIEntity() 554 | { 555 | var uiEntity = ScriptableObject.CreateInstance(); 556 | uiEntity.DefaultValue = DefaultValue; 557 | return uiEntity; 558 | } 559 | 560 | /// 561 | public Toggle(string key, bool defaultValue, LocalizedString description) 562 | : base(key, defaultValue, description) { } 563 | } 564 | 565 | public class Dropdown 566 | : BaseSettingWithValue, UISettingsEntityDropdownEnum, Dropdown> 567 | where T : Enum 568 | { 569 | private readonly UISettingsEntityDropdownEnum DropdownEntity; 570 | 571 | /// 572 | public static Dropdown New( 573 | string key, T defaultValue, LocalizedString description, UISettingsEntityDropdownEnum dropdown) 574 | { 575 | return new(key, defaultValue, description, dropdown); 576 | } 577 | 578 | protected override SettingsEntityEnum CreateEntity() 579 | { 580 | return new SettingsEntityEnum(Key, DefaultValue, SaveDependent, RebootRequired); 581 | } 582 | 583 | protected override UISettingsEntityDropdownEnum CreateUIEntity() 584 | { 585 | var values = new List(); 586 | foreach (var value in Enum.GetValues(typeof(T))) 587 | { 588 | values.Add(value.ToString()); 589 | } 590 | DropdownEntity.m_CashedLocalizedValues = values; 591 | return DropdownEntity; 592 | } 593 | 594 | /// 595 | /// 596 | /// 597 | /// Due to Unity limitations you need to create yourself: 598 | /// 599 | /// 600 | /// 601 | /// public enum MySettingsEnum { /* ... */ } 602 | /// // Declare a non-generic class which inherits from the generic type 603 | /// private class UISettingsEntityDropdownMySettingsEnum : UISettingsEntityDropdownEnum<MysettingsEnum> { } 604 | /// 605 | /// new( 606 | /// "mymod.feature.enum", 607 | /// MySettingsEnum.SomeValue, 608 | /// MyEnumFeatureDescription, 609 | /// ScriptableObject.CreateInstance<UISettingsEntityDropdownMySettingsEnum>()); 610 | /// 611 | /// 612 | /// 613 | /// 614 | /// 615 | /// Instance of class inheriting from UISettingsEntityDropdownEnum<TEnum>, created by calling 616 | /// ScriptableObject.CreateInstance<T>() 617 | /// 618 | public Dropdown( 619 | string key, T defaultValue, LocalizedString description, UISettingsEntityDropdownEnum dropdown) 620 | : base(key, defaultValue, description) 621 | { 622 | DropdownEntity = dropdown; 623 | } 624 | } 625 | 626 | public class DropdownList 627 | : BaseSettingWithValue 628 | { 629 | private readonly List DropdownValues; 630 | 631 | /// 632 | public static DropdownList New( 633 | string key, int defaultSelected, LocalizedString description, List values) 634 | { 635 | return new(key, defaultSelected, description, values); 636 | } 637 | 638 | protected override SettingsEntityInt CreateEntity() 639 | { 640 | return new SettingsEntityInt(Key, DefaultValue, SaveDependent, RebootRequired); 641 | } 642 | 643 | protected override UISettingsEntityDropdownInt CreateUIEntity() 644 | { 645 | var dropdown = ScriptableObject.CreateInstance(); 646 | dropdown.m_LocalizedValues = DropdownValues.Select(value => value.ToString()).ToList(); 647 | return dropdown; 648 | } 649 | 650 | /// 651 | /// 652 | /// Index of the default selected value in 653 | /// List of values to display 654 | public DropdownList( 655 | string key, int defaultSelected, LocalizedString description, List values) 656 | : base(key, defaultSelected, description) 657 | { 658 | DropdownValues = values; 659 | } 660 | } 661 | 662 | public class DropdownButton 663 | : BaseSettingWithValue 664 | { 665 | private readonly LocalizedString ButtonText; 666 | private readonly Action OnClick; 667 | private readonly List DropdownValues; 668 | 669 | /// 670 | public static DropdownButton New( 671 | string key, 672 | int defaultSelected, 673 | LocalizedString description, 674 | LocalizedString buttonText, 675 | Action onClick, 676 | List values) 677 | { 678 | return new(key, defaultSelected, description, buttonText, onClick, values); 679 | } 680 | 681 | protected override SettingsEntityInt CreateEntity() 682 | { 683 | return new SettingsEntityInt(Key, DefaultValue, SaveDependent, RebootRequired); 684 | } 685 | 686 | protected override UISettingsEntityDropdownButton CreateUIEntity() 687 | { 688 | var dropdown = UISettingsEntityDropdownButton.Create(Description, LongDescription, ButtonText, OnClick); 689 | dropdown.m_LocalizedValues = DropdownValues.Select(value => value.ToString()).ToList(); 690 | return dropdown; 691 | } 692 | 693 | /// 694 | /// 695 | /// Index of the default selected value in 696 | /// List of values to display 697 | public DropdownButton( 698 | string key, 699 | int defaultSelected, 700 | LocalizedString description, 701 | LocalizedString buttonText, 702 | Action onClick, 703 | List values) 704 | : base(key, defaultSelected, description) 705 | { 706 | ButtonText = buttonText; 707 | OnClick = onClick; 708 | DropdownValues = values; 709 | } 710 | } 711 | 712 | public class SliderFloat 713 | : BaseSettingWithValue 714 | { 715 | private readonly float MinValue; 716 | private readonly float MaxValue; 717 | 718 | private float Step = 0.1f; 719 | private int DecimalPlaces = 1; 720 | private bool ShowValueText = true; 721 | 722 | /// 723 | public static SliderFloat New( 724 | string key, float defaultValue, LocalizedString description, float minValue, float maxValue) 725 | { 726 | return new(key, defaultValue, description, minValue, maxValue); 727 | } 728 | 729 | /// 730 | /// Sets the size of a single step on the slider. 731 | /// 732 | public SliderFloat WithStep(float step) 733 | { 734 | Step = step; 735 | return this; 736 | } 737 | 738 | /// 739 | /// Sets the number of decimal places tracked on the slider. 740 | /// 741 | public SliderFloat WithDecimalPlaces(int decimalPlaces) 742 | { 743 | DecimalPlaces = decimalPlaces; 744 | return this; 745 | } 746 | 747 | /// 748 | /// Hides the text showing the slider value. 749 | /// 750 | public SliderFloat HideValueText() 751 | { 752 | ShowValueText = false; 753 | return this; 754 | } 755 | 756 | protected override SettingsEntityFloat CreateEntity() 757 | { 758 | return new SettingsEntityFloat(Key, DefaultValue, SaveDependent, RebootRequired); 759 | } 760 | 761 | protected override UISettingsEntitySliderFloat CreateUIEntity() 762 | { 763 | var uiEntity = ScriptableObject.CreateInstance(); 764 | uiEntity.m_MinValue = MinValue; 765 | uiEntity.m_MaxValue = MaxValue; 766 | uiEntity.m_Step = Step; 767 | uiEntity.m_DecimalPlaces = DecimalPlaces; 768 | uiEntity.m_ShowValueText = ShowValueText; 769 | return uiEntity; 770 | } 771 | 772 | /// 773 | /// 774 | /// 775 | /// UISettingsEntitySliderFloat Defaults: 776 | /// 777 | /// 778 | /// m_Step 779 | /// 0.1f 780 | /// 781 | /// 782 | /// m_DecimalPlaces 783 | /// 1 784 | /// 785 | /// 786 | /// m_ShowValueText 787 | /// true 788 | /// 789 | /// 790 | /// 791 | public SliderFloat( 792 | string key, float defaultValue, LocalizedString description, float minValue, float maxValue) 793 | : base(key, defaultValue, description) 794 | { 795 | MinValue = minValue; 796 | MaxValue = maxValue; 797 | } 798 | } 799 | 800 | public class SliderInt 801 | : BaseSettingWithValue 802 | { 803 | private readonly int MinValue; 804 | private readonly int MaxValue; 805 | 806 | private bool ShowValueText = true; 807 | 808 | /// 809 | public static SliderInt New( 810 | string key, int defaultValue, LocalizedString description, int minValue, int maxValue) 811 | { 812 | return new(key, defaultValue, description, minValue, maxValue); 813 | } 814 | 815 | /// 816 | /// Hides the text showing the slider value. 817 | /// 818 | public SliderInt HideValueText() 819 | { 820 | ShowValueText = false; 821 | return this; 822 | } 823 | 824 | protected override SettingsEntityInt CreateEntity() 825 | { 826 | return new SettingsEntityInt(Key, DefaultValue, SaveDependent, RebootRequired); 827 | } 828 | 829 | protected override UISettingsEntitySliderInt CreateUIEntity() 830 | { 831 | var uiEntity = ScriptableObject.CreateInstance(); 832 | uiEntity.m_MinValue = MinValue; 833 | uiEntity.m_MaxValue = MaxValue; 834 | uiEntity.m_ShowValueText = ShowValueText; 835 | return uiEntity; 836 | } 837 | 838 | /// 839 | /// 840 | /// 841 | /// UISettingsEntitySliderInt Defaults: 842 | /// 843 | /// 844 | /// m_ShowValueText 845 | /// true 846 | /// 847 | /// 848 | /// 849 | public SliderInt( 850 | string key, int defaultValue, LocalizedString description, int minValue, int maxValue) 851 | : base(key, defaultValue, description) 852 | { 853 | MinValue = minValue; 854 | MaxValue = maxValue; 855 | } 856 | } 857 | } 858 | -------------------------------------------------------------------------------- /ModMenu/Settings/TestSettings.cs: -------------------------------------------------------------------------------- 1 | using Kingmaker.Localization; 2 | using Kingmaker.PubSubSystem; 3 | using Kingmaker.UI.MVVM._VM.Settings.Entities; 4 | using Kingmaker.UI.SettingsUI; 5 | using ModMenu.NewTypes; 6 | using System.Text; 7 | using UnityEngine; 8 | using static Kingmaker.UI.KeyboardAccess; 9 | using static ModMenu.Helpers; 10 | 11 | namespace ModMenu.Settings 12 | { 13 | #if DEBUG 14 | /// 15 | /// Test settings group shown on debug builds to validate usage. 16 | /// 17 | internal class TestSettings : ISettingsChanged 18 | { 19 | private static readonly string RootKey = "mod-menu.test-settings"; 20 | private enum TestEnum 21 | { 22 | First, 23 | Second, 24 | Third, 25 | Last 26 | } 27 | 28 | private class UISettingsEntityDropdownTestEnum : UISettingsEntityDropdownEnum { } 29 | 30 | internal void Initialize() 31 | { 32 | ModMenu.AddSettings( 33 | SettingsBuilder.New(RootKey, CreateString("title", "Testing settings")) 34 | .AddImage(Helpers.CreateSprite("ModMenu.WittleWolfie.png"), 250) 35 | .AddDefaultButton(OnDefaultsApplied) 36 | .AddButton( 37 | Button.New( 38 | CreateString("button-desc", "This is a button"), CreateString("button-text", "Click Me!"), OnClick)) 39 | .AddToggle( 40 | Toggle.New(GetKey("toggle"), defaultValue: true, CreateString("toggle-desc", "This is a toggle")) 41 | .ShowVisualConnection() 42 | .OnValueChanged(OnToggle)) 43 | .AddToggle( 44 | Toggle.New(GetKey("toggle-updateable"), defaultValue: true, CreateString("toggle-updateable-desc", "This is a toggle changes the LongDescription text!")) 45 | .ShowVisualConnection() 46 | .OnTempValueChanged(OnToggleUDescriptionUpdate)) 47 | .AddDropdown( 48 | Dropdown.New( 49 | GetKey("dropdown"), 50 | TestEnum.Second, 51 | CreateString("dropdown-desc", "This is a dropdown"), 52 | ScriptableObject.CreateInstance()) 53 | .ShowVisualConnection() 54 | .IsModificationAllowed(CheckToggle) 55 | .WithLongDescription( 56 | CreateString( 57 | "dropdown-long-desc", 58 | "This is a dropdown based on TestEnum. In order to change the value the connected toggle must be on." 59 | + " After switching it on or off exit and enter the menu again to lock/unlock it.")) 60 | .DependsOnSave()) 61 | .AddSubHeader(CreateString("sub-header", "Test Sliders")) 62 | .AddSliderFloat( 63 | SliderFloat.New( 64 | GetKey("float-default"), 65 | defaultValue: 1.0f, 66 | CreateString("float-default-desc", "This is a default float slider"), 67 | minValue: 0.0f, 68 | maxValue: 2.6f)) 69 | .AddSliderFloat( 70 | SliderFloat.New( 71 | GetKey("float"), 72 | defaultValue: 0.05f, 73 | CreateString("float-desc", "This is a custom float slider"), 74 | minValue: 0.05f, 75 | maxValue: 1.00f) 76 | .WithStep(0.05f) 77 | .WithDecimalPlaces(2) 78 | .HideValueText() 79 | .OnTempValueChanged(OnSliderFloatChanged)) 80 | .AddSliderInt( 81 | SliderInt.New( 82 | GetKey("int-default"), 83 | defaultValue: 1, 84 | CreateString("int-default-desc", "This is a default int slider"), 85 | minValue: 0, 86 | maxValue: 5)) 87 | .AddSliderInt( 88 | SliderInt.New( 89 | GetKey("int"), 90 | defaultValue: 2, 91 | CreateString("int-desc", "This is a custom int slider"), 92 | minValue: 1, 93 | maxValue: 6) 94 | .HideValueText())); 95 | 96 | ModMenu.AddSettings( 97 | SettingsBuilder.New(GetKey("extra"), CreateString("extra-title", "More Test Settings")) 98 | .AddDefaultButton() 99 | .AddToggle( 100 | Toggle.New( 101 | GetKey("empty-toggle"), defaultValue: false, CreateString("empty-toggle-desc", "A useless toggle"))) 102 | .AddDropdownList( 103 | DropdownList.New( 104 | GetKey("dropdown-list"), 105 | defaultSelected: 2, 106 | CreateString("dropdown-list", "A dropdown list"), 107 | new() 108 | { 109 | CreateString("dropdown-list-1", "Value is 0"), 110 | CreateString("dropdown-list-2", "Value is 1"), 111 | CreateString("dropdown-list-3", "Value is 2"), 112 | }) 113 | .OnTempValueChanged(value => Main.Logger.Log($"Currently selected dropdown in list is {value}"))) 114 | .AddKeyBinding( 115 | KeyBinding.New( 116 | GetKey("key-binding"), 117 | GameModesGroup.All, 118 | CreateString(GetKey("key-binding-desc"), "This sets a key binding")) 119 | .SetPrimaryBinding(KeyCode.W, withCtrl: true, withAlt: true), 120 | OnKeyPress) 121 | .AddKeyBinding( 122 | KeyBinding.New( 123 | GetKey("key-binding-default"), 124 | GameModesGroup.All, 125 | CreateString(GetKey("key-binding-default-desc"), "This binding is pre-set")) 126 | .SetPrimaryBinding(KeyCode.W, withCtrl: true, withAlt: true) 127 | .SetSecondaryBinding(KeyCode.M, withAlt: true, withShift: true), 128 | OnKeyPress) 129 | .AddDropdownButton( 130 | DropdownButton.New( 131 | GetKey("dropdown-button"), 132 | defaultSelected: 2, 133 | CreateString("dropdown-button", "A dropdown button"), 134 | CreateString("dropdown-button-text", "Apply"), 135 | selected => Main.Logger.Log($"Dropdown button clicked: {selected}"), 136 | new() 137 | { 138 | CreateString("dropdown-button-1", "Button calls onClick(0)"), 139 | CreateString("dropdown-button-2", "Button calls onClick(1)"), 140 | CreateString("dropdown-button-3", "Button calls onClick(2)"), 141 | }) 142 | .OnTempValueChanged(value => Main.Logger.Log($"Currently selected dropdown button is {value}")))); 143 | 144 | EventBus.Subscribe(this); 145 | } 146 | 147 | private void OnKeyPress() 148 | { 149 | Main.Logger.Log($"Key was pressed!"); 150 | } 151 | 152 | private void OnClick() 153 | { 154 | var log = new StringBuilder(); 155 | log.AppendLine("Current settings: "); 156 | log.AppendLine($"-Toggle: {CheckToggle()}"); 157 | log.AppendLine($"-Dropdown: {ModMenu.GetSettingValue(GetKey("dropdown"))}"); 158 | log.AppendLine($"-Default Slider Float: {ModMenu.GetSettingValue(GetKey("float-default"))}"); 159 | log.AppendLine($"-Slider Float: {ModMenu.GetSettingValue(GetKey("float"))}"); 160 | log.AppendLine($"-Default Slider Int: {ModMenu.GetSettingValue(GetKey("int-default"))}"); 161 | log.AppendLine($"-Slider Int: {ModMenu.GetSettingValue(GetKey("int"))}"); 162 | Main.Logger.Log(log.ToString()); 163 | } 164 | 165 | private void OnDefaultsApplied() 166 | { 167 | Main.Logger.NativeLog("Defaults were applied!"); 168 | } 169 | 170 | private bool CheckToggle() 171 | { 172 | Main.Logger.NativeLog("Checking toggle"); 173 | return ModMenu.GetSettingValue(GetKey("toggle")); 174 | } 175 | 176 | public void OnToggleUDescriptionUpdate(bool value) 177 | { 178 | SettingsDescriptionUpdater sdu = new(); 179 | sdu.TryUpdate("This is a toggle changes the LongDescription text!", $"Hey this value is now {value}"); 180 | } 181 | 182 | private void OnToggle(bool value) 183 | { 184 | Main.Logger.Log($"Toggle switched to {value}"); 185 | } 186 | 187 | private void OnSliderFloatChanged(float value) 188 | { 189 | Main.Logger.Log($"Float slider changed to {value}"); 190 | } 191 | 192 | private static LocalizedString CreateString(string partialKey, string text) 193 | { 194 | return Helpers.CreateString(GetKey(partialKey), text); 195 | } 196 | 197 | private static string GetKey(string partialKey) 198 | { 199 | return $"{RootKey}.{partialKey}"; 200 | } 201 | 202 | public void HandleApplySettings() 203 | { 204 | Main.Logger.Log("'Apply' button in the settings window was pressed!"); 205 | } 206 | } 207 | #endif 208 | } 209 | -------------------------------------------------------------------------------- /ModMenu/WittleWolfie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WittleWolfie/ModMenu/7861738edf0f5a82d9ba22028eb7331db60efa33/ModMenu/WittleWolfie.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This mod is now maintained here: https://github.com/CasDragon/ModMenu/ 2 | 3 | # ModMenu 4 | 5 | Adds a new page to the game options for mods. This allows mods to easily implement settings using native UI instead of UMM. This does not create a save dependency. 6 | 7 | ![Test settings screenshot](https://github.com/WittleWolfie/ModMenu/blob/main/test_settings.png) 8 | 9 | ![More settings screenshot](https://github.com/WittleWolfie/ModMenu/blob/main/more_settings.png) 10 | 11 | ## Installation 12 | 13 | 1. Install [Unity Mod Manager](https://github.com/newman55/unity-mod-manager) (UMM), minimum version 0.23.0, and configure for use with Wrath 14 | 2. Install [ModFinder](https://github.com/Pathfinder-WOTR-Modding-Community/ModFinder) and use it to search for Mewsifer Console 15 | 3. Click "Install" 16 | 17 | If you don't want to use ModFinder you can download the [latest release](https://github.com/WittleWolfie/ModMenu/releases/latest) and install normally using UMM. 18 | 19 | ## Problems or Suggestions 20 | 21 | File an [issue on GitHub](https://github.com/WittleWolfie/ModMenu/issues/new) or reach out to me (@WittleWolfie) on [Discord](https://discord.com/invite/wotr) in #mod-dev-technical or #mod-user-general channel. 22 | 23 | ### Controller Support 24 | 25 | **This does not support controllers**. It's a lot of work to support, but let me know if you need this. If there is enough demand I will add it. 26 | 27 | ## Mods Using ModMenu 28 | 29 | This is a non-exhaustive list, let me know if you want your mod added here! 30 | 31 | * [Added Feats](https://github.com/Telyl/AddedFeats) 32 | * [BOAT BOAT BOAT](https://github.com/Balkoth-dev/WOTR_BOAT_BOAT_BOAT) 33 | * [Character Options+](https://github.com/WittleWolfie/CharacterOptionsPlus) 34 | * [MewsiferConsole](https://github.com/Pathfinder-WOTR-Modding-Community/MewsiferConsole) 35 | 36 | ## Mod Developers 37 | 38 | ### Why should you use it? 39 | 40 | * It looks nice! 41 | * Automatically persists your settings 42 | * Handles restoring defaults 43 | * Automatically persists per-save settings 44 | * Super easy to use 45 | 46 | ### How to use it 47 | 48 | The screenshot above was generated using [TestSettings](https://github.com/WittleWolfie/ModMenu/blob/main/ModMenu/Settings/TestSettings.cs). That exercises every function supported. The API is documented and generally self-explanatory. 49 | 50 | In your mod's `Info.json` add `ModMenu` as a requirement: 51 | 52 | ```json 53 | "Requirements": ["ModMenu"] 54 | ``` 55 | 56 | You should specify a minimum version: 57 | 58 | ```json 59 | "Requirements": ["ModMenu-1.1.0"] 60 | ``` 61 | 62 | It's safest to just specify the version you build against as the minimum version, but methods added after 1.0 do specify the version in their remarks. 63 | 64 | Install ModMenu then in your mod's project add `%WrathPath%/Mods/ModMenu/ModMenu.dll` as an assembly reference. 65 | 66 | ### Basic Usage 67 | 68 | Create a setting: 69 | 70 | ```C# 71 | ModMenu.AddSettings( 72 | SettingsBuilder.New("mymod-settings, SettingsTitle) 73 | .AddToggle(Toggle.New("mymod-settings-toggle", defaultValue: true, MyToggleTitle) 74 | .OnValueChanged(OnToggle))); 75 | 76 | private static void OnToggle(bool toggleValue) { 77 | // The user just changed the toggle, toggleValue is the new setting. 78 | // If you need to react to it changing then you can do that here. 79 | // If you don't need to do something whenever the value changes, you can skip OnValueChanged() 80 | } 81 | ``` 82 | 83 | Get the setting value: 84 | 85 | ```C# 86 | ModMenu.GetSettingValue("mymod-settings-toggle"); 87 | ``` 88 | 89 | **The game handles the setting value for you.** You do not need to save the setting, or set the setting to a specific value. You *can* set it if necessary but most of the time it isn't necessary. This includes saving settings that you flag as per-save using `DependsOnSave()`. 90 | 91 | For more examples see [TestSettings](https://github.com/WittleWolfie/ModMenu/blob/main/ModMenu/Settings/TestSettings.cs). 92 | 93 | ### Best Practices 94 | 95 | * **Do not add settings during mod load,** without additional handling you cannot create a `LocalizedString`. I recommend adding settings before, during, or after `BlueprintsCache.Init()`. 96 | * Don't use `IsModificationAllowed` to enable/disable a setting based on another setting. This is checked when the page is opened so it won't apply immediately. 97 | * Indicate settings which require reboot using `WithLongDescription()`. The game's setting boolean `RequireReboot` does nothing. 98 | 99 | Define a "root" key unique to your mod to make sure there are no key conflicts: 100 | 101 | ```C# 102 | private const string RootKey = "mymod-settings"; 103 | ``` 104 | 105 | You can then prepend this to all of your settings keys: 106 | 107 | ```C# 108 | 109 | // Results in a settings key "mymod-settings-key" 110 | var toggle = Toggle.New(GetKey("toggle"), MyToggleTitle); 111 | 112 | private static string GetKey(string key) 113 | { 114 | return $"{RootKey}-{key}"; 115 | } 116 | ``` 117 | 118 | Just make sure you always get the key the same way when getting a setting value. 119 | 120 | ### Settings Behavior 121 | 122 | * Settings with `DependsOnSave()` are associated with a save slot, but do not create save dependencies 123 | * You do not need to handle saving or restoring settings at all, though save dependent settings may be lost if the mod is disabled 124 | * `OnValueChanged()` is called after the user clicks "Apply" and confirms 125 | * `OnTempValueChanged()` is called immediately after the user changes the value, but before it is applied 126 | * A setting's value can be checked at any time by calling [GetSettingValue()](https://github.com/WittleWolfie/ModMenu/blob/main/ModMenu/ModMenu.cs#L85) 127 | 128 | ## Acknowledgements 129 | 130 | * A shout out to Bubbles (factsubio) who essentially wrote the new image and button settings types when I was about to give up. 131 | * The modding community on [Discord](https://discord.com/invite/wotr), an invaluable and supportive resource for help modding. 132 | * All the Owlcat modders who came before me, wrote documents, and open sourced their code. 133 | 134 | ## Interested in modding? 135 | 136 | * Check out the [OwlcatModdingWiki](https://github.com/WittleWolfie/OwlcatModdingWiki/wiki). 137 | * Join us on [Discord](https://discord.com/invite/wotr). 138 | -------------------------------------------------------------------------------- /more_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WittleWolfie/ModMenu/7861738edf0f5a82d9ba22028eb7331db60efa33/more_settings.png -------------------------------------------------------------------------------- /test_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WittleWolfie/ModMenu/7861738edf0f5a82d9ba22028eb7331db60efa33/test_settings.png --------------------------------------------------------------------------------