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