Compare commits

...
172 Commits
Author SHA1 Message Date
Sameer Umar 11e894a29a Add NuGet login step and update push command 2026-10-19 17:20:30 +00:00
Atony 94affd8bf3 Merge pull request #113 from All-Of-Us-Mods/dev
Next MiraAPI release
2026-10-18 18:00:39 -05:00
Az 5021afeee7 improve reflection search 2026-10-18 03:04:04 -04:00
Az 6bfc49af00 rename file 2026-10-18 02:25:04 -04:00
Atony 7a4e891367 Merge branch 'dev' into xml-translations 2026-10-18 00:23:35 -05:00
Az 1667fa4541 add missing documentation 2026-10-18 00:41:39 -04:00
AS fc7a1dc831 Merge pull request #112 from All-Of-Us-Mods/options-improvements
More additions to the options system
2026-10-18 00:37:36 -04:00
Atony d90d3c0a28 Merge pull request #107 from All-Of-Us-Mods/controller-support
Controller Support
2026-10-17 23:10:47 -05:00
Atony 6e48446894 Merge branch 'dev' into controller-support 2026-10-17 21:43:49 -05:00
Atony 0224ac95bc Merge branch 'dev' into options-improvements 2026-10-17 21:43:45 -05:00
Atony 48ee06990d Add background for tab buttons 2026-10-17 21:23:17 -05:00
Atony 0ddbabadd6 Add full grid options 2026-10-17 19:39:08 -05:00
Atony 12dee96200 Add auto-alignment to the left of the guide 2026-10-17 19:10:45 -05:00
Az d8162dcb32 avoid patching indexers instead 2026-10-17 18:23:15 -04:00
Az 5106efe2ac optimisations and utilities 2026-10-17 15:52:53 -04:00
Atony dc87c0fc7f Fix conflicting Local Settings Tabs
Neil made this change fyi
2026-10-17 11:26:29 -05:00
Az 6364f8eae6 constructor parity + scrap toggle display delegate 2026-10-17 04:27:24 -04:00
Az 3e421909de Merge branch 'dev' into options-improvements 2026-10-17 04:03:19 -04:00
Az e4cb4a28df warnings begone 2026-10-17 04:02:58 -04:00
AS 0f51eee1be Merge branch 'dev' into options-improvements 2026-10-16 20:52:49 -04:00
Atony 0b85c9fe96 Add new modifiers, settings, and page buttons 2026-10-16 14:57:56 -05:00
Atony 8fa6903a8d Merge pull request #111 from idkimneil/wiki-improv
Wiki improv
2026-10-16 14:19:20 -05:00
Az ca587ebb18 optimise code + add new example option 2026-10-15 23:21:09 -04:00
Az e0db03b576 Merge branch 'dev' into controller-support 2026-10-15 21:30:54 -04:00
Az aa69fafcf1 minor changes 2026-10-15 21:16:58 -04:00
Az 5e12d1f0d6 oops, copy paste 2026-10-15 21:07:17 -04:00
Az 04b7701b79 more stuff 2026-10-15 20:46:41 -04:00
neilmjain 5c51037503 track options better 2026-10-15 18:09:27 -04:00
neilmjain da7162d60d resize option content with more than 2 columns 2026-10-15 18:03:48 -04:00
neilmjain 602665079c wire up headers to pages with more than 2 columns 2026-10-15 18:03:09 -04:00
neilmjain 39d77a301b add properties to merge groups of options on one page 2026-10-15 18:01:27 -04:00
neilmjain 5e9967606d enlarge setting content by base 2026-10-15 16:37:11 -04:00
neilmjain a7585ee2fe add setting searching by mod name 2026-10-15 16:37:00 -04:00
Az b074fdc330 order code 2026-10-15 03:37:23 -04:00
Az fb74a688c3 remove useless translation calls + order players 2026-10-15 03:28:40 -04:00
Az 7ed3944ad1 target non public getters and setters 2026-10-15 03:10:38 -04:00
Az f08a00ded7 field support 2026-10-15 02:58:01 -04:00
Az f433454fe6 add missing documentation line 2026-10-15 02:01:14 -04:00
Az a31b9e29b9 improve options system 2026-10-15 01:41:56 -04:00
Atony bcacbad0e6 Merge branch 'dev' into wiki-improv 2026-10-14 23:13:28 -05:00
Atony c1978bbb88 Add sprites for settings in wiki 2026-10-14 23:09:08 -05:00
neilmjain cac15bf07f adjust positons of tab labels 2026-10-14 22:25:20 -04:00
neilmjain 1db6a80768 add the no results found text again 2026-10-14 22:18:55 -04:00
neilmjain 06ca48c088 Fix No Results Found text overlaying other UI elements. and add No Results found message to Settings tab 2026-10-14 22:08:59 -04:00
neilmjain 7b77d0d102 use Option Headers as Dividers to Search Options Results 2026-10-14 22:08:28 -04:00
neilmjain 901503f762 fix double dummy roles being spawned on the panel 2026-10-14 22:00:23 -04:00
Az 56cc7661c7 Merge branch 'master' into dev 2026-10-14 21:04:02 -04:00
AS bceed03ac7 Merge pull request #109 from All-Of-Us-Mods/more-colors
Add variable colors
2026-10-14 21:01:35 -04:00
neilmjain 144c762ae3 Merge branch 'wiki-improv' of https://github.com/idkimneil/MiraAPI into wiki-improv 2026-10-14 20:57:13 -04:00
neilmjain 5197b90360 layer sorting by extra mods 2026-10-14 20:57:12 -04:00
neilmjain 542bf72ece Sort Pages by Priority of menus and groups 2026-10-14 20:45:01 -04:00
neilmjain 754161d439 Implement ShowInMatchInfoGuide 2026-10-14 20:44:45 -04:00
Atony 63c411754c Merge branch 'wiki-improv' of https://github.com/idkimneil/MiraAPI into pr/111 2026-10-14 19:13:54 -05:00
Atony 5e863747f7 Merge branch 'dev' into pr/111 2026-10-14 19:13:42 -05:00
neilmjain a01ff6168c Override Vanilla View to match others 2026-10-14 20:08:44 -04:00
neilmjain 75142dbcb8 rename sm properties for easier reading 2026-10-14 19:01:36 -04:00
neilmjain 6d40113ea0 fix scroller and option content clipping 2026-10-14 18:36:57 -04:00
neilmjain e3358507bf Update UI to clone Options Num Rows to display settings 2026-10-14 18:20:21 -04:00
neilmjain 5ec02eec9c add CreateAdvancedSettingsPage to print the settings 2026-10-14 17:50:45 -04:00
neilmjain 1e453cf6af add very scuffed pagination of game settings 2026-10-14 17:26:00 -04:00
CallOfCreator 2748c64db2 Fix custom controller bindings blocking unused vanilla actions 2026-10-14 16:51:37 +00:00
Az 1027e90447 minor improvements 2026-10-14 03:49:21 -04:00
Az 60537fe1a3 Merge branch 'dev' into more-colors 2026-10-14 00:49:38 -04:00
Az 50312e31c2 resolve and suppress warnings 2026-10-14 00:48:08 -04:00
Az 19dd1bb497 fix mentioned issues + add more functionality 2026-10-14 00:18:56 -04:00
Atony 51efa74811 Merge branch 'dev' into controller-support 2026-10-13 22:50:50 -05:00
neilmjain 1c8f7318d3 add no results found text to modifiers page 2026-10-13 23:04:02 -04:00
neilmjain 8e4ff7c179 add filtering/hiding to modifiers page 2026-10-13 23:00:54 -04:00
neilmjain afcb4dc180 change No Roles Enabled to No Results Found for more general meaning across search settings 2026-10-13 23:00:40 -04:00
neilmjain 98aa9dc9a8 Merge branch 'wiki-improv' of https://github.com/idkimneil/MiraAPI into wiki-improv 2026-10-13 22:56:11 -04:00
neilmjain b6414fe5a0 Filter/hide roles by search in cleaner way 2026-10-13 22:52:16 -04:00
neilmjain 6f36aa91a5 also add filtering/hiding of entries not pertaining to search 2026-10-13 22:44:20 -04:00
neilmjain 57471289e1 Scale advanced wiki page content 2026-10-13 22:43:48 -04:00
neilmjain cc4daf5d8b change maximum to 1.00 instead of 1.50 2026-10-13 22:40:58 -04:00
AS 3b336fb27c Merge branch 'dev' into more-colors 2026-10-13 22:30:16 -04:00
AS 9da879fd61 Merge pull request #108 from All-Of-Us-Mods/overlooked-behaviour
Fixes to codebase
2026-10-13 22:23:04 -04:00
Atony 503b7cec51 Merge branch 'dev' into controller-support 2026-10-13 21:06:27 -05:00
Atony 348c7c615b Merge branch 'dev' into more-colors 2026-10-13 21:06:23 -05:00
Atony a07efcd268 Merge branch 'dev' into overlooked-behaviour 2026-10-13 21:06:12 -05:00
neilmjain 613ebafa8a Add match info guide scale setting 2026-10-13 21:24:18 -04:00
Atony 7349a259a1 Add icons to all menus 2026-10-12 03:07:57 -05:00
Atony a9dabe9ff4 Merge branch 'dev' into xml-translations 2026-10-11 22:39:05 -05:00
Atony 5d354eff5e Save wiki preferences in config file 2026-10-11 18:52:29 -05:00
Atony a6dab42df3 Hide searchbox in advanced menu pane 2026-10-11 18:31:10 -05:00
Atony 67e3a6a7bc Add labels to filter, grouping, and order buttons 2026-10-11 17:33:15 -05:00
Az 3982867943 cache il2cpp type 2026-10-11 12:29:32 -04:00
Az d5b08a7145 better audio loading 2026-10-11 12:25:13 -04:00
Az 01b172d73e remove more code, actually
and also fix build error
2026-10-11 12:05:25 -04:00
Az bdf9f4c9e8 deprecate addressables loader
- decprecates the addressables loader utility
- increments the required version
2026-10-11 12:01:26 -04:00
Sameer Umar 362cfc02ff Merge pull request #110 from rewalo/wiki-preset-fixes
Wiki preset fixes
2026-10-11 00:42:13 -04:00
ReAdmin d3bce25ef6 Guard EnsurePanel and expose TriggerPreload 2026-10-08 20:49:11 -07:00
ReAdmin 317fd243b1 Show role/modifier icons on wiki pages, fix wiki softlock after rejoining, even out sort buttons 2026-10-08 20:15:16 -07:00
ReAdmin b6bddbd0df Fix duplicate entries by building entries once on first open 2026-10-08 19:16:37 -07:00
ReAdmin edadd4f350 Defer UI init to first open, add search debounce, fix advanced wiki scale 2026-10-08 19:15:04 -07:00
ReAdmin c5431f0666 Keep advanced wiki pages at local scale when parenting 2026-10-08 18:46:44 -07:00
ReAdmin ec4a5f2bae Keep the wiki open animation smooth by deferring panel work 2026-10-08 18:27:14 -07:00
ReAdmin abfbd8ee73 Reveal wiki panels progressively and preload them before first open 2026-10-08 18:16:13 -07:00
ReAdmin f0650a9841 Look up role parent mods by type instead of scanning IL2CPP objects 2026-10-08 18:16:13 -07:00
Az 43f1209c1c minor code changes 2026-10-08 01:27:24 -04:00
Az a90a0c4905 cache audio clips 2026-10-08 01:20:05 -04:00
Az 6410a0dd42 consolidate copy pasted addressables code 2026-10-08 01:02:09 -04:00
Az f5a994abc3 avoid over-allocating objects
when reading sprite sheets
2026-10-08 00:30:57 -04:00
rewalo c4a5c638f7 Merge branch 'dev' into wiki-preset-fixes 2026-10-07 03:30:16 -07:00
ReAdmin 5379f22364 Remove wiki open timing diagnostics 2026-10-07 03:28:30 -07:00
ReAdmin cb292308c4 Add temporary wiki open frame timing diagnostics 2026-10-07 03:04:16 -07:00
ReAdmin ff17c946a4 Spread wiki panel reveal over frames and show the ring immediately 2026-10-07 03:03:51 -07:00
ReAdmin 5c0e5473a5 Reveal wiki panels across frames on every open 2026-10-07 02:59:10 -07:00
ReAdmin b84c5b064c Avoid redundant panel refreshes when reopening the wiki 2026-10-07 02:53:04 -07:00
ReAdmin ffd42a4a91 Copy the original label layout onto the cloned wiki search field 2026-10-07 02:44:04 -07:00
ReAdmin b94f0aa63b Build the wiki search box from the chat text field instead of a bare TextBoxTMP 2026-10-07 02:37:48 -07:00
ReAdmin 5d9c38a503 Wire a placeholder into the wiki search box so clearing an empty box works 2026-10-07 02:32:42 -07:00
ReAdmin 9cefeebe2b Keep the loading ring sprite alive across scene loads 2026-10-07 02:24:27 -07:00
ReAdmin a88f9b6611 Give the wiki search box a caret so clearing it does not corrupt input 2026-10-07 02:24:27 -07:00
ReAdmin 034521b7b6 Create resource sprites as full rect to stop tiling warnings 2026-10-07 02:16:11 -07:00
ReAdmin 77366593f1 Fix wiki open aborting before the player list is created 2026-10-07 02:16:11 -07:00
ReAdmin 0903503782 Show a loading spinner while wiki panels are built 2026-10-07 02:08:25 -07:00
ReAdmin a297898e6e Fix wiki failing to open after clearing the search box 2026-10-07 02:03:03 -07:00
ReAdmin c5c55745fd Build wiki panels across frames and stop re-initialising them on refresh 2026-10-07 01:56:58 -07:00
ReAdmin 9e5b470ae6 Fix CoLoopWithBudget default budget being zero 2026-10-07 01:53:09 -07:00
ReAdmin d827e5d1f9 Tidy role guide sorting, refresh and layout code 2026-10-07 01:40:35 -07:00
ReAdmin b00775e868 Clean up wiki options text generation 2026-10-07 01:31:44 -07:00
ReAdmin 83722a81c8 Share advanced wiki page creation 2026-10-07 01:31:06 -07:00
ReAdmin a1d22b2fac Fix preset lookup racing mod switches during option creation 2026-10-07 01:29:13 -07:00
Atony eac86ca4eb Remove clear garbage collector functionality
Crashes instantly on x64
2026-10-05 21:02:05 -05:00
Atony e5c41913cb Merge pull request #106 from TheDavSmasher/generic-option-list
Adds a generic IModdedOptionList definition for easier typed iteration
2026-10-05 01:04:55 -05:00
Atony 3b4b6e1800 Merge branch 'dev' into generic-option-list 2026-10-05 01:04:14 -05:00
Atony 7ee4d7da96 Bump appended number for option lists 2026-10-05 01:00:01 -05:00
Atony 206b00c11f De-duplicate some code 2026-10-04 23:18:39 -05:00
Atony aec2cfcad3 Fix presets failing to save 2026-10-04 22:09:03 -05:00
Atony 0dfb65e9b6 clear redundant code 2026-10-04 18:29:34 -05:00
Atony 3cf1963414 Optimize search and filter to be "per-menu" 2026-10-04 18:29:18 -05:00
Atony 6d1557a48d Add modifier support for wiki 2026-10-04 17:17:42 -05:00
Atony 5e6645ff3b Add wiki components to modifiers 2026-10-04 11:49:19 -05:00
AS a36f010207 Merge branch 'dev' into overlooked-behaviour 2026-10-04 02:52:02 -04:00
AS 7deaf5e963 Merge branch 'dev' into more-colors 2026-10-04 02:51:44 -04:00
Atony d0138936be Clear helper warnings 2026-10-04 00:16:04 -05:00
Atony 7cb8246953 Add more locale 2026-10-03 22:47:49 -05:00
Atony 9fd5b488b8 Add wiki abilities as a list for ICustomRole 2026-10-03 22:25:37 -05:00
Atony 24407580d5 Add summarized options helper for wiki 2026-10-03 22:25:01 -05:00
Atony 15c0002395 Open Roles Wiki tab immediately 2026-10-03 19:21:50 -05:00
Atony 91c7ea8dda Toggle roles enabled message properly 2026-10-03 19:16:39 -05:00
Atony 3bcc5139f3 Fix code spacing 2026-10-03 19:10:02 -05:00
Atony b912f6ffa3 Add search box reset safeguard 2026-10-03 19:09:28 -05:00
Atony f0503a6d68 Finish search functionality 2026-10-03 18:47:31 -05:00
Atony c2e251e9c6 Change text color 2026-10-03 15:59:20 -05:00
Atony 43fa7a0a2a Add functional search box and change UI 2026-10-03 15:44:26 -05:00
Atony a27a45944c Add WIP search box 2026-10-03 03:06:33 -05:00
XtraCube 79dc3280e8 dont use il2cpp lists unless absolutely necessary! 2026-10-02 11:52:15 -04:00
Sameer Umar 5303f09627 Add language links and badges to README 2026-10-01 23:07:47 -04:00
Sameer Umar 85f8da54dd Update README with new badge and language links 2026-10-01 23:07:12 -04:00
Az 2e212dee47 bounds checks 2026-10-01 01:50:39 -04:00
Az c2c7c63b9b correct documentation 2026-09-30 19:02:40 -04:00
Atony f52c5de4cd Add wip sorting toggling
az i know there's warnings PLEASE don't slime me out i'll fix it later i promise 🙏
2026-09-30 10:31:27 -05:00
Az cd753d554b fix naming
it originally was called "VariantColor" before i switched it to "VariableColor"
2026-09-30 10:28:26 -04:00
Az f3ddf8d2ab squash final bugs 2026-09-30 10:00:26 -04:00
Atony ab7d43f18b Add vanilla button checks for Influencer 2026-09-29 20:56:03 -05:00
Atony 9f4e519b9e Update game version 2026-09-29 20:55:42 -05:00
Az 27ac201f88 fix oversights + errors 2026-09-29 16:06:49 -04:00
Az a73d1df67e optimisations + example color 2026-09-29 13:54:06 -04:00
CallOfCreator 24f71de1dd Fixed the keybind menu not appearing when using controller input 2026-09-29 15:56:34 +00:00
Az 3bf066f38c add variable color system + color spaces 2026-09-29 09:53:18 -04:00
HayashiUme 44d476dd3a Translated using Weblate (Literary Chinese (lzh_CN))
Currently translated at 52.6% (20 of 38 strings)

Translation: Mira API/Mira API
Translate-URL: https://weblate.duikbo.at/projects/mira-api/mira-api/lzh_CN/
2026-09-28 13:08:53 +00:00
Az c1cd4a7d67 fix small overlooked behaviours
- fixes issue #45 where the player leave event was in a postfix, when the player character was already destroyed
- fixes #46 where modded role options did not have the same change zero behaviour as vanilla ones
2026-09-27 23:24:57 -04:00
Az 5fb60a7082 fix warnings
scav you have committed a war crime. C# =/= python
2026-09-27 16:59:17 -04:00
CallOfCreator 041c39d7ff Controller Support 2026-09-27 16:14:56 +00:00
David Higueros 2d4619f2cf Used generic interface for ModdedOptionList 2026-09-25 12:44:44 -06:00
David Higueros 1b587547f7 Added ConfigName property to IModdedOptionList to optionally rename the individual options 2026-09-25 12:43:14 -06:00
David Higueros ffe9194d54 Resolved warnings 2026-09-25 11:47:12 -06:00
Atony 3575f308e4 Merge branch 'dev' into pr/106 2026-09-24 20:47:16 -05:00
David Higueros 0f8d5bc8ca Merge branch 'dev' into generic-option-list 2026-09-23 21:23:07 -06:00
AS 1c35e93121 Merge branch 'dev' into generic-option-list 2026-09-23 19:08:01 -04:00
David Higueros 1ab662bcf1 Adds a generic IModdedOptionList definition for easier typed iteration 2026-09-21 18:48:09 -06:00
163 changed files with 7792 additions and 2452 deletions

No files matched your search

+18
View File
@@ -86,3 +86,21 @@ dotnet_diagnostic.SA1133.severity = none
# IDE0001: Simplify Names
dotnet_diagnostic.IDE0001.severity = warning
# IDE0079: Remove unnecessary suppression
dotnet_diagnostic.IDE0079.severity = none
# S3903: Types should be defined in named namespaces
dotnet_diagnostic.S3903.severity = none
# IDE0045: Convert to conditional expression
dotnet_diagnostic.IDE0045.severity = none
# IDE0046: Convert to conditional expression
dotnet_diagnostic.IDE0046.severity = none
# S1244: Floating point numbers should not be tested for equality
dotnet_diagnostic.S1244.severity = none
# S2325: Methods and properties that don't access instance data should be static
dotnet_diagnostic.S2325.severity = none
+12 -3
View File
@@ -5,6 +5,8 @@ on: ["workflow_dispatch", "push", "pull_request"]
jobs:
build:
runs-on: ubuntu-22.04
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
with:
@@ -38,14 +40,21 @@ jobs:
with:
name: AllOfUs.MiraAPI.nupkg
path: MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg
- uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag'
with:
draft: true
files: MiraAPI/bin/Release/net6.0/MiraAPI.dll
- name: NuGet login
if: github.ref_type == 'tag'
uses: NuGet/login@v1
id: login
with:
user: xtracube
- name: Push NuGet package
if: github.ref_type == 'tag'
run: |
dotnet nuget push MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
dotnet nuget push MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ steps.login.outputs.NUGET_API_KEY }}
+1 -1
View File
@@ -2,7 +2,7 @@
<ItemGroup>
<PackageReference Include="Reactor" Version="2.5.0-ci.371" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.8.18" PrivateAssets="all" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.9.29" PrivateAssets="all" />
<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime" />
+7
View File
@@ -0,0 +1,7 @@
[*.{cs,vb}]
# SA1602: Enumeration items should be documented.
dotnet_diagnostic.SA1602.severity = none
# SA1601: Partial elements should be documented
dotnet_diagnostic.SA1601.severity = none
+3
View File
@@ -1,4 +1,5 @@
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Translation;
using MiraAPI.Utilities.Assets;
using UnityEngine;
@@ -13,6 +14,8 @@ public class ThinkButton : CustomActionButton
public override int MaxUses => 1;
public override ButtonUsesMode UsesMode => ButtonUsesMode.PerRound;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
protected override void OnClick()
{
Button!.OverrideText("button.think.thinking".Translate());
+7
View File
@@ -1,4 +1,6 @@
using MiraAPI.Colors;
using MiraAPI.Colors.ColorSpaces;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Example;
@@ -14,4 +16,9 @@ public static class ExampleColors
public static CustomColor Rose { get; } = new("color.rose", new Color(0.98f, 0.26f, 0.62f));
public static CustomColor Gold { get; } = new("color.gold", new Color(1.0f, 0.84f, 0.0f));
public static VariableColor Rainbow { get; } = new("color.rainbow", t => new HsbColor(MathUtilities.PingPong(t, 1f, 0.3f), 1f, 1f))
{
ColorBrightness = CustomColorBrightness.Lighter,
};
}
+1 -2
View File
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Globalization;
using System.Linq;
using MiraAPI.Events;
using MiraAPI.Events.Mira;
+2 -2
View File
@@ -1,4 +1,4 @@
using MiraAPI.Keybinds;
using MiraAPI.Keybinds;
using Rewired;
namespace MiraAPI.Example;
@@ -6,5 +6,5 @@ namespace MiraAPI.Example;
[RegisterCustomKeybinds]
public static class ExampleKeybinds
{
public static MiraKeybind NeutralWinKeybind { get; } = new("Neutral Win", KeyboardKeyCode.C, [ModifierKey.Shift]);
public static MiraKeybind NeutralWinKeybind { get; } = new("Neutral Win", KeyboardKeyCode.C, ControllerButton.RightStick, [ModifierKey.Shift]);
}
@@ -1,5 +1,4 @@
using Il2CppSystem;
using MiraAPI.Example.Roles;
using MiraAPI.Example.Roles;
using MiraAPI.Modifiers.Types;
using MiraAPI.Translation;
using UnityEngine;
@@ -28,7 +27,7 @@ public class FreezeModifier : TimedModifier
if (Player.AmOwner || PlayerControl.LocalPlayer.Data.Role is FreezerRole)
{
Player.cosmetics.SetOutline(true, new Nullable<Color>(Palette.LightBlue));
Player.cosmetics.SetOutline(true, new Il2CppSystem.Nullable<Color>(Palette.LightBlue));
}
}
+53 -12
View File
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Utilities;
@@ -6,33 +6,74 @@ using UnityEngine;
namespace MiraAPI.Example.Options;
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Demo.")]
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Read above.")]
[SuppressMessage("Minor Code Smell", "S3400:Methods should not return constants", Justification = "Read above.")]
public class ExampleOptions : AbstractOptionGroup
{
public override string GroupName => "options.exampleOptions1";
public override Color GroupColor => Color.green;
public override bool ShowInMatchInfoGuide => true;
[ModdedToggleOption("options.exampleOptions1.toggleOpt1")]
public bool ToggleOpt { get; set; } = false;
[ModdedOptionVisibility] // Automatically resolves to using ToggleOpt1Visibility, can also target toggle options
public bool ToggleOpt1 { get; set; } = false;
[ModdedToggleOption("options.exampleOptions1.toggleOpt1")]
[ModdedToggleOption("options.exampleOptions1.toggleOpt2")]
[ModdedOptionVisibility(nameof(ToggleOpt1))] // ToggleOpt2 will be visible only when ToggleOpt1 is true
public bool ToggleOpt2 { get; set; } = true;
[ModdedToggleOption("Toggle Opt 3")]
[ModdedOptionVisibility(nameof(ToggleOpt2))] // ToggleOpt3 will be visible only when ToggleOpt2 is true
public bool ToggleOpt3 { get; set; } = true;
[ModdedNumberOption("options.exampleOptions1.numberOpt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)]
[ModdedOptionVisibility(nameof(NumberOptVisibility))] // Can also target static methods
public float NumberOpt { get; set; } = 4f;
[ModdedEnumOption("options.exampleOptions1.bestApi", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])]
public BestApi Opt { get; set; } = BestApi.MiraAPI;
[ModdedNumberOption("options.exampleOptions1.numberOpt", min: 0, max: 10, suffixType: MiraNumberSuffixes.Distance)]
public int NumberOpt2 { get; set; } = 4; // Can also be int!
[ModdedEnumOption("options.exampleOptions1.bestApi", typeof(Api), ["Mira API", "Mitochondria", "Reactor"])]
public Api BestApi { get; set; } = Api.MiraAPI;
[ModdedEnumOption<Api>("options.exampleOptions1.worstApi", ["Mira API", "Mitochondria", "Reactor"])]
// These three can target both instance and static methods
[ModdedOptionGetter(nameof(GetApi))] // Can also automatically resolve to {Option Name}Getter
[ModdedOptionSetter(nameof(SetApi))] // Can also automatically resolve to {Option Name}Setter
[ModdedOptionChanged(nameof(ApiChanged))] // Can also automatically resolve to {Option Name}Changed
public Api WorstApi { get; set; } = Api.MiraAPI;
[ModdedEnumOptionList<ValueState>("Value State {0}", ["None", "Primary", "Secondary"])]
public List<ValueState> ValueStates { get; } =
[ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
public ModdedArray<ValueState> ValueStates { get; set; } = [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
[ModdedPlayerOption("options.exampleOptions1.players")]
public NetworkedPlayerInfo PlayerSelected { get; set; }
public bool ToggleOpt1Visibility()
{
return true;
}
public static bool NumberOptVisibility()
{
return true;
}
public static Api GetApi(Api currentValue)
{
return currentValue;
}
public static Api SetApi(Api newValue, Api oldValue)
{
return default;
}
public static void ApiChanged(Api currentValue)
{
// Something
}
}
public enum BestApi
public enum Api
{
MiraAPI,
Mitochondria,
+3 -2
View File
@@ -7,10 +7,9 @@ namespace MiraAPI.Example.Options;
public class ExampleOptions2 : AbstractOptionGroup
{
public override string GroupName => "options.exampleOptions2";
public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue.
public override MenuCategory ParentMenu => MenuCategory.CustomTwo;
public override bool ShowInMatchInfoGuide => true;
public ModdedPlayerOption PlayerOption2 { get; } = new("options.exampleOptions2.youMustChoseAPlayer", false);
public ModdedPlayerOption PlayerOption { get; } = new("options.exampleOptions2.ehWhatever");
@@ -27,6 +26,8 @@ public class ExampleOptions2 : AbstractOptionGroup
public ModdedEnumOption<TestingData> EnumOpt { get; } = new("options.exampleOptions2.enumOpt", 0)
{
ChangedEvent = x => Logger<ExamplePlugin>.Info($"changed Enum Opt to {x}"),
Getter = currentValue => currentValue,
Setter = (newValue, oldValue) => newValue,
};
}
+9 -6
View File
@@ -16,11 +16,11 @@
Your votes have double the power.</string>
<string name="ApiExample.Role.Crewmate.Mayor.TabDescription">You get one extra vote in the meeting!</string>
<string name="ApiExample.Role.Crewmate.Mayor.WikiDescription">The Mayor is a Crewmate role that gets two votes in the meeting, instead of the singular vote other Crewmates get.</string>
<string name="ApiExample.Role.Crewmate.Chamelon">Chamelon</string>
<string name="ApiExample.Role.Crewmate.Chamelon.IntroBlurb">Become one with the environment.</string>
<string name="ApiExample.Role.Crewmate.Chamelon.MedDescription">Fade out of everyone's vision by not moving.</string>
<string name="ApiExample.Role.Crewmate.Chamelon.TabDescription">Stay invisible while not moving.</string>
<string name="ApiExample.Role.Crewmate.Chamelon.WikiDescription">The Chamelon is a Crewmate role that slowly becomes invisible while standing still.</string>
<string name="ApiExample.Role.Crewmate.Chameleon">Chameleon</string>
<string name="ApiExample.Role.Crewmate.Chameleon.IntroBlurb">Become one with the environment.</string>
<string name="ApiExample.Role.Crewmate.Chameleon.MedDescription">Fade out of everyone's vision by not moving.</string>
<string name="ApiExample.Role.Crewmate.Chameleon.TabDescription">Stay invisible while not moving.</string>
<string name="ApiExample.Role.Crewmate.Chameleon.WikiDescription">The Chameleon is a Crewmate role that slowly becomes invisible while standing still.</string>
<string name="ApiExample.Role.Outcast.OutcastKiller">Outcast Killer</string>
<string name="ApiExample.Role.Outcast.OutcastKiller.IntroBlurb">Kill everyone else.</string>
<string name="ApiExample.Role.Outcast.OutcastKiller.MedDescription">Be the last outcast standing.
@@ -47,6 +47,8 @@ Eliminate every other faction.</string>
<string name="options.exampleOptions1.toggleOpt2">Toggle Opt 2</string>
<string name="options.exampleOptions1.numberOpt">Number Opt</string>
<string name="options.exampleOptions1.bestApi">Best API</string>
<string name="options.exampleOptions1.worstApi">Worst API</string>
<string name="options.exampleOptions1.players">Choose A Player</string>
<string name="options.exampleOptions2">Example Options 2</string>
<string name="options.exampleOptions2.youMustChoseAPlayer">YOU MUST CHOSE A PLAYER</string>
<string name="options.exampleOptions2.ehWhatever">Eh whatever</string>
@@ -73,11 +75,12 @@ Eliminate every other faction.</string>
<string name="settings.miraExample.exampleBool2">Example Bool 2</string>
<string name="settings.miraExample.slider">Slider</string>
<string name="settings.miraExample.exampleNumber">Example Number</string>
<string name="settings.miraExample.exampleMultiplier">Example Multplier</string>
<string name="settings.miraExample.exampleMultiplier">Example Multiplier</string>
<string name="settings.miraExample.exampleEnum">Example Enum</string>
<string name="settings.miraExample.button.exampleButton">Example Button</string>
<string name="gameover.outcastKiller.win">Outcast Killer Wins!</string>
<string name="color.cerulean">Cerulean</string>
<string name="color.rose">Rose</string>
<string name="color.gold">Gold</string>
<string name="color.rainbow">Rainbow</string>
</resources>
+107
View File
@@ -0,0 +1,107 @@
using System;
using System.Diagnostics.CodeAnalysis;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Colors.ColorSpaces;
/// <summary>
/// A struct that denotes the CMYK (Cyan, Magenta, Yellow, Key) color space.
/// </summary>
/// <param name="c">The cyan component.</param>
/// <param name="m">The magenta component.</param>
/// <param name="y">The yellow component.</param>
/// <param name="k">The key component.</param>
/// <param name="a">The alpha component.</param>
[Serializable]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")]
public record struct CmykColor(float c, float m, float y, float k, float a = 1f)
{
/// <summary>
/// Implicitly converts a <see cref="CmykColor"/> to a Unity <see cref="Color"/>.
/// </summary>
/// <param name="c">The color being converted.</param>
public static implicit operator Color(CmykColor c)
{
return ToColor(c);
}
/// <summary>
/// Implicitly converts a Unity <see cref="Color"/> to a <see cref="CmykColor"/>.
/// </summary>
/// <param name="c">The color being converted.</param>
public static implicit operator CmykColor(Color c)
{
return FromColor(c);
}
/// <summary>
/// Converts a <see cref="CmykColor"/> to a standard Unity RGB <see cref="Color"/>.
/// </summary>
/// <param name="cmyk">The CMYK color to convert.</param>
/// <returns>A Unity <see cref="Color"/> representing the same visual color.</returns>
public static Color ToColor(CmykColor cmyk)
{
var key = 1f - cmyk.k;
var r = (1f - cmyk.c) * key;
var g = (1f - cmyk.m) * key;
var b = (1f - cmyk.y) * key;
return new Color(MathUtilities.Clamp01(r), MathUtilities.Clamp01(g), MathUtilities.Clamp01(b), cmyk.a);
}
/// <summary>
/// Converts a standard Unity RGB <see cref="Color"/> to a <see cref="CmykColor"/>.
/// </summary>
/// <param name="color">The Unity Color to convert.</param>
/// <returns>A <see cref="CmykColor"/> representing the same visual color.</returns>
public static CmykColor FromColor(Color color)
{
var r = color.r;
var g = color.g;
var b = color.b;
var k = 1f - MathF.Max(r, MathF.Max(g, b));
if (k >= 1f)
return new CmykColor(0f, 0f, 0f, 1f, color.a);
var invK = 1f / (1f - k);
var c = (1f - r - k) * invK;
var m = (1f - g - k) * invK;
var y = (1f - b - k) * invK;
return new CmykColor(MathUtilities.Clamp01(c), MathUtilities.Clamp01(m), MathUtilities.Clamp01(y), MathUtilities.Clamp01(k), color.a);
}
/// <summary>
/// Linearly interpolates between two <see cref="CmykColor"/> values.
/// </summary>
/// <param name="a">The starting color.</param>
/// <param name="b">The ending color.</param>
/// <param name="t">The interpolation value between the two colors, clamped to a 0 to 1 range.</param>
/// <returns>The interpolated <see cref="CmykColor"/>.</returns>
public static CmykColor Lerp(CmykColor a, CmykColor b, float t)
{
return LerpUnclamped(a, b, MathUtilities.Clamp01(t));
}
/// <summary>
/// Linearly interpolates between two <see cref="CmykColor"/> values without clamping the interpolant.
/// </summary>
/// <param name="a">The starting color.</param>
/// <param name="b">The ending color.</param>
/// <param name="t">The interpolation value between the two colors.</param>
/// <returns>The interpolated <see cref="CmykColor"/>.</returns>
public static CmykColor LerpUnclamped(CmykColor a, CmykColor b, float t)
{
return new CmykColor(
MathUtilities.LerpUnclamped(a.c, b.c, t),
MathUtilities.LerpUnclamped(a.m, b.m, t),
MathUtilities.LerpUnclamped(a.y, b.y, t),
MathUtilities.LerpUnclamped(a.k, b.k, t),
MathUtilities.LerpUnclamped(a.a, b.a, t));
}
}
+136
View File
@@ -0,0 +1,136 @@
using System;
using System.Diagnostics.CodeAnalysis;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Colors.ColorSpaces;
/// <summary>
/// A struct that denotes the HSB (Hue, Saturation, Brightness) color space.
/// </summary>
/// <param name="h">The hue component.</param>
/// <param name="s">The saturation component.</param>
/// <param name="b">The brightness component.</param>
/// <param name="a">The alpha component.</param>
[Serializable]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")]
public record struct HsbColor(float h, float s, float b, float a = 1f)
{
/// <summary>
/// Implicitly converts an <see cref="HsbColor"/> to a Unity <see cref="Color"/>.
/// </summary>
/// <param name="c">The color being converted.</param>
public static implicit operator Color(HsbColor c)
{
return ToColor(c);
}
/// <summary>
/// Implicitly converts a Unity <see cref="Color"/> to an <see cref="HsbColor"/>.
/// </summary>
/// <param name="c">The color being converted.</param>
public static implicit operator HsbColor(Color c)
{
return FromColor(c);
}
/// <summary>
/// Converts an <see cref="HsbColor"/> to a standard Unity RGB <see cref="Color"/>.
/// </summary>
/// <param name="hsb">The HSB color to convert.</param>
/// <returns>A Unity <see cref="Color"/> representing the same visual color.</returns>
public static Color ToColor(HsbColor hsb)
{
if (hsb.s <= float.Epsilon)
return new Color(MathUtilities.Clamp01(hsb.b), MathUtilities.Clamp01(hsb.b), MathUtilities.Clamp01(hsb.b), hsb.a);
var c = hsb.b * hsb.s;
var hPrime = hsb.h * 6f;
var x = c * (1f - MathF.Abs((hPrime % 2f) - 1f));
var m = hsb.b - c;
var (r, g, b) = hPrime switch
{
< 1f => (c, x, 0f),
< 2f => (x, c, 0f),
< 3f => (0f, c, x),
< 4f => (0f, x, c),
< 5f => (x, 0f, c),
<= 6f => (c, 0f, x),
_ => (0f, 0f, 0f),
};
return new Color(
MathUtilities.Clamp01(r + m),
MathUtilities.Clamp01(g + m),
MathUtilities.Clamp01(b + m),
hsb.a);
}
/// <summary>
/// Converts a standard Unity RGB <see cref="Color"/> to an <see cref="HsbColor"/>.
/// </summary>
/// <param name="color">The Unity Color to convert.</param>
/// <returns>An <see cref="HsbColor"/> representing the same visual color.</returns>
public static HsbColor FromColor(Color color)
{
var max = MathF.Max(color.r, MathF.Max(color.g, color.b));
var min = MathF.Min(color.r, MathF.Min(color.g, color.b));
var delta = max - min;
var h = 0f;
if (delta > 0f)
{
if (max == color.r)
h = (color.g - color.b) / delta % 6f;
else if (max == color.g)
h = (color.b - color.r) / delta + 2f;
else
h = (color.r - color.g) / delta + 4f;
h /= 6f;
if (h < 0f)
h += 1f;
}
var s = max == 0f ? 0f : delta / max;
var b = max;
return new HsbColor(h, s, b, color.a);
}
/// <summary>
/// Linearly interpolates between two <see cref="HsbColor"/> values, using the shortest path for the hue.
/// </summary>
/// <param name="a">The starting color.</param>
/// <param name="b">The ending color.</param>
/// <param name="t">The interpolation value between the two colors, clamped to a 0 to 1 range.</param>
/// <returns>The interpolated <see cref="HsbColor"/>.</returns>
public static HsbColor Lerp(HsbColor a, HsbColor b, float t)
{
return LerpUnclamped(a, b, MathUtilities.Clamp01(t));
}
/// <summary>
/// Linearly interpolates between two <see cref="HsbColor"/> values without clamping the interpolant.
/// </summary>
/// <param name="a">The starting color.</param>
/// <param name="b">The ending color.</param>
/// <param name="t">The interpolation value between the two colors.</param>
/// <returns>The interpolated <see cref="HsbColor"/>.</returns>
public static HsbColor LerpUnclamped(HsbColor a, HsbColor b, float t)
{
var hueDifference = MathUtilities.Repeat(b.h - a.h, 1f);
if (hueDifference > 0.5f)
hueDifference -= 1f;
return new HsbColor(
MathUtilities.Repeat(a.h + (hueDifference * t), 1f),
MathUtilities.LerpUnclamped(a.s, b.s, t),
MathUtilities.LerpUnclamped(a.b, b.b, t),
MathUtilities.LerpUnclamped(a.a, b.a, t));
}
}
+132
View File
@@ -0,0 +1,132 @@
using System;
using System.Diagnostics.CodeAnalysis;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Colors.ColorSpaces;
/// <summary>
/// A struct that denotes the HSL (Hue, Saturation, Lightness) color space.
/// </summary>
/// <param name="h">The hue component.</param>
/// <param name="s">The saturation component.</param>
/// <param name="l">The lightness component.</param>
/// <param name="a">The alpha component.</param>
[Serializable]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")]
public record struct HslColor(float h, float s, float l, float a = 1f)
{
/// <summary>
/// Implicitly converts an <see cref="HslColor"/> to a Unity <see cref="Color"/>.
/// </summary>
/// <param name="c">The color being converted.</param>
public static implicit operator Color(HslColor c)
{
return ToColor(c);
}
/// <summary>
/// Implicitly converts a Unity <see cref="Color"/> to an <see cref="HslColor"/>.
/// </summary>
/// <param name="c">The color being converted.</param>
public static implicit operator HslColor(Color c)
{
return FromColor(c);
}
/// <summary>
/// Converts an <see cref="HslColor"/> to a standard Unity RGB <see cref="Color"/>.
/// </summary>
/// <param name="hsl">The HSL color to convert.</param>
/// <returns>A Unity <see cref="Color"/> representing the same visual color.</returns>
public static Color ToColor(HslColor hsl)
{
if (hsl.s <= float.Epsilon)
return new Color(MathUtilities.Clamp01(hsl.l), MathUtilities.Clamp01(hsl.l), MathUtilities.Clamp01(hsl.l), hsl.a);
var c = (1f - MathF.Abs(2f * hsl.l - 1f)) * hsl.s;
var hPrime = hsl.h * 6f;
var x = c * (1f - MathF.Abs((hPrime % 2f) - 1f));
var m = hsl.l - c / 2f;
var (r, g, b) = hPrime switch
{
< 1f => (c, x, 0f),
< 2f => (x, c, 0f),
< 3f => (0f, c, x),
< 4f => (0f, x, c),
< 5f => (x, 0f, c),
<= 6f => (c, 0f, x),
_ => (0f, 0f, 0f),
};
return new Color(MathUtilities.Clamp01(r + m), MathUtilities.Clamp01(g + m), MathUtilities.Clamp01(b + m), hsl.a);
}
/// <summary>
/// Converts a standard Unity RGB <see cref="Color"/> to an <see cref="HslColor"/>.
/// </summary>
/// <param name="color">The Unity Color to convert.</param>
/// <returns>An <see cref="HslColor"/> representing the same visual color.</returns>
public static HslColor FromColor(Color color)
{
var max = MathF.Max(color.r, MathF.Max(color.g, color.b));
var min = MathF.Min(color.r, MathF.Min(color.g, color.b));
var l = (max + min) / 2f;
var delta = max - min;
var s = delta == 0f ? 0f : delta / (1f - MathF.Abs((2f * l) - 1f));
var h = 0f;
if (delta > 0f)
{
if (max == color.r)
h = (color.g - color.b) / delta % 6f;
else if (max == color.g)
h = (color.b - color.r) / delta + 2f;
else
h = (color.r - color.g) / delta + 4f;
h /= 6f;
if (h < 0f)
h += 1f;
}
return new HslColor(h, s, l, color.a);
}
/// <summary>
/// Linearly interpolates between two <see cref="HslColor"/> values, using the shortest path for the hue.
/// </summary>
/// <param name="a">The starting color.</param>
/// <param name="b">The ending color.</param>
/// <param name="t">The interpolation value between the two colors, clamped to a 0 to 1 range.</param>
/// <returns>The interpolated <see cref="HslColor"/>.</returns>
public static HslColor Lerp(HslColor a, HslColor b, float t)
{
return LerpUnclamped(a, b, MathUtilities.Clamp01(t));
}
/// <summary>
/// Linearly interpolates between two <see cref="HslColor"/> values without clamping the interpolant.
/// </summary>
/// <param name="a">The starting color.</param>
/// <param name="b">The ending color.</param>
/// <param name="t">The interpolation value between the two colors.</param>
/// <returns>The interpolated <see cref="HslColor"/>.</returns>
public static HslColor LerpUnclamped(HslColor a, HslColor b, float t)
{
var hueDifference = MathUtilities.Repeat(b.h - a.h, 1f);
if (hueDifference > 0.5f)
hueDifference -= 1f;
return new HslColor(
MathUtilities.Repeat(a.h + (hueDifference * t), 1f),
MathUtilities.LerpUnclamped(a.s, b.s, t),
MathUtilities.LerpUnclamped(a.l, b.l, t),
MathUtilities.LerpUnclamped(a.a, b.a, t));
}
}
+33 -4
View File
@@ -1,4 +1,5 @@
using MiraAPI.Translation;
using System.Diagnostics.CodeAnalysis;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using UnityEngine;
@@ -10,17 +11,37 @@ namespace MiraAPI.Colors;
/// <param name="name">The name of the option.</param>
/// <param name="mainColor">The main color.</param>
/// <param name="shadowColor">The shadow color.</param>
public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal code.")]
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1513:Closing brace should be followed by blank line", Justification = "Looks weird.")]
public class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
{
/// <summary>
/// Gets or sets the main color.
/// </summary>
public Color32 MainColor { get; set; } = mainColor;
public Color32 MainColor
{
get;
set
{
field = value;
InternalMainColor = value;
}
}
= mainColor;
/// <summary>
/// Gets or sets the shadow color.
/// </summary>
public Color32 ShadowColor { get; set; } = shadowColor;
public Color32 ShadowColor
{
get;
set
{
field = value;
InternalShadowColor = value;
}
}
= shadowColor;
/// <summary>
/// Gets or sets the name of the color.
@@ -32,6 +53,9 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha
/// </summary>
public CustomColorBrightness ColorBrightness { get; set; } = CustomColorBrightness.Darker;
internal Color InternalMainColor = mainColor;
internal Color InternalShadowColor = shadowColor;
/// <inheritdoc cref="CustomColor"/>
public CustomColor(StringNames name, Color32 mainColor)
: this(name, mainColor, mainColor.GetShadowColor(60))
@@ -56,6 +80,11 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha
/// </summary>
public enum CustomColorBrightness
{
/// <summary>
/// An invalid color.
/// </summary>
Invalid = -1,
/// <summary>
/// A lighter color.
/// </summary>
+115
View File
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using HarmonyLib;
using UnityEngine;
namespace MiraAPI.Colors;
@@ -15,9 +17,36 @@ public static class PaletteManager
public static CustomColor[] RegisteredColors => [.. CustomColors];
internal static readonly List<CustomColor> CustomColors = [];
internal static readonly HashSet<StringNames> VariableColorNames = [];
private static readonly CustomColorBrightness[] ColorBrightnessMap =
[
CustomColorBrightness.Darker, // Red
CustomColorBrightness.Darker, // Blue
CustomColorBrightness.Darker, // Green
CustomColorBrightness.Lighter, // Pink
CustomColorBrightness.Lighter, // Orange
CustomColorBrightness.Lighter, // Yellow
CustomColorBrightness.Darker, // Black
CustomColorBrightness.Lighter, // White
CustomColorBrightness.Darker, // Purple
CustomColorBrightness.Darker, // Brown
CustomColorBrightness.Lighter, // Cyan
CustomColorBrightness.Lighter, // Lime
CustomColorBrightness.Darker, // Maroon
CustomColorBrightness.Lighter, // Rose
CustomColorBrightness.Lighter, // Banana
CustomColorBrightness.Darker, // Gray
CustomColorBrightness.Darker, // Tan
CustomColorBrightness.Lighter, // Coral
];
private static int originalLength;
internal static void RegisterAllColors()
{
originalLength = Palette.PlayerColors.Length;
var colors = CustomColors.Select(x => x.MainColor).ToArray();
var shadowColors = CustomColors.Select(x => x.ShadowColor).ToArray();
var stringNames = CustomColors.Select(x => x.Name).ToArray();
@@ -29,4 +58,90 @@ public static class PaletteManager
Palette.TextColors = Palette.TextColors.ToArray().AddRangeToArray(colors);
Palette.TextOutlineColors = Palette.TextOutlineColors.ToArray().AddRangeToArray(shadowColors);
}
/// <summary>
/// Checks to see if the provided color ID belongs to a variable color.
/// </summary>
/// <param name="colorId">The id of the color.</param>
/// <returns><c>true</c> if the color is a variable color; <c>false</c> otherwise.</returns>
public static bool IsVariable(int colorId)
{
if ((uint)colorId >= (uint)Palette.ColorNames.Length)
return false;
return VariableColorNames.Contains(Palette.ColorNames[colorId]);
}
/// <summary>
/// Checks to see if the provided color ID belongs to a custom color.
/// </summary>
/// <param name="colorId">The ID of the color.</param>
/// <returns><c>true</c> if the color is a custom color; <c>false</c> otherwise.</returns>
public static bool IsCustomColor(int colorId)
{
return (uint)(colorId - originalLength) < (uint)CustomColors.Count;
}
/// <summary>
/// Gets the main color of the provided color ID.
/// </summary>
/// <param name="colorId">The id of the color.</param>
/// <returns>The main color associated with the provided ID.</returns>
public static Color GetMainColor(int colorId)
{
if ((uint)colorId >= (uint)Palette.ColorNames.Length)
return Color.white;
return !TryGetCustomColor(colorId, out var custom)
? Palette.PlayerColors[colorId]
: (custom is VariableColor variable
? variable.EvaluateMainColor(Time.time)
: custom.InternalMainColor);
}
/// <summary>
/// Gets the shadow color of the provided color ID.
/// </summary>
/// <param name="colorId">The ID of the color.</param>
/// <returns>The shadow color associated with the provided ID.</returns>
public static Color GetShadowColor(int colorId)
{
if ((uint)colorId >= (uint)Palette.ColorNames.Length)
return Color.white;
return !TryGetCustomColor(colorId, out var custom)
? Palette.ShadowColors[colorId]
: (custom is VariableColor variable
? variable.EvaluateShadowColor(Time.time)
: custom.InternalShadowColor);
}
/// <summary>
/// Gets the brightness type of the color associated with the provided ID.
/// </summary>
/// <param name="colorId">The ID of the color.</param>
/// <returns>The brightness type of the provided ID.</returns>
public static CustomColorBrightness GetBrightness(int colorId)
{
if ((uint)colorId >= (uint)Palette.ColorNames.Length)
return CustomColorBrightness.Invalid;
return TryGetCustomColor(colorId, out var custom)
? custom.ColorBrightness
: ColorBrightnessMap[colorId];
}
internal static bool TryGetCustomColor(int colorId, [NotNullWhen(true)] out CustomColor? custom)
{
var index = colorId - originalLength;
if ((uint)index >= (uint)CustomColors.Count)
{
custom = null;
return false;
}
custom = CustomColors[index];
return true;
}
}
+72
View File
@@ -0,0 +1,72 @@
using System;
using MiraAPI.LocalSettings;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Colors;
/// <summary>
/// Represents a dynamic custom color that changes over time.
/// </summary>
/// <param name="name">The name of the color.</param>
/// <param name="mainEvaluator">The function to evaluate the dynamic main color.</param>
/// <param name="shadowEvaluator">The function to evaluate the dynamic shadow color.</param>
/// <param name="mainColor">The static main color used when dynamic colors are disabled.</param>
/// <param name="shadowColor">The static shadow color used when dynamic colors are disabled.</param>
/// <param name="defaultTime">The default time that is used to automatically fill in the static colors when the latter is not provided.</param>
public sealed class VariableColor(StringNames name, Func<float, Color> mainEvaluator, Func<float, Color>? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f)
: CustomColor(name, mainColor ?? mainEvaluator(defaultTime), shadowColor ?? shadowEvaluator?.Invoke(defaultTime) ?? (mainColor ?? mainEvaluator(defaultTime)).GetShadowColor(60))
{
/// <summary>
/// Gets or sets a delegate that takes in a time value and returns the dynamic main color.
/// </summary>
public Func<float, Color> MainEvaluator { get; set; } = mainEvaluator;
/// <summary>
/// Gets or sets a delegate that takes in a time value and returns the dynamic shadow color.
/// </summary>
public Func<float, Color> ShadowEvaluator { get; set; } = shadowEvaluator ?? (t => mainEvaluator(t).GetShadowColor(0.24f));
/// <inheritdoc cref="VariableColor"/>
public VariableColor(StringNames name, Func<float, Color> mainEvaluator, Color32? mainColor = null, float defaultTime = 0f)
: this(name, mainEvaluator, null, mainColor, null, defaultTime)
{
}
/// <inheritdoc cref="VariableColor"/>
public VariableColor(string name, Func<float, Color> mainEvaluator, Color32? mainColor = null, float defaultTime = 0f)
: this(name, mainEvaluator, null, mainColor, null, defaultTime)
{
}
/// <inheritdoc cref="VariableColor"/>
public VariableColor(string name, Func<float, Color> mainEvaluator, Func<float, Color>? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f)
: this(MiraLocaleManager.GetOrCreateLocaleString(name), mainEvaluator, shadowEvaluator, mainColor, shadowColor, defaultTime)
{
}
/// <summary>
/// Evaluates the dynamic main color based on the provided time value.
/// </summary>
/// <param name="time">The time to evaluate the color against.</param>
/// <returns>The dynamically evaluated color, or the static <see cref="CustomColor.MainColor"/> if static variable colors are enabled in the user's settings.</returns>
public Color EvaluateMainColor(float time)
{
return LocalSettingsTabSingleton<MiraApiSettings>.Instance.EnableVariableColors.Value
? MainEvaluator(time)
: InternalMainColor;
}
/// <summary>
/// Evaluates the dynamic shadow color based on the provided time value.
/// </summary>
/// <param name="time">The time to evaluate the color against.</param>
/// <returns>The dynamically evaluated color, or the static <see cref="CustomColor.ShadowColor"/> if static variable colors are enabled in the user's settings.</returns>
public Color EvaluateShadowColor(float time)
{
return LocalSettingsTabSingleton<MiraApiSettings>.Instance.EnableVariableColors.Value
? ShadowEvaluator(time)
: InternalShadowColor;
}
}
+102
View File
@@ -0,0 +1,102 @@
using System.Diagnostics.CodeAnalysis;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Colors;
using MiraAPI.Utilities;
using Reactor.Utilities.Attributes;
using UnityEngine;
namespace MiraAPI;
/// <summary>
/// The base class for variable color behaviour on <see cref="SpriteRenderer"/>s.
/// </summary>
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")]
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity fields.")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
public abstract class BaseVariableColorBehaviour : MonoBehaviour
{
/// <summary>
/// The <see cref="VariableColor"/> to apply.
/// </summary>
protected VariableColor? color;
/// <summary>
/// Sets the current variable color.
/// </summary>
/// <param name="newColor">The new <see cref="VariableColor"/>.</param>
[HideFromIl2Cpp]
public void SetColor(VariableColor? newColor)
{
color = newColor;
}
}
/// <summary>
/// A mono script that adjusts the material colors of a <see cref="Renderer"/> component that it's attached to based on the provided variable color.
/// </summary>
[RegisterInIl2Cpp]
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")]
public sealed class VariableColorBehaviour : BaseVariableColorBehaviour
{
private Renderer? rend;
private static readonly Color VisorColor = Palette.VisorColor;
/// <summary>
/// Sets the new <see cref="Renderer"/> target.
/// </summary>
/// <param name="newRend">The new <see cref="Renderer"/> that the script must target.</param>
public void SetRend(Renderer? newRend)
{
rend = newRend;
}
private void Awake()
{
rend = GetComponent<Renderer>();
}
private void Update()
{
if (color == null || rend == null)
return;
var time = Time.time;
var material = rend.material;
material.SetColor(ShaderID.VisorColor, VisorColor);
material.SetColor(ShaderID.BodyColor, color.EvaluateMainColor(time));
material.SetColor(ShaderID.BackColor, color.EvaluateShadowColor(time));
}
}
/// <summary>
/// A mono script that adjusts the graphic colors of a <see cref="SpriteRenderer"/> component that it's attached to based on the provided variable color.
/// </summary>
[RegisterInIl2Cpp]
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")]
public sealed class BasicVariableColorBehaviour : BaseVariableColorBehaviour
{
private SpriteRenderer? rend;
/// <summary>
/// Sets the new <see cref="SpriteRenderer"/> target.
/// </summary>
/// <param name="newRend">The new <see cref="SpriteRenderer"/> that the script must target.</param>
public void SetRend(SpriteRenderer? newRend)
{
rend = newRend;
}
private void Awake()
{
rend = GetComponent<SpriteRenderer>();
}
private void Update()
{
if (color != null && rend != null)
rend.color = color.EvaluateMainColor(Time.time);
}
}
+6 -8
View File
@@ -4,6 +4,7 @@ using System.Linq;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Networking;
using MiraAPI.Patches;
using MiraAPI.Patches.GameModes;
using MiraAPI.Patches.Options;
using MiraAPI.Translation;
@@ -39,7 +40,7 @@ public static class GameModeOption
}
}
internal static readonly StringNames GamemodeName = MiraLocaleManager.GetOrCreateLocaleString("Gamemode");
internal static readonly StringNames GamemodeName = MiraLocaleManager.GetOrCreateLocaleString("MiraApi.Gamemode");
internal static readonly StringNames CustomName = MiraLocaleManager.GetOrCreateLocaleString("Custom");
internal static readonly Dictionary<uint, StringNames> Values = new()
{
@@ -128,6 +129,9 @@ public static class GameModeOption
MenuState.Instance.QueuedRoleMenuRefresh[roleMenu.Key] = true;
}
}
GameOptionsMenuPatch.RefreshForGameMode();
RoleGuidePatches.RefreshSettingsForGameMode();
}
// TODO: could make Values a dict of AbstractGameMode too
@@ -135,7 +139,7 @@ public static class GameModeOption
[HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))]
[HarmonyPrefix]
private static bool ValueChanged(GameOptionsMenu __instance, OptionBehaviour option)
private static bool ValueChanged(OptionBehaviour option)
{
if (!OptionBehaviour.Equals(option))
return true;
@@ -143,12 +147,6 @@ public static class GameModeOption
Info($"Game mode changed to {option.GetInt()}");
RpcSyncGamemode(PlayerControl.LocalPlayer, option.GetInt());
if (GameSettingMenu.Instance && CustomGameModeManager.ActiveMode != null)
{
GameOptionsMenuPatch.ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, __instance);
GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(CustomGameModeManager.ActiveMode.ShowNormalRoleSettings);
}
return false;
}
-2
View File
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using AmongUs.Data;
@@ -462,7 +461,6 @@ public class HideAndSeekMode : AbstractGameMode
}
/// <inheritdoc/>
[SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Warning cascades into forcing the entire tree to be ternary operators.")]
public override PlayerBodyTypes GetBodyType(PlayerControl player)
{
if (player == null || player.Data == null || player.Data.Role == null)
+46 -1
View File
@@ -3,8 +3,11 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.GameOptions;
@@ -26,6 +29,12 @@ public abstract class AbstractOptionGroup
/// </summary>
public abstract string GroupName { get; }
/// <summary>
/// Gets the sprite of the group that is displayed in the match info guide.
/// </summary>
[HideFromIl2Cpp]
public virtual LoadableAsset<Sprite> Sprite => MiraAssets.IconSettings;
/// <summary>
/// Gets the Optionable type of the group.
/// </summary>
@@ -47,7 +56,43 @@ public abstract class AbstractOptionGroup
/// <summary>
/// Gets the function that determines whether the group should be visible or not.
/// </summary>
public virtual Func<bool> GroupVisible => () => true;
public virtual Func<bool> GroupVisible => DelegateUtilities.AlwaysTrue;
/// <summary>
/// Gets a value indicating whether this group's options are shown in the Match Info Guide.
/// </summary>
public virtual bool ShowInMatchInfoGuide => ParentMenu is MenuCategory.Game;
/// <summary>
/// Gets the name title of the group when it is in the match info guide.
/// </summary>
public virtual string RoleGuideTitle => GroupName;
/// <summary>
/// Gets the key used to combine this group with other option groups on one Match Info Guide page.
/// Groups are combined only with groups from the same plugin that use the same non-empty key.
/// </summary>
public virtual string? MatchInfoGuidePageKey => null;
/// <summary>
/// Gets the title of the Match Info Guide page. For groups sharing a page key, the first
/// group in menu order supplies the page title.
/// </summary>
public virtual string MatchInfoGuidePageTitle => RoleGuideTitle;
/// <summary>
/// Gets the ordering priority of this Match Info Guide page. Lower values appear first
/// among pages with the same menu category and group priority. Groups sharing a page key
/// use the lowest page-order value among those groups.
/// </summary>
public virtual int MatchInfoGuidePageOrder => int.MaxValue;
/// <summary>
/// Gets the number of columns used to lay out options on this Match Info Guide page.
/// For a combined page, groups are placed into these columns. Values are limited to 1-6;
/// for groups sharing a page key, the first group in menu order supplies this value.
/// </summary>
public virtual int MatchInfoGuidePageColumns => 2;
/// <summary>
/// Gets the group <see cref="Color"/>. This is used to color the group in the options menu.
@@ -0,0 +1,50 @@
using System;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute to set the <see cref="IModdedOption.ChangedEvent"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
/// </summary>
/// <param name="methodName">The method to get the changed event from.</param>
/// <param name="holderType">The type the method is in.</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ModdedOptionChangedAttribute(string? methodName = null, Type? holderType = null)
: ModdedOptionMemberAttribute(methodName, holderType, "Changed")
{
internal Delegate? GetChanged(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
var valueType = option.GetValueType();
if (!ValidateSignature(method, typeof(void), valueType))
return null;
var delegateType = typeof(Action<>).MakeGenericType(valueType);
return CompileDynamicDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "Changed");
}
internal Func<int, Delegate?>? GetListChanged(AbstractOptionGroup group, PropertyInfo property, Type valueType)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
if (!ValidateListSignature(method, typeof(void), out var hasIndex, valueType))
return null;
var delegateType = typeof(Action<>).MakeGenericType(valueType);
return CompileDynamicListDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "ListChanged", hasIndex);
}
}
@@ -0,0 +1,50 @@
using System;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute to set the <see cref="IModdedOption.Getter"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
/// </summary>
/// <param name="methodName">The method to get the getter from.</param>
/// <param name="holderType">The type the method is in.</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ModdedOptionGetterAttribute(string? methodName = null, Type? holderType = null)
: ModdedOptionMemberAttribute(methodName, holderType, "Getter")
{
internal Delegate? GetGetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
var valueType = option.GetValueType();
if (!ValidateSignature(method, valueType, valueType))
return null;
var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType);
return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "Getter");
}
internal Func<int, Delegate?>? GetListGetter(AbstractOptionGroup group, PropertyInfo property, Type valueType)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
if (!ValidateListSignature(method, valueType, out var hasIndex, valueType))
return null;
var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType);
return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "ListGetter", hasIndex);
}
}
@@ -0,0 +1,428 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using MiraAPI.Utilities;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Base attribute for resolving and compiling option delegate members (<see cref="IModdedOption.Visible"/>, <see cref="IModdedOption.Getter"/>, <see cref="IModdedOption.Setter"/>, and <see cref="IModdedOption.ChangedEvent"/>).
/// </summary>
/// <param name="memberName">The name of the target member, or <see langword="null"/> to use the property name with <paramref name="defaultSuffix"/>.</param>
/// <param name="holderType">The type containing the target member, or <see langword="null"/> to use the declaring <see cref="AbstractOptionGroup"/> type.</param>
/// <param name="defaultSuffix">The default suffix appended to the property name when <paramref name="memberName"/> is not specified.</param>
[AttributeUsage(AttributeTargets.Property)]
public abstract class ModdedOptionMemberAttribute(string? memberName, Type? holderType, string defaultSuffix) : Attribute
{
private static readonly Dictionary<MethodInfo, Delegate> DelegateCache = [];
/// <summary>
/// A helper class to bind a specific list index to a dynamically compiled delegate.
/// </summary>
/// <param name="index">The index to bind.</param>
public sealed class DelegateIndexClosure(int index)
{
/// <summary>
/// The bound index.
/// </summary>
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "No need to be private.")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
public readonly int Index = index;
}
/// <summary>
/// Exposes the index field so child attributes can emit instructions to read it.
/// </summary>
protected static readonly FieldInfo IndexClosureField = typeof(DelegateIndexClosure).GetField(nameof(DelegateIndexClosure.Index))!;
/// <summary>
/// Gets the resolved name of the target member.
/// </summary>
protected string? MemberName { get; private set; } = memberName;
/// <summary>
/// Validates that a method matches the expected return type and parameter types.
/// </summary>
/// <param name="method">The method to validate.</param>
/// <param name="expectedReturnType">The expected return type of the method.</param>
/// <param name="expectedParamTypes">The expected types of the parameters of the method.</param>
/// <returns><c>true</c> if the method is valid; <c>false</c> otherwise.</returns>
protected bool ValidateSignature(MethodInfo method, Type expectedReturnType, params Type[] expectedParamTypes)
{
if (method.ReturnType != expectedReturnType)
{
Error($"{MemberName}'s return type must be {expectedReturnType.Name}.");
return false;
}
var parameters = method.GetParameters();
if (parameters.Length != expectedParamTypes.Length)
{
Error($"{MemberName} must have exactly {expectedParamTypes.Length} parameter(s).");
return false;
}
for (var i = 0; i < expectedParamTypes.Length; i++)
{
if (parameters[i].ParameterType == expectedParamTypes[i])
continue;
Error($"{MemberName}'s parameter at index {i} must be {expectedParamTypes[i].Name}.");
return false;
}
return true;
}
/// <summary>
/// Validates that a list method matches the expected return type, base parameter types, and an optional integer index.
/// </summary>
/// <param name="method">The method to validate.</param>
/// <param name="expectedReturnType">The expected return type of the method.</param>
/// <param name="hasIndex">When the method returns, a flag that indicates if the method has an index parameter.</param>
/// <param name="expectedParamTypes">The expected types of the parameters of the method.</param>
/// <returns><c>true</c> if the method is valid; <c>false</c> otherwise.</returns>
protected bool ValidateListSignature(MethodInfo method, Type expectedReturnType, out bool hasIndex, params Type[] expectedParamTypes)
{
hasIndex = false;
if (method.ReturnType != expectedReturnType)
{
Error($"{MemberName}'s return type must be {expectedReturnType.Name}.");
return false;
}
var parameters = method.GetParameters();
if (parameters.Length < expectedParamTypes.Length || parameters.Length > expectedParamTypes.Length + 1)
{
Error($"{MemberName} must have {expectedParamTypes.Length} parameter(s), optionally with an extra trailing int parameter.");
return false;
}
for (var i = 0; i < expectedParamTypes.Length; i++)
{
if (parameters[i].ParameterType == expectedParamTypes[i])
continue;
Error($"{MemberName}'s parameter at index {i} must be {expectedParamTypes[i].Name}.");
return false;
}
if (parameters.Length == expectedParamTypes.Length + 1)
{
if (parameters[^1].ParameterType != typeof(int))
{
Error($"{MemberName}'s final parameter must be an int (the list index).");
return false;
}
hasIndex = true;
}
return true;
}
/// <summary>
/// Resolves the target <see cref="MemberInfo"/> (property, field or method) and its associated singleton instance getter, if applicable.
/// </summary>
/// <param name="group">The option group instance the property belongs to.</param>
/// <param name="property">The option property decorated with this attribute.</param>
/// <param name="type">When this method returns, contains the resolved <see cref="Type"/> expected to hold the target member.</param>
/// <param name="instanceGetter">
/// When this method returns, contains the <see cref="OptionGroupSingleton{T}.Instance"/> getter <see cref="MethodInfo"/>
/// if the resolved member is an instance member on an <see cref="AbstractOptionGroup"/>; otherwise, <see langword="null"/>.
/// </param>
/// <param name="notFound">When this method returns, contains a flag that indicates if the member was not found, as null can be returned from other circumstances too.</param>
/// <returns>The resolved <see cref="MemberInfo"/> if found; otherwise, <see langword="null"/>.</returns>
[SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "It is safe.")]
protected MemberInfo? ResolveMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter, out bool notFound)
{
var groupType = group.GetType();
var flags = BindingFlags.Static | BindingFlags.Public;
MemberName ??= property.Name + defaultSuffix;
type = holderType ?? groupType;
instanceGetter = null;
if (type.IsAssignableTo(typeof(AbstractOptionGroup)))
{
flags |= BindingFlags.Instance;
if (type == groupType)
flags |= BindingFlags.NonPublic;
instanceGetter = typeof(OptionGroupSingleton<>)
.MakeGenericType(type)
.GetProperty(nameof(OptionGroupSingleton<>.Instance))!
.GetGetMethod();
}
else if (!type.IsStatic())
{
flags |= BindingFlags.Instance;
var canSearchPrivates = type.GetCustomAttribute<SearchPrivateAttribute>() != null;
if (canSearchPrivates)
flags |= BindingFlags.NonPublic;
var getterFlags = BindingFlags.Static | BindingFlags.Public | (canSearchPrivates ? BindingFlags.NonPublic : BindingFlags.Default);
foreach (var method in type.GetMethods(getterFlags))
{
if (method.GetParameters().Length > 0 || (method.GetCustomAttribute<InstanceGetterAttribute>() == null && method.Name != "GetInstance"))
continue;
instanceGetter = method;
break;
}
if (instanceGetter == null)
{
foreach (var prop in type.GetProperties(getterFlags))
{
if (prop.GetCustomAttribute<InstanceGetterAttribute>() == null && prop.Name != "Instance")
continue;
var getMethod = prop.GetGetMethod(canSearchPrivates);
if (getMethod == null)
continue;
instanceGetter = getMethod;
break;
}
}
if (instanceGetter == null)
{
Error($"No valid static instance getter (method GetInstance or property Instance) found in instanced type {type.Name}. Try renaming the method/property, or decorate it with InstanceGetterAttribute.");
notFound = false;
return null;
}
if (!instanceGetter.ReturnType.IsAssignableTo(type))
{
Error($"Found instance getter {instanceGetter.Name}, but the return type did not match what was expected ({type.Name}).");
notFound = false;
return null;
}
}
else if (type.GetCustomAttribute<SearchPrivateAttribute>() != null)
{
flags |= BindingFlags.NonPublic;
}
var member = type.GetMember(MemberName, flags).FirstOrDefault();
notFound = true;
if (member == null)
return null;
notFound = false;
if (instanceGetter == null)
return member;
bool isStatic;
if (member is FieldInfo field)
{
isStatic = field.IsStatic;
}
else
{
var method = (member as PropertyInfo)?.GetGetMethod(true) ?? member as MethodInfo;
isStatic = method is { IsStatic: true };
}
if (isStatic)
instanceGetter = null;
return member;
}
/// <summary>
/// Resolves the target <see cref="MethodInfo"/> and its associated singleton instance getter, if applicable.
/// </summary>
/// <param name="group">The option group instance the property belongs to.</param>
/// <param name="property">The option property decorated with this attribute.</param>
/// <param name="type">When this method returns, contains the resolved <see cref="Type"/> expected to hold the target method.</param>
/// <param name="instanceGetter">
/// When this method returns, contains the <see cref="OptionGroupSingleton{T}.Instance"/> getter <see cref="MethodInfo"/>
/// if the resolved method is an instance method on an <see cref="AbstractOptionGroup"/>; otherwise, <see langword="null"/>.
/// </param>
/// <param name="notFound">When this method returns, contains a flag that indicates if the member was not found, as null can be returned from other circumstances too.</param>
/// <returns>The resolved <see cref="MethodInfo"/> if found; otherwise, <see langword="null"/>.</returns>
protected MethodInfo? ResolveMethod(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter, out bool notFound)
{
return ResolveMember(group, property, out type, out instanceGetter, out notFound) as MethodInfo;
}
/// <summary>
/// Compiles a <see cref="DynamicMethod"/> using IL generation to invoke the resolved static or instance <paramref name="method"/>.
/// </summary>
/// <param name="ownerType">The <see cref="Type"/> that owns the generated <see cref="DynamicMethod"/> and holds the target method.</param>
/// <param name="method">The target <see cref="MethodInfo"/> to invoke.</param>
/// <param name="instanceGetter">
/// The static getter <see cref="MethodInfo"/> used to push the target instance onto the evaluation stack for instance methods,
/// or <see langword="null"/> if <paramref name="method"/> is static.
/// </param>
/// <param name="returnType">The return <see cref="Type"/> of the generated delegate (or <see cref="void"/> for actions).</param>
/// <param name="paramTypes">The parameter types accepted by the generated delegate.</param>
/// <param name="delegateType">The concrete delegate <see cref="Type"/> to create (e.g., <see cref="Func{T, TResult}"/> or <see cref="Action{T}"/>).</param>
/// <param name="suffix">A descriptive suffix appended to the generated <see cref="DynamicMethod"/> name for debugging and stack traces.</param>
/// <param name="forwardParameters">Whether to push the delegate parameters onto the stack before invoking <paramref name="method"/>.</param>
/// <param name="postProcessCall">An optional callback to emit additional IL instructions before returning.</param>
/// <returns>A compiled <see cref="Delegate"/> of type <paramref name="delegateType"/> bound to the generated IL method.</returns>
protected static Delegate CompileDynamicDelegate(
Type ownerType,
MethodInfo method,
MethodInfo? instanceGetter,
Type returnType,
Type[] paramTypes,
Type delegateType,
string suffix,
bool forwardParameters = true,
Action<ILGenerator>? postProcessCall = null)
{
if (postProcessCall == null && DelegateCache.TryGetValue(method, out var cachedDelegate))
return cachedDelegate;
var dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}", returnType, paramTypes, ownerType, true);
var il = dm.GetILGenerator();
if (!method.IsStatic && instanceGetter != null)
il.Emit(OpCodes.Call, instanceGetter);
if (forwardParameters)
{
for (byte i = 0; i < paramTypes.Length; i++)
{
var loadOp = i switch
{
0 => OpCodes.Ldarg_0,
1 => OpCodes.Ldarg_1,
2 => OpCodes.Ldarg_2,
3 => OpCodes.Ldarg_3,
_ => OpCodes.Ldarg_S,
};
if (loadOp == OpCodes.Ldarg_S)
il.Emit(loadOp, i);
else
il.Emit(loadOp);
}
}
var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt;
il.Emit(callOp, method);
postProcessCall?.Invoke(il);
il.Emit(OpCodes.Ret);
var compiledDelegate = dm.CreateDelegate(delegateType);
if (postProcessCall == null)
DelegateCache[method] = compiledDelegate;
return compiledDelegate;
}
/// <summary>
/// Compiles a factory function that provides a <see cref="Delegate"/> for each index in an option list.
/// If <paramref name="hasIndexParameter"/> is <see langword="false"/> and <paramref name="postProcessListCall"/> is <see langword="null"/>,
/// a single <see cref="Delegate"/> is compiled once and reused for all indices.
/// </summary>
/// <param name="ownerType">The <see cref="Type"/> that owns the generated <see cref="DynamicMethod"/> and holds the target method.</param>
/// <param name="method">The target <see cref="MethodInfo"/> to invoke.</param>
/// <param name="instanceGetter">
/// The static getter <see cref="MethodInfo"/> used to push the target instance onto the evaluation stack for instance methods,
/// or <see langword="null"/> if <paramref name="method"/> is static.
/// </param>
/// <param name="returnType">The return <see cref="Type"/> of the generated delegate (or <see cref="void"/> for actions).</param>
/// <param name="baseParamTypes">The parameter types accepted by the generated delegate.</param>
/// <param name="delegateType">The concrete delegate <see cref="Type"/> to create.</param>
/// <param name="suffix">A descriptive suffix appended to the generated <see cref="DynamicMethod"/> name for debugging and stack traces.</param>
/// <param name="hasIndexParameter">Whether the target <paramref name="method"/> takes the list <see langword="int"/> index as its final parameter.</param>
/// <param name="postProcessCall">An optional index-independent callback to emit additional IL instructions after <paramref name="method"/> is called.</param>
/// <param name="postProcessListCall">An optional index-dependent callback to emit additional IL instructions after <paramref name="method"/> is called.</param>
/// <returns>A factory function that returns a compiled <see cref="Delegate"/> of type <paramref name="delegateType"/> for a given list index.</returns>
protected static Func<int, Delegate> CompileDynamicListDelegate(
Type ownerType,
MethodInfo method,
MethodInfo? instanceGetter,
Type returnType,
Type[] baseParamTypes,
Type delegateType,
string suffix,
bool hasIndexParameter,
Action<ILGenerator>? postProcessCall = null,
Action<ILGenerator>? postProcessListCall = null)
{
if (!hasIndexParameter && postProcessListCall is null)
{
var sharedDelegate = CompileDynamicDelegate(
ownerType,
method,
instanceGetter,
returnType,
baseParamTypes,
delegateType,
suffix,
baseParamTypes.Length > 0,
postProcessCall);
return _ => sharedDelegate;
}
var fullParamTypes = new Type[baseParamTypes.Length + 1];
fullParamTypes[0] = typeof(DelegateIndexClosure);
Array.Copy(baseParamTypes, 0, fullParamTypes, 1, baseParamTypes.Length);
var dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}_List", returnType, fullParamTypes, ownerType, true);
var il = dm.GetILGenerator();
if (!method.IsStatic && instanceGetter != null)
il.Emit(OpCodes.Call, instanceGetter);
for (var i = 0; i < baseParamTypes.Length; i++)
{
var argPos = (byte)(i + 1);
var loadOp = argPos switch
{
1 => OpCodes.Ldarg_1,
2 => OpCodes.Ldarg_2,
3 => OpCodes.Ldarg_3,
_ => OpCodes.Ldarg_S,
};
if (loadOp == OpCodes.Ldarg_S)
il.Emit(loadOp, argPos);
else
il.Emit(loadOp);
}
if (hasIndexParameter)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, IndexClosureField);
}
var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt;
il.Emit(callOp, method);
postProcessCall?.Invoke(il);
postProcessListCall?.Invoke(il);
il.Emit(OpCodes.Ret);
return index => dm.CreateDelegate(delegateType, new DelegateIndexClosure(index));
}
}
@@ -0,0 +1,50 @@
using System;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute to set the <see cref="IModdedOption.Setter"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
/// </summary>
/// <param name="methodName">The method to get the setter from.</param>
/// <param name="holderType">The type the method is in.</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ModdedOptionSetterAttribute(string? methodName = null, Type? holderType = null)
: ModdedOptionMemberAttribute(methodName, holderType, "Setter")
{
internal Delegate? GetSetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
var valueType = option.GetValueType();
if (!ValidateSignature(method, valueType, valueType, valueType))
return null;
var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType);
return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "Setter");
}
internal Func<int, Delegate?>? GetListSetter(AbstractOptionGroup group, PropertyInfo property, Type valueType)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
if (!ValidateListSignature(method, valueType, out var hasIndex, valueType, valueType))
return null;
var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType);
return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "ListSetter", hasIndex);
}
}
@@ -0,0 +1,257 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using MiraAPI.GameOptions.OptionTypes;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute to set the <see cref="IModdedOption.Visible"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
/// </summary>
/// <param name="memberName">The member to get the visibility function from.</param>
/// <param name="holderType">The type the member is in.</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, Type? holderType = null)
: ModdedOptionMemberAttribute(memberName, holderType, "Visibility")
{
private static readonly MethodInfo ModdedOptionBoolValueGetter = typeof(ModdedOption<bool>).GetProperty(nameof(ModdedOption<>.Value))!.GetGetMethod()!;
private static readonly MethodInfo ListBoolIndexerGetter = typeof(IList<bool>).GetProperty("Item")!.GetGetMethod()!;
internal Func<bool>? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
{
if (ResolveMember(group, property, out Type type, out var instanceGetter, out var notFound) is not { } member)
{
if (notFound)
Error($"Member {MemberName} does not exist in {type.FullName}");
return null;
}
MethodInfo? targetMethod;
Action<ILGenerator>? postProcess = null;
if (member is PropertyInfo vProperty)
{
if (vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)))
{
postProcess = il => il.Emit(OpCodes.Callvirt, ModdedOptionBoolValueGetter);
}
else if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)))
{
Error($"Property {MemberName} is not a bool or a bool ModdedOption.");
return null;
}
if (vProperty.GetIndexParameters().Length > 0)
{
Error($"Property {MemberName} cannot be an indexer.");
return null;
}
targetMethod = vProperty.GetGetMethod(true);
if (targetMethod is null)
{
Error($"Property {MemberName} does not have a getter.");
return null;
}
}
else if (member is MethodInfo vMethod)
{
if (vMethod.ReturnType != typeof(bool))
{
Error($"Method {MemberName} does not return a bool.");
return null;
}
if (vMethod.GetParameters().Length > 0)
{
Error($"Method {MemberName} has too many parameters.");
return null;
}
targetMethod = vMethod;
}
else if (member is FieldInfo field)
{
if (field.FieldType != typeof(bool))
{
Error($"Field {MemberName} is not a bool.");
return null;
}
var dynamicMethod = new DynamicMethod($"GetVisibility_{type.Name}_{field.Name}", typeof(bool), Type.EmptyTypes, type, true);
var il = dynamicMethod.GetILGenerator();
if (instanceGetter != null)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, instanceGetter);
il.Emit(OpCodes.Ldfld, field);
}
else
{
il.Emit(OpCodes.Ldsfld, field);
}
il.Emit(OpCodes.Ret);
return dynamicMethod.CreateDelegate<Func<bool>>();
}
else
{
Error($"Member {MemberName} of {type.FullName} is not valid for Visible function.");
return null;
}
return (Func<bool>)CompileDynamicDelegate(type, targetMethod, instanceGetter, typeof(bool), Type.EmptyTypes, typeof(Func<bool>), "Visibility", false, postProcess);
}
internal Func<int, Delegate?>? GetListVisibility(AbstractOptionGroup group, PropertyInfo property)
{
if (ResolveMember(group, property, out Type type, out var instanceGetter, out var notFound) is not { } member)
{
if (notFound)
Error($"Member {MemberName} does not exist in {type.FullName}");
return null;
}
MethodInfo? targetMethod;
Action<ILGenerator>? postProcess = null;
Action<ILGenerator>? postProcessList = null;
var hasIndexParam = false;
if (member is PropertyInfo vProperty)
{
var isListBool = false;
if (vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
{
isListBool = true;
postProcessList = il =>
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, IndexClosureField);
il.Emit(OpCodes.Callvirt, ListBoolIndexerGetter);
};
}
else if (vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)))
{
postProcess = il => il.Emit(OpCodes.Callvirt, ModdedOptionBoolValueGetter);
}
else if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)))
{
Error($"Property {MemberName} is not a bool value or indexer, a list of bools, or a bool ModdedOption.");
return null;
}
var indexParams = vProperty.GetIndexParameters();
if (indexParams.Length > 1)
{
Error($"Indexer {MemberName} has too many parameters.");
return null;
}
if (isListBool && indexParams.Length != 0)
{
Error($"Property {MemberName} cannot be an indexer of lists of bools.");
return null;
}
if (indexParams.Length == 1)
{
if (indexParams[0].ParameterType != typeof(int))
{
Error($"Indexer {MemberName}'s parameter is not an int.");
return null;
}
hasIndexParam = true;
}
targetMethod = vProperty.GetGetMethod(true);
if (targetMethod is null)
{
Error($"Property {MemberName} does not have a getter.");
return null;
}
}
else if (member is MethodInfo vMethod)
{
if (vMethod.ReturnType != typeof(bool))
{
Error($"Method {MemberName} does not return a bool.");
return null;
}
var paramList = vMethod.GetParameters();
if (paramList.Length > 1)
{
Error($"Method {MemberName} has too many parameters.");
return null;
}
if (paramList.Length == 1)
{
if (paramList[0].ParameterType != typeof(int))
{
Error($"Method {MemberName}'s parameter is not an int.");
return null;
}
hasIndexParam = true;
}
targetMethod = vMethod;
}
else if (member is FieldInfo field)
{
if (field.FieldType.IsAssignableTo(typeof(IList<bool>)))
{
postProcess = il =>
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, ListBoolIndexerGetter);
};
}
else if (field.FieldType != typeof(bool))
{
Error($"Field {MemberName} is not a bool or a list of bools.");
return null;
}
var dynamicMethod = new DynamicMethod($"GetListVisibility_{type.Name}_{field.Name}", typeof(bool), [typeof(int)], type, true);
var il = dynamicMethod.GetILGenerator();
if (instanceGetter != null)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, instanceGetter);
il.Emit(OpCodes.Ldfld, field);
}
else
{
il.Emit(OpCodes.Ldsfld, field);
}
postProcess?.Invoke(il);
il.Emit(OpCodes.Ret);
var func = dynamicMethod.CreateDelegate<Func<int, bool>>();
return index => () => func(index);
}
else
{
Error($"Member {MemberName} of {type.FullName} is not valid for Visibility function.");
return null;
}
return CompileDynamicListDelegate(type, targetMethod, instanceGetter, typeof(bool), Type.EmptyTypes, typeof(Func<bool>), "ListVisibility", hasIndexParam, postProcess, postProcessList);
}
}
@@ -0,0 +1,15 @@
using System;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// An attribute used to mark a method or property as one that returns an instance of the class it's declared in.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
public sealed class InstanceGetterAttribute : Attribute;
/// <summary>
/// An attribute used to mark a class as one whose reflection search should include private members.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class SearchPrivateAttribute : Attribute;
@@ -1,7 +1,6 @@
using System;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Translation;
namespace MiraAPI.GameOptions.Attributes;
@@ -17,7 +16,7 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
var opt = new ModdedEnumOption(Title.Translate(), (int)(value ?? 0), enumType, values);
var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values);
return opt;
}
@@ -33,7 +32,7 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
{
return HolderOption is ModdedEnumOption opt
? Enum.ToObject(enumType, opt.Value)
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption");
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType {enumType.FullName} is not a ModdedEnumOption");
}
}
@@ -41,10 +40,12 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
/// Attribute for creating an enum option.
/// </summary>
/// <typeparam name="T">The enum type.</typeparam>
/// <param name="title">The title of the option.</param>
/// <param name="values">An option list of string values to use in place of the <see langword="enum"/> name.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionAttribute<T>(string title, string[]? values = null)
: ModdedOptionAttribute(title)
where T : Enum
where T : struct, Enum
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
@@ -64,6 +65,6 @@ public class ModdedEnumOptionAttribute<T>(string title, string[]? values = null)
{
return HolderOption is ModdedEnumOption<T> opt
? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption");
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType {typeof(T).FullName} is not a ModdedEnumOption<{typeof(T).Name}>");
}
}
@@ -1,7 +1,6 @@
using System;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Translation;
using MiraAPI.Utilities;
namespace MiraAPI.GameOptions.Attributes;
@@ -13,45 +12,93 @@ namespace MiraAPI.GameOptions.Attributes;
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param>
/// <param name="zeroBehavior">Determines what is shown for zero.</param>
/// <param name="negativeBehavior">Determines what is shown for negative 1.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroInfinity">Flag indicating whether 0 means infinity.</param>
/// <param name="halfIncrements">Whether increments can be split in half (using Shift).</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedNumberOptionAttribute(
string title,
float min,
float max,
float increment = 1,
float increment,
string zeroBehavior,
string negativeBehavior,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null,
bool zeroInfinity = false)
bool halfIncrements = false)
: ModdedOptionAttribute(title)
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOptionAttribute"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
public ModdedNumberOptionAttribute(
string title,
float min,
float max,
float increment = 1,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null,
bool zeroInfinity = false)
: this(title, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f)
{
}
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
var final = value switch
{
null => min + increment,
int integer => integer,
_ => (float)value,
};
return new ModdedNumberOption(
Title.Translate(),
(float)(value ?? min + increment),
Title,
final,
min,
max,
increment,
zeroBehavior,
negativeBehavior,
suffixType,
formatString,
zeroInfinity);
halfIncrements);
}
/// <inheritdoc />
public override void SetValue(object value)
{
var opt = HolderOption as ModdedNumberOption;
opt?.SetValue((float)value);
if (HolderOption is not ModdedNumberOption opt)
return;
var final = value switch
{
int integer => integer,
_ => (float)value,
};
opt.SetValue(final);
}
/// <inheritdoc />
public override object GetValue()
{
return HolderOption is ModdedNumberOption opt
? (object)opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption");
if (HolderOption is not ModdedNumberOption opt)
throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption");
var value = opt.Value;
if (ValueType == typeof(int))
return (int)Math.Round(value);
return value;
}
}
@@ -13,6 +13,7 @@ namespace MiraAPI.GameOptions.Attributes;
public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : PropertyOptionAttribute
{
internal IModdedOption? HolderOption { get; set; }
internal Type ValueType { get; set; }
/// <summary>
/// Gets the title of the option.
@@ -1,201 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute to modify a property with a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="ModdedOptionVisibilityAttribute"/> class.
/// </remarks>
/// <param name="holderType">The type the member is in.</param>
/// <param name="memberName">The member to get the visibility function from.</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ModdedOptionVisibilityAttribute(Type? holderType = null, string? memberName = null) : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOptionVisibilityAttribute"/> class.
/// </summary>
/// <param name="memberName">The member to get the visibility function from.</param>
public ModdedOptionVisibilityAttribute(string memberName)
: this(null, memberName)
{
}
internal Func<bool>? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
{
if (GetVisibilityMember(group, property, out Type type, out var instanceGet) is not { } member)
{
Error($"Member {memberName} does not exist in {type.FullName}");
return null;
}
if (member is PropertyInfo vProperty)
{
if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)) &&
!vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)))
{
Error($"Property {memberName} is not a bool or a bool ModdedOption.");
return null;
}
var indexParams = vProperty.GetIndexParameters();
if (indexParams.Length > 0)
{
Error($"Property {memberName} cannot be an indexer.");
return null;
}
return () => (bool)vProperty.GetValue(instanceGet?.Invoke())!;
}
if (member is MethodInfo vMethod)
{
if (vMethod.ReturnType != typeof(bool))
{
Error($"Method {memberName} does not return a bool.");
return null;
}
var paramList = vMethod.GetParameters();
if (paramList.Length > 0)
{
Error($"Method {memberName} has too many parameters.");
return null;
}
return () => (bool)vMethod.Invoke(instanceGet?.Invoke(), null)!;
}
Error($"Member {memberName} of {type.FullName} is not valid for Visible function.");
return null;
}
internal Func<int, bool>? GetListVisibility(AbstractOptionGroup group, PropertyInfo property)
{
if (GetVisibilityMember(group, property, out Type type, out var instanceGet) is not { } member)
{
Error($"Member {memberName} does not exist in {type.FullName}");
return null;
}
if (member is PropertyInfo vProperty)
{
if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)) &&
!vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)) &&
!vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
{
Error($"Property {memberName} is not a bool value or indexer, a list of bools, or a bool ModdedOption.");
return null;
}
var indexParams = vProperty.GetIndexParameters();
if (indexParams.Length > 1)
{
Error($"Indexer {memberName} has too many parameters.");
return null;
}
if (vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
{
if (indexParams.Length != 0)
{
Error($"Property {memberName} cannot be an indexer of lists of bools.");
return null;
}
return i => ((IList<bool>)vProperty.GetValue(instanceGet?.Invoke())!)[i];
}
if (indexParams.Length == 0)
{
return _ => (bool)vProperty.GetValue(instanceGet?.Invoke())!;
}
if (indexParams[0].ParameterType != typeof(int))
{
Error($"Indexer {memberName}'s parameter is not an int.");
return null;
}
return i => (bool)vProperty.GetValue(instanceGet?.Invoke(), [i])!;
}
if (member is MethodInfo vMethod)
{
if (vMethod.ReturnType != typeof(bool))
{
Error($"Method {memberName} does not return a bool.");
return null;
}
var paramList = vMethod.GetParameters();
if (paramList.Length > 1)
{
Error($"Method {memberName} has too many parameters.");
return null;
}
if (paramList.Length == 0)
{
return _ => (bool)vMethod.Invoke(instanceGet?.Invoke(), null)!;
}
if (paramList[0].ParameterType != typeof(int))
{
Error($"Method {memberName}'s parameter is not an int.");
return null;
}
return i => (bool)vMethod.Invoke(instanceGet?.Invoke(), [i])!;
}
Error($"Member {memberName} of {type.FullName} is not valid for Visible function.");
return null;
}
[SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "It is safe.")]
private MemberInfo? GetVisibilityMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out Func<object>? instanceGet)
{
Type groupType = group.GetType();
BindingFlags flags = BindingFlags.Static | BindingFlags.Public;
instanceGet = null;
type = holderType ?? groupType;
memberName ??= $"{property.Name}Visible";
if (type.IsAssignableTo(typeof(AbstractOptionGroup)))
{
flags |= BindingFlags.Instance;
if (type == groupType)
{
flags |= BindingFlags.NonPublic;
instanceGet = () => group;
}
else
{
var instanceProp = typeof(OptionGroupSingleton<>).MakeGenericType(type)
.GetProperty(nameof(OptionGroupSingleton<>.Instance))!;
instanceGet = () => instanceProp.GetValue(null)!;
}
}
MemberInfo? member = type.GetMember(memberName, flags).FirstOrDefault();
if (instanceGet != null)
{
MethodInfo? method = (member as PropertyInfo)?.GetGetMethod()
?? member as MethodInfo;
if (method != null && method.IsStatic)
{
instanceGet = null;
}
}
return member;
}
}
@@ -0,0 +1,49 @@
using System;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute for a <see cref="ModdedPlayerOption"/>.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="allowNone">Whether the option includes a none option. If None is selected, value will return -1.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedPlayerOptionAttribute(
string title,
bool allowNone = true)
: ModdedOptionAttribute(title)
{
internal override IModdedOption? CreateOption(object? value, PropertyInfo property)
{
var option = new ModdedPlayerOption(Title, allowNone);
return option;
}
/// <inheritdoc />
public override object GetValue()
{
if (HolderOption is not ModdedPlayerOption opt)
throw new InvalidOperationException($"Holder option for {Title} is not a ModdedPlayerOption.");
if (ValueType == typeof(int))
return opt.Value;
return opt.SelectedPlayer!;
}
/// <inheritdoc />
public override void SetValue(object value)
{
if (HolderOption is not ModdedPlayerOption opt)
return;
var final = value switch
{
NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : (allowNone ? -1 : 0),
_ => (int)value,
};
opt?.SetValue(final);
}
}
@@ -0,0 +1,38 @@
using System;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute for a <see cref="ModdedStringOption"/>.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="values">An option list of string value.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedStringOptionAttribute(
string title,
string[] values)
: ModdedOptionAttribute(title)
{
internal override IModdedOption? CreateOption(object? value, PropertyInfo property)
{
var option = new ModdedStringOption(Title, (string)(value ?? string.Empty), values);
return option;
}
/// <inheritdoc />
public override object GetValue()
{
return HolderOption is ModdedStringOption opt
? opt.Value
: throw new InvalidOperationException($"Holder option for {Title} is not a ModdedStringOption.");
}
/// <inheritdoc />
public override void SetValue(object value)
{
var opt = HolderOption as ModdedStringOption;
opt?.SetValue((string)value);
}
}
@@ -1,7 +1,6 @@
using System;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Translation;
namespace MiraAPI.GameOptions.Attributes;
@@ -9,12 +8,27 @@ namespace MiraAPI.GameOptions.Attributes;
/// Attribute for a <see cref="ModdedToggleOption"/>.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="on">What the option displays when toggled on.</param>
/// <param name="off">What the option displays when toggled off.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(title)
public class ModdedToggleOptionAttribute(
string title,
string on,
string off)
: ModdedOptionAttribute(title)
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOptionAttribute"/> class.
/// </summary>
/// <param name="title">The option title.</param>
public ModdedToggleOptionAttribute(string title)
: this(title, "On", "Off")
{
}
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
var toggleOpt = new ModdedToggleOption(Title.Translate(), (bool)(value ?? false));
var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), on, off);
return toggleOpt;
}
@@ -29,7 +43,7 @@ public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(t
public override object GetValue()
{
return HolderOption is ModdedToggleOption opt
? (object)opt.Value
? opt.Value
: throw new InvalidOperationException($"Holder option for {Title} is not a ModdedToggleOption.");
}
}
@@ -8,6 +8,9 @@ namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute for creating a list of enum options.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="enumType">The <see cref="Enum"/> type.</param>
/// <param name="values">An option list of string values to use in place of the <see langword="enum"/> name.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]? values = null)
: ModdedOptionListAttribute(title)
@@ -31,7 +34,7 @@ public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]
{
return HolderOptionList?[idx] is ModdedEnumOption opt
? Enum.ToObject(enumType, opt.Value)
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption");
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType {enumType.FullName} is not a ModdedEnumOption");
}
}
@@ -39,10 +42,12 @@ public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]
/// Attribute for creating a list of enum options.
/// </summary>
/// <typeparam name="T">The enum type.</typeparam>
/// <param name="title">The option's title.</param>
/// <param name="values">The option's possible values.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionListAttribute<T>(string title, string[]? values = null)
: ModdedOptionListAttribute(title)
where T : Enum
where T : struct, Enum
{
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{
@@ -63,6 +68,6 @@ public class ModdedEnumOptionListAttribute<T>(string title, string[]? values = n
{
return HolderOptionList?[idx] is ModdedEnumOption<T> opt
? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption");
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType {typeof(T).FullName} is not a ModdedEnumOption<{typeof(T).FullName}>");
}
}
@@ -7,47 +7,102 @@ using MiraAPI.Utilities;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// A number option attribute for the modded options system.
/// Attribute for creating a list of number options.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param>
/// <param name="zeroBehavior">Determines what is shown for zero.</param>
/// <param name="negativeBehavior">Determines what is shown for negative 1.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="halfIncrements">Whether increments can be split in half (using Shift).</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedNumberOptionListAttribute(
string title,
float min,
float max,
float increment = 1,
float increment,
string zeroBehavior,
string negativeBehavior,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null,
bool zeroInfinity = false)
bool halfIncrements = false)
: ModdedOptionListAttribute(title)
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOptionListAttribute"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
public ModdedNumberOptionListAttribute(
string title,
float min,
float max,
float increment = 1,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null,
bool zeroInfinity = false)
: this(title, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f)
{
}
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{
return new ModdedOptionList<ModdedNumberOption>(value.Count, idx =>
{
var final = value[idx] switch
{
null => min + increment,
int integer => integer,
_ => (float)value[idx]!,
};
return new(
GetFormattedTitle(idx),
(float)(value[idx] ?? min + increment),
final,
min,
max,
increment,
zeroBehavior,
negativeBehavior,
suffixType,
formatString,
zeroInfinity);
halfIncrements);
});
}
/// <inheritdoc />
public override void SetValue(int idx, object value)
{
var opt = HolderOptionList?[idx] as ModdedNumberOption;
opt?.SetValue((float)value);
if (HolderOptionList?[idx] is not ModdedNumberOption opt)
return;
var final = value switch
{
int integer => integer,
_ => (float)value,
};
opt.SetValue(final);
}
/// <inheritdoc />
public override object GetValue(int idx)
{
return HolderOptionList?[idx] is ModdedNumberOption opt
? (object)opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedNumberOption");
if (HolderOptionList?[idx] is not ModdedNumberOption opt)
throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedNumberOption");
var value = opt.Value;
if (ValueType == typeof(int))
return (int)Math.Round(value);
return value;
}
}
@@ -3,6 +3,7 @@ using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.RegularExpressions;
using MiraAPI.Translation;
namespace MiraAPI.GameOptions.Attributes;
@@ -14,8 +15,8 @@ namespace MiraAPI.GameOptions.Attributes;
public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAttribute
{
internal IModdedOptionList? HolderOptionList { get; set; }
internal object? Value { get; set; }
internal IList? Value { get; set; }
internal Type ValueType { get; set; }
/// <summary>
/// Gets the title of the options.
@@ -31,16 +32,16 @@ public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAt
/// <param name="value">The new values as an object.</param>
public override void SetValue(object value)
{
Value = value;
var list = (IList)value;
if (list.Count != HolderOptionList!.Count)
Value = (IList)value;
if (Value.Count != HolderOptionList!.Count)
{
throw new InvalidOperationException($"Value set to option list cannot change the list's length.");
}
for (int i = 0; i < list.Count; i++)
for (int i = 0; i < Value.Count; i++)
{
SetValue(i, list[i]!);
SetValue(i, Value[i]!);
}
}
@@ -77,18 +78,19 @@ public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAt
[SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "I don't even know if this is worth it.")]
protected string GetFormattedTitle(int index)
{
Match match = Regex.Match(Title, @"\{(\d+)(?::[^}]+)?\}", default, Regex.InfiniteMatchTimeout);
var match = Regex.Match(Title.Translate(), @"\{(\d+)(?::[^}]+)?\}", default, Regex.InfiniteMatchTimeout);
if (match.Success)
if (!match.Success)
{
if (match.Groups.Count == 1 && match.Groups[0].Value == "0")
{
return string.Format(Title, index);
}
Error("ModdedOptionList Title cannot contain more than the first parameter for formatting.");
return Title + index;
}
if (match.Value == "{0}" && !match.NextMatch().Success)
{
return string.Format(Title, index);
}
Error($"ModdedOptionList Title of {Title} cannot contain more than the first parameter for formatting.");
return Title + index;
}
}
@@ -0,0 +1,52 @@
using System;
using System.Collections;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute for creating a list of player options.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="allowNone">Whether the option includes a none option. If None is selected, value will return -1.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedPlayerOptionListAttribute(
string title,
bool allowNone = true)
: ModdedOptionListAttribute(title)
{
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{
return new ModdedOptionList<ModdedPlayerOption>(value.Count, idx =>
{
return new(GetFormattedTitle(idx), allowNone);
});
}
/// <inheritdoc />
public override void SetValue(int idx, object value)
{
if (HolderOptionList?[idx] is not ModdedPlayerOption opt)
return;
var final = value switch
{
NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : (allowNone ? -1 : 0),
_ => (int)value,
};
opt?.SetValue(final);
}
/// <inheritdoc />
public override object GetValue(int idx)
{
if (HolderOptionList?[idx] is not ModdedPlayerOption opt)
throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedPlayerOption");
if (ValueType == typeof(int))
return opt.Value;
return opt.SelectedPlayer!;
}
}
@@ -0,0 +1,42 @@
using System;
using System.Collections;
using System.Reflection;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Translation;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Attribute for creating a list of string options.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="values">An option list of string value.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedStringOptionListAttribute(
string title,
string[] values)
: ModdedOptionListAttribute(title)
{
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{
return new ModdedOptionList<ModdedStringOption>(value.Count, idx =>
{
return new ModdedStringOption(Title.Translate(), (string)(value[idx] ?? string.Empty), values);
});
}
/// <inheritdoc />
public override void SetValue(int idx, object value)
{
var opt = HolderOptionList?[idx] as ModdedStringOption;
opt?.SetValue((string)value);
}
/// <inheritdoc />
public override object GetValue(int idx)
{
return HolderOptionList?[idx] is ModdedStringOption opt
? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedStringOption");
}
}
@@ -9,13 +9,28 @@ namespace MiraAPI.GameOptions.Attributes;
/// Attribute for a list of toggle options.
/// </summary>
/// <param name="title">The option's title.</param>
/// <param name="on">What the option displays when toggled on.</param>
/// <param name="off">What the option displays when toggled off.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedToggleOptionListAttribute(string title) : ModdedOptionListAttribute(title)
public class ModdedToggleOptionListAttribute(
string title,
string on,
string off)
: ModdedOptionListAttribute(title)
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOptionListAttribute"/> class.
/// </summary>
/// <param name="title">The option's title.</param>
public ModdedToggleOptionListAttribute(string title)
: this(title, "On", "Off")
{
}
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{
var optList = new ModdedOptionList<ModdedToggleOption>(
value.Count, idx => new(GetFormattedTitle(idx), (bool)(value[idx] ?? false)));
value.Count, idx => new(GetFormattedTitle(idx), (bool)(value[idx] ?? false), on, off));
return optList;
}
@@ -30,7 +45,7 @@ public class ModdedToggleOptionListAttribute(string title) : ModdedOptionListAtt
public override object GetValue(int idx)
{
return HolderOptionList?[idx] is ModdedToggleOption opt
? (object)opt.Value
? opt.Value
: throw new InvalidOperationException($"Holder option for {GetFormattedTitle(idx)} is not a ModdedToggleOption.");
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)]
internal sealed class CollectionBuilderAttribute(Type builderType, string methodName) : Attribute
{
public Type BuilderType { get; } = builderType;
public string MethodName { get; } = methodName;
}
+41
View File
@@ -57,6 +57,21 @@ public interface IModdedOption
/// </summary>
Func<bool> Visible { get; set; }
/// <summary>
/// Gets or sets a getter event for the option, allowing for dynamic values based on external circumstances.
/// </summary>
Delegate? Getter { get; set; }
/// <summary>
/// Gets or sets a setter event for the option, allowing for dynamic value changes based on external circumstances.
/// </summary>
Delegate? Setter { get; set; }
/// <summary>
/// Gets or sets the event that is invoked when the value of the option changes.
/// </summary>
Delegate? ChangedEvent { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the option should be included with presets.
/// </summary>
@@ -114,6 +129,12 @@ public interface IModdedOption
/// </summary>
/// <param name="presetConfig">The <see cref="ConfigFile"/> representing the preset configuration.</param>
void LoadFromPreset(ConfigFile presetConfig);
/// <summary>
/// Gets the type of the value that the option represents.
/// </summary>
/// <returns>The type of the option value.</returns>
Type GetValueType();
}
/// <summary>
@@ -121,4 +142,24 @@ public interface IModdedOption
/// </summary>
public interface IModdedOptionList : IReadOnlyList<IModdedOption>
{
/// <summary>
/// Gets the function to name the keys of the individual options in the configuration file.
/// <para/>
/// Return <see langword="null"/> to use the default of the property's name with its index appended.
/// </summary>
Func<int, string>? ConfigName { get; init; }
}
/// <summary>
/// Interface for list of <typeparamref name="T"/> options.
/// </summary>
/// <typeparam name="T">The type of <see cref="IModdedOption"/>.</typeparam>
public interface IModdedOptionList<out T> : IModdedOptionList
where T : IModdedOption
{
/// <inheritdoc cref="IReadOnlyList{T}.this[int]"/>
new T this[int index] { get; }
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
new IEnumerator<T> GetEnumerator();
}
+21
View File
@@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace MiraAPI.GameOptions;
/// <summary>
/// Allows an option group to compress noisy option blocks in the wiki "Options" section.
/// Implementers can hide specific option keys and replace them with one or more summary lines.
/// </summary>
public interface ISummarizedOptions
{
/// <summary>
/// Gets the option string keys (StringNames) to omit from the wiki options list (e.g., a large set of related options).
/// </summary>
IReadOnlySet<StringNames> WikiHiddenOptionKeys { get; }
/// <summary>
/// Gets summary lines to insert when the first hidden option would have appeared.
/// Lines should already be formatted as "Title: Value".
/// </summary>
IEnumerable<string> GetWikiOptionSummaryLines();
}
+294
View File
@@ -0,0 +1,294 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace MiraAPI.GameOptions;
/// <summary>
/// A fixed-size array created specifically for modded options that automatically synchronizes with option attributes.
/// </summary>
/// <typeparam name="T">The type of the values that the array holds.</typeparam>
[CollectionBuilder(typeof(ModdedArrayBuilder), nameof(ModdedArrayBuilder.Create))]
public sealed class ModdedArray<T> : IList<T>, IReadOnlyList<T>, IList
{
private readonly T[] items;
private int version;
/// <inheritdoc/>
public int Count => items.Length;
/// <inheritdoc/>
public bool IsReadOnly => false;
/// <inheritdoc/>
bool IList.IsFixedSize => true;
/// <inheritdoc/>
bool ICollection.IsSynchronized => false;
/// <inheritdoc/>
object ICollection.SyncRoot => items.SyncRoot;
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with the provided capacity.
/// </summary>
/// <param name="capacity">The capacity of the list.</param>
public ModdedArray(int capacity)
{
items = new T[capacity];
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
/// </summary>
/// <param name="collection">The initial collection of values to initialise with.</param>
public ModdedArray(IEnumerable<T> collection)
{
items = [.. collection];
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
/// </summary>
/// <param name="span">The initial span of values to initialise with.</param>
public ModdedArray(Span<T> span)
{
items = [.. span];
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
/// </summary>
/// <param name="span">The initial span of values to initialise with.</param>
public ModdedArray(ReadOnlySpan<T> span)
{
items = [.. span];
}
/// <summary>
/// Gets or sets the element at the specified index, syncing with the attribute if registered.
/// </summary>
/// <param name="index">The index of the value.</param>
public T this[int index]
{
get => ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute)
? (T)attribute.GetValue(index)
: items[index];
set
{
if (ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute))
attribute.SetValue(index, value!);
else
items[index] = value;
version++;
}
}
/// <inheritdoc/>
object? IList.this[int index]
{
get => this[index];
set => this[index] = (T)value!;
}
/// <inheritdoc/>
public bool Contains(T item)
{
return Array.IndexOf(items, item) >= 0;
}
/// <inheritdoc/>
public void CopyTo(T[] array, int arrayIndex)
{
items.CopyTo(array, arrayIndex);
}
/// <inheritdoc/>
public int IndexOf(T item)
{
return Array.IndexOf(items, item);
}
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
public Enumerator GetEnumerator()
{
return new(this);
}
/// <inheritdoc/>
bool IList.Contains(object? value)
{
return ((IList)items).Contains(value);
}
/// <inheritdoc/>
int IList.IndexOf(object? value)
{
return ((IList)items).IndexOf(value);
}
/// <inheritdoc/>
void ICollection.CopyTo(Array array, int index)
{
items.CopyTo(array, index);
}
/// <inheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc/>
int IList.Add(object? value)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.Clear()
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.Insert(int index, object? value)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.Remove(object? value)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
void IList.RemoveAt(int index)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Insert(int index, T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void RemoveAt(int index)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Add(T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Clear()
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public bool Remove(T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <summary>
/// Enumerates the elements of a <see cref="ModdedArray{T}"/>.
/// </summary>
public struct Enumerator : IEnumerator<T>
{
private readonly ModdedArray<T> array;
private readonly int version;
private int index;
private T? current;
internal Enumerator(ModdedArray<T> moddedArray)
{
array = moddedArray;
version = moddedArray.version;
index = 0;
current = default;
}
/// <inheritdoc/>
public readonly T Current => current!;
/// <inheritdoc/>
readonly object? IEnumerator.Current
{
get
{
if (index == 0 || index == array.Count + 1)
throw new InvalidOperationException("Enumeration has not started or has already finished.");
return Current;
}
}
/// <inheritdoc/>
public readonly void Dispose()
{
}
/// <inheritdoc/>
public bool MoveNext()
{
if (version != array.version)
throw new InvalidOperationException("Collection was modified; enumeration operation may not execute.");
if ((uint)index < (uint)array.Count)
{
current = array[index];
index++;
return true;
}
index = array.Count + 1;
current = default;
return false;
}
/// <inheritdoc/>
public void Reset()
{
if (version != array.version)
throw new InvalidOperationException("Collection was modified; enumeration operation may not execute.");
index = 0;
current = default;
}
}
}
/// <summary>
/// Provides a builder for <see cref="ModdedArray{T}"/> to support C# 12 collection expressions.
/// </summary>
public static class ModdedArrayBuilder
{
/// <summary>
/// Creates a new <see cref="ModdedArray{T}"/> from a span of values.
/// </summary>
/// <typeparam name="T">The the type of the values.</typeparam>
/// <param name="values">The values to initialise with.</param>
/// <returns>The <see cref="ModdedArray{T}"/> instance initialised with the provided values.</returns>
public static ModdedArray<T> Create<T>(ReadOnlySpan<T> values)
{
return new(values);
}
}
+122 -86
View File
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using BepInEx.Configuration;
using HarmonyLib;
using MiraAPI.GameModes;
@@ -24,9 +25,15 @@ namespace MiraAPI.GameOptions;
/// </summary>
public static class ModdedOptionsManager
{
private static readonly Dictionary<PropertyInfo, PropertyOptionAttribute> OptionAttributes = [];
private static readonly Dictionary<MethodBase, PropertyOptionAttribute> GetterLookups = [];
private static readonly Dictionary<MethodBase, PropertyOptionAttribute> SetterLookups = [];
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
private static readonly HashSet<PropertyInfo> OptionAttributes = [];
private static readonly HarmonyMethod CachedGetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertyGetterPatch));
private static readonly HarmonyMethod CachedSetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertySetterPatch));
internal static readonly ConditionalWeakTable<object, ModdedOptionListAttribute> ListInstanceCache = [];
internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
internal static readonly Dictionary<OptionBehaviour, ModdedStringOption> CreatedStringOptions = [];
internal static readonly Dictionary<Type, List<AbstractOptionGroup>> GameModeOptionGroups = [];
@@ -61,14 +68,14 @@ public static class ModdedOptionsManager
internal static bool RegisterGroup(Type type)
{
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
if (TypeToGroup.ContainsKey(type))
{
Error($"Group {type.Name} already exists.");
return false;
}
if (TypeToGroup.ContainsKey(type))
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
{
Logger<MiraApiPlugin>.Error($"Group {type.Name} already exists.");
return false;
}
@@ -78,17 +85,17 @@ public static class ModdedOptionsManager
internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo)
{
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
{
return false;
}
if (TypeToGroup.ContainsKey(type))
{
Error($"Group {type.Name} already exists.");
return false;
}
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
{
return false;
}
if (typeof(AbstractGameMode).IsAssignableFrom(group.OptionableType))
{
if (GameModeOptionGroups.TryGetValue(group.OptionableType, out var oldList))
@@ -116,7 +123,7 @@ public static class ModdedOptionsManager
Groups.Add(group);
TypeToGroup.Add(type, group);
typeof(OptionGroupSingleton<>).MakeGenericType(type)
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! // Message suppression points to here
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
.SetValue(null, group);
}
@@ -143,7 +150,7 @@ public static class ModdedOptionsManager
PropertyInfo property,
MiraPluginInfo pluginInfo)
{
if (OptionAttributes.ContainsKey(property))
if (!OptionAttributes.Add(property))
{
Error($"Property {property.Name} already has an attribute registered.");
return;
@@ -163,22 +170,40 @@ public static class ModdedOptionsManager
return;
}
var setterOriginal = property.GetSetMethod();
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
var getterOriginal = property.GetGetMethod(true)!;
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch);
GetterLookups[getterOriginal] = attribute;
var getterOriginal = property.GetGetMethod();
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
var setterOriginal = property.GetSetMethod(true);
OptionAttributes.Add(property, attribute);
attribute.HolderOption = option;
var visibilityAttr = property.GetCustomAttribute<ModdedOptionVisibilityAttribute>();
var visibilityFunc = visibilityAttr?.GetVisibility(group, property);
if (visibilityFunc != null)
if (setterOriginal != null)
{
option.Visible = visibilityFunc;
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
SetterLookups[setterOriginal] = attribute;
}
attribute.HolderOption = option;
attribute.ValueType = property.PropertyType;
var customAttributes = property.GetCustomAttributes(false);
foreach (var attr in customAttributes)
{
switch (attr)
{
case ModdedOptionVisibilityAttribute visibilityAttr:
option.Visible = visibilityAttr.GetVisibility(group, property) ?? DelegateUtilities.AlwaysTrue;
break;
case ModdedOptionGetterAttribute getterAttr:
option.Getter = getterAttr.GetGetter(group, property, option);
break;
case ModdedOptionSetterAttribute setterAttr:
option.Setter = setterAttr.GetSetter(group, property, option);
break;
case ModdedOptionChangedAttribute changedAttr:
option.ChangedEvent = changedAttr.GetChanged(group, property, option);
break;
}
}
RegisterOption(option, group, property.Name, pluginInfo);
@@ -198,9 +223,11 @@ public static class ModdedOptionsManager
return;
}
for (int i = 0; i < optionList.Count; i++)
var configName = optionList.ConfigName ?? (i => property.Name + (i + 1));
for (var i = 0; i < optionList.Count; i++)
{
RegisterOption(optionList[i], group, property.Name + i, pluginInfo);
RegisterOption(optionList[i], group, configName(i), pluginInfo);
}
}
@@ -210,7 +237,7 @@ public static class ModdedOptionsManager
PropertyInfo property,
MiraPluginInfo pluginInfo)
{
if (OptionAttributes.ContainsKey(property))
if (!OptionAttributes.Add(property))
{
Error($"Property {property.Name} already has an attribute registered.");
return;
@@ -245,39 +272,73 @@ public static class ModdedOptionsManager
return;
}
var setterOriginal = property.GetSetMethod();
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
var listType = propertyList.GetType();
var valueType = listType.GetGenericArguments()[0];
var getterOriginal = property.GetGetMethod();
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
if (!listType.IsGenericType || listType.GetGenericTypeDefinition() != typeof(ModdedArray<>))
{
Error($"Please use ModdedList<{valueType.Name}> for modded option lists.");
return;
}
var listIndex = propertyList.GetType().GetProperty("Item")!;
var getterOriginal = property.GetGetMethod(true)!;
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch);
GetterLookups[getterOriginal] = attribute;
var listSetterOriginal = listIndex.GetSetMethod();
var listSetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listSetterOriginal, postfix: new HarmonyMethod(listSetterPatch));
var setterOriginal = property.GetSetMethod(true);
var listGetterOriginal = listIndex.GetSetMethod();
var listGetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listGetterOriginal, prefix: new HarmonyMethod(listGetterPatch));
if (setterOriginal != null)
{
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
SetterLookups[setterOriginal] = attribute;
}
OptionAttributes.Add(property, attribute);
attribute.HolderOptionList = optionList;
attribute.Value = propertyVal;
attribute.Value = propertyList;
attribute.ValueType = valueType;
var visibilityAttr = property.GetCustomAttribute<ModdedOptionVisibilityAttribute>();
var visibilityFunc = visibilityAttr?.GetListVisibility(group, property);
for (int i = 0; i < optionList.Count; i++)
ListInstanceCache.Add(propertyVal, attribute);
var customAttributes = property.GetCustomAttributes(false);
Func<int, Delegate?>? visibilityFactory = null, getterFactory = null, setterFactory = null, changedFactory = null;
foreach (var attr in customAttributes)
{
switch (attr)
{
case ModdedOptionVisibilityAttribute visibilityAttr:
visibilityFactory = visibilityAttr.GetListVisibility(group, property);
break;
case ModdedOptionGetterAttribute getterAttr:
getterFactory = getterAttr.GetListGetter(group, property, valueType);
break;
case ModdedOptionSetterAttribute setterAttr:
setterFactory = setterAttr.GetListSetter(group, property, valueType);
break;
case ModdedOptionChangedAttribute changedAttr:
changedFactory = changedAttr.GetListChanged(group, property, valueType);
break;
}
}
for (var i = 0; i < optionList.Count; i++)
{
var option = optionList[i];
if (visibilityFunc != null)
{
option.Visible = () => visibilityFunc(i);
}
RegisterOption(option, group, property.Name + i, pluginInfo);
if (visibilityFactory != null)
option.Visible = (Func<bool>)(visibilityFactory(i) ?? DelegateUtilities.AlwaysTrue);
if (getterFactory != null)
option.Getter = getterFactory(i);
if (setterFactory != null)
option.Setter = setterFactory(i);
if (changedFactory != null)
option.ChangedEvent = changedFactory(i);
RegisterOption(option, group, property.Name + (i + 1), pluginInfo);
}
}
@@ -348,11 +409,16 @@ public static class ModdedOptionsManager
/// <param name="__originalMethod">The original setter method.</param>
/// <param name="value">The new object value.</param>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention.")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Harmony naming convention.")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
{
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
if (!SetterLookups.TryGetValue(__originalMethod, out var attribute))
return;
attribute.SetValue(value);
if (attribute is ModdedOptionListAttribute listAttribute && value != null)
ListInstanceCache.AddOrUpdate(value, listAttribute);
}
/// <summary>
@@ -362,43 +428,13 @@ public static class ModdedOptionsManager
/// <param name="__result">The result of the property getter.</param>
/// <returns><see langword="false"/> so the original getter gets skipped.</returns>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Harmony naming convention.")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
{
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
if (!GetterLookups.TryGetValue(__originalMethod, out var attribute))
return true;
__result = attribute.GetValue();
return false;
}
/// <summary>
/// Patches the setter of a list property to update the value of the option.
/// </summary>
/// <param name="__instance">The list's instance.</param>
/// <param name="index">The index to find in the list.</param>
/// <param name="value">The new object value.</param>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static void PropertyListSetterPatch(object __instance, int index, object value)
{
var attribute = (ModdedOptionListAttribute)OptionAttributes.First(
pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value;
attribute.SetValue(index, value);
}
/// <summary>
/// Patches the getter of a list property to return the value of the option.
/// </summary>
/// <param name="__instance">The list's instance.</param>
/// <param name="index">The index to find in the list.</param>
/// <param name="__result">The result of the property getter.</param>
/// <returns>False so the original getter gets skipped.</returns>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static bool PropertyListGetterPatch(object __instance, int index, ref object __result)
{
var attribute = (ModdedOptionListAttribute)OptionAttributes.First(
pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value;
__result = attribute.GetValue(index);
return false;
}
}
@@ -50,7 +50,11 @@ public class ModdedEnumOption : ModdedOption<int>
$"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
stringOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
// Changing the button text to make them more distinct from number options
stringOption.MinusBtn.ChangeButtonText("<");
stringOption.PlusBtn.ChangeButtonText(">");
// SetUpFromData method doesn't work correctly so we must set the values manually
stringOption.Title = StringName;
@@ -113,7 +117,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// </summary>
/// <typeparam name="T">The <see cref="Enum"/> type.</typeparam>
public class ModdedEnumOption<T> : ModdedOption<T>
where T : Enum
where T : struct, Enum
{
/// <summary>
/// Gets the string values of the <see langword="enum"/>.
@@ -130,7 +134,7 @@ public class ModdedEnumOption<T> : ModdedOption<T>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset)
{
Values = values ?? Enum.GetNames(typeof(T));
Values = values ?? Enum.GetNames<T>();
var data = ScriptableObject.CreateInstance<StringGameSetting>();
data.Title = StringName;
@@ -149,7 +153,11 @@ public class ModdedEnumOption<T> : ModdedOption<T>
$"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
stringOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
// Changing the button text to make them more distinct from number options
stringOption.MinusBtn.ChangeButtonText("<");
stringOption.PlusBtn.ChangeButtonText(">");
// SetUpFromData method doesn't work correctly so we must set the values manually
stringOption.Title = StringName;
@@ -176,13 +184,13 @@ public class ModdedEnumOption<T> : ModdedOption<T>
/// <inheritdoc />
public override void HandleNetData(byte[] data)
{
SetValue((T)Enum.Parse(typeof(T), Encoding.Unicode.GetString(data)));
SetValue(Enum.Parse<T>(Encoding.Unicode.GetString(data)));
}
/// <inheritdoc />
public override T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return (T)Enum.Parse(typeof(T), optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo));
return Enum.Parse<T>(optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo));
}
/// <inheritdoc />
@@ -61,10 +61,10 @@ public class ModdedNumberOption : ModdedOption<float>
/// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroBehavior">Determines what is shown for zero. Options: ∞, #, or a word value (such as Off).</param>
/// <param name="negativeBehavior">Determines what is shown for negative 1. Options: ∞, #, or a word value (such as Off).</param>
/// <param name="zeroBehavior">Determines what is shown for zero.</param>
/// <param name="negativeBehavior">Determines what is shown for negative 1.</param>
/// <param name="halfIncrements">Whether increments can be split in half.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedNumberOption(
string title,
@@ -85,14 +85,12 @@ public class ModdedNumberOption : ModdedOption<float>
Increment = increment;
SuffixType = suffixType;
ZeroWordValue = zeroBehavior;
NegativeWordValue = negativeBehavior;
ShiftIncrement = halfIncrements;
NegativeWordValue = negativeBehavior;
Value = Mathf.Clamp(defaultValue, min, max);
Data = ScriptableObject.CreateInstance<FloatGameSetting>();
var data = (FloatGameSetting)Data;
var data = ScriptableObject.CreateInstance<FloatGameSetting>();
data.Type = global::OptionTypes.Float;
data.Title = StringName;
data.Value = Value;
@@ -108,6 +106,8 @@ public class ModdedNumberOption : ModdedOption<float>
data.ZeroIsInfinity = ZeroInfinity;
data.SuffixType = (NumberSuffixes)SuffixType;
data.OptionName = FloatOptionNames.Invalid;
Data = data;
}
/// <summary>
@@ -119,8 +119,8 @@ public class ModdedNumberOption : ModdedOption<float>
/// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedNumberOption(
string title,
@@ -132,36 +132,8 @@ public class ModdedNumberOption : ModdedOption<float>
string? formatString = null,
bool zeroInfinity = false,
bool includeInPreset = true)
: base(title, defaultValue, includeInPreset)
: this(title, defaultValue, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f, includeInPreset)
{
Min = min;
Max = max;
Increment = increment;
SuffixType = suffixType;
ZeroWordValue = zeroInfinity ? "∞" : "#";
NegativeWordValue = "#";
ShiftIncrement = Mathf.Approximately(increment, 1f);
Value = Mathf.Clamp(defaultValue, min, max);
Data = ScriptableObject.CreateInstance<FloatGameSetting>();
var data = (FloatGameSetting)Data;
data.Type = global::OptionTypes.Float;
data.Title = StringName;
data.Value = Value;
data.Increment = Increment;
data.ValidRange = new FloatRange(Min, Max);
data.FormatString = formatString ??
(defaultValue.IsInteger() &&
Increment.IsInteger() &&
Value.IsInteger() &&
Min.IsInteger() &&
Max.IsInteger() ? "0" : "0.0");
data.ZeroIsInfinity = ZeroInfinity;
data.SuffixType = (NumberSuffixes)SuffixType;
data.OptionName = FloatOptionNames.Invalid;
}
/// <inheritdoc />
+61 -16
View File
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using BepInEx.Configuration;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using Reactor.Networking.Rpc;
using UnityEngine;
@@ -43,16 +45,32 @@ public abstract class ModdedOption<T> : IModdedOption
/// <summary>
/// Gets or sets the value of the option.
/// </summary>
public T Value { get; protected set; }
public T Value
{
get => Getter != null ? Getter(_value) : _value;
protected set
{
if (Setter != null)
value = Setter(value, _value);
_value = value;
}
}
private T _value;
/// <summary>
/// Gets the default value of the option.
/// </summary>
public T DefaultValue { get; }
/// <summary>
/// Gets or sets the event that is invoked when the value of the option changes.
/// </summary>
/// <inheritdoc cref="IModdedOption.Getter"/>
public Func<T, T>? Getter { get; set; }
/// <inheritdoc cref="IModdedOption.Setter"/>
public Func<T, T, T>? Setter { get; set; }
/// <inheritdoc cref="IModdedOption.ChangedEvent"/>
public Action<T>? ChangedEvent { get; set; }
/// <inheritdoc />
@@ -67,6 +85,33 @@ public abstract class ModdedOption<T> : IModdedOption
/// <inheritdoc />
public ConfigDefinition? ConfigDefinition { get; set; }
/// <inheritdoc />
public AbstractOptionGroup ParentGroup { get; set; }
/// <inheritdoc />
public virtual OptionNotifConfiguration Configuration => new(ParentGroup);
/// <inheritdoc />
Delegate? IModdedOption.Getter
{
get => Getter;
set => Getter = value as Func<T, T>;
}
/// <inheritdoc />
Delegate? IModdedOption.Setter
{
get => Setter;
set => Setter = value as Func<T, T, T>;
}
/// <inheritdoc />
Delegate? IModdedOption.ChangedEvent
{
get => ChangedEvent;
set => ChangedEvent = value as Action<T>;
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOption{T}"/> class.
/// </summary>
@@ -80,7 +125,7 @@ public abstract class ModdedOption<T> : IModdedOption
DefaultValue = defaultValue;
Value = defaultValue;
StringName = MiraLocaleManager.GetOrCreateLocaleString(Title);
Visible = () => true;
Visible = DelegateUtilities.AlwaysTrue;
IncludeInPreset = includeInPreset;
}
@@ -96,25 +141,25 @@ public abstract class ModdedOption<T> : IModdedOption
/// <param name="sendRpc">Whether to send the value to other players.</param>
public void SetValue(T newValue, bool sendRpc = true)
{
var oldVal = Value;
var oldVal = _value;
Value = newValue;
if (Value?.Equals(oldVal) == false)
if (!EqualityComparer<T>.Default.Equals(_value, oldVal))
{
ChangedEvent?.Invoke(Value);
ChangedEvent?.Invoke(_value);
}
if (sendRpc && AmongUsClient.Instance.AmHost)
{
if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true)
{
entry.Value = Value;
entry.Value = _value;
}
Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true);
}
OnValueChanged(newValue);
OnValueChanged(_value);
}
/// <inheritdoc />
@@ -149,6 +194,12 @@ public abstract class ModdedOption<T> : IModdedOption
}
}
/// <inheritdoc />
public Type GetValueType()
{
return typeof(T);
}
/// <inheritdoc />
public abstract float GetFloatData();
@@ -188,10 +239,4 @@ public abstract class ModdedOption<T> : IModdedOption
{
return option.Value;
}
/// <inheritdoc />
public AbstractOptionGroup ParentGroup { get; set; }
/// <inheritdoc />
public virtual OptionNotifConfiguration Configuration => new(ParentGroup);
}
@@ -9,7 +9,7 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// Represents a modded option list.
/// </summary>
/// <typeparam name="T">The type of options.</typeparam>
public class ModdedOptionList<T> : IModdedOptionList
public class ModdedOptionList<T> : IModdedOptionList<T>
where T : IModdedOption
{
/// <inheritdoc/>
@@ -20,6 +20,9 @@ public class ModdedOptionList<T> : IModdedOptionList
/// </summary>
public IReadOnlyList<T> Options { get; }
/// <inheritdoc/>
public Func<int, string>? ConfigName { get; init; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOptionList{T}"/> class.
/// </summary>
@@ -41,17 +44,6 @@ public class ModdedOptionList<T> : IModdedOptionList
Options = [.. options];
}
/// <inheritdoc/>
public IEnumerator<IModdedOption> GetEnumerator()
{
return ((IEnumerable<IModdedOption>)Options).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return Options.GetEnumerator();
}
/// <summary>
/// Indexes the option of type <typeparamref name="T"/>.
/// </summary>
@@ -59,5 +51,24 @@ public class ModdedOptionList<T> : IModdedOptionList
/// <returns>option of type <typeparamref name="T"/>.</returns>
public T this[int idx] => Options[idx];
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
public IEnumerator<T> GetEnumerator()
{
return Options.GetEnumerator();
}
/// <inheritdoc cref="IEnumerable.GetEnumerator"/>
IEnumerator IEnumerable.GetEnumerator()
{
return Options.GetEnumerator();
}
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
IEnumerator<IModdedOption> IEnumerable<IModdedOption>.GetEnumerator()
{
return ((IEnumerable<IModdedOption>)Options).GetEnumerator();
}
/// <inheritdoc cref="IReadOnlyList{T}.this"/>
IModdedOption IReadOnlyList<IModdedOption>.this[int index] => this[index];
}
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Il2CppSystem.Collections.Generic;
using MiraAPI.Networking;
using MiraAPI.Translation;
using MiraAPI.Utilities;
@@ -29,6 +29,11 @@ public class ModdedPlayerOption : ModdedOption<int>
/// </summary>
public bool AllowNone { get; set; }
/// <summary>
/// Gets the player selected by this option. May be <c>null</c> if allowed to have <c>None</c> as an option.
/// </summary>
public NetworkedPlayerInfo? SelectedPlayer => Value == -1 ? null : Values[Value];
/// <summary>
/// Initializes a new instance of the <see cref="ModdedPlayerOption"/> class.
/// </summary>
@@ -37,15 +42,16 @@ public class ModdedPlayerOption : ModdedOption<int>
public ModdedPlayerOption(string title, bool allowNone = true)
: base(title, allowNone ? -1 : 0, false)
{
Data = ScriptableObject.CreateInstance<PlayerSelectionGameSetting>();
var data = (PlayerSelectionGameSetting)Data;
Values = [];
AllowNone = allowNone;
Value = DefaultValue;
var data = ScriptableObject.CreateInstance<PlayerSelectionGameSetting>();
data.Title = StringName;
data.Type = global::OptionTypes.Player;
Values = new List<NetworkedPlayerInfo>();
AllowNone = allowNone;
Value = DefaultValue;
Data = data;
}
/// <inheritdoc />
@@ -60,17 +66,16 @@ public class ModdedPlayerOption : ModdedOption<int>
playerOption.name =
$"{ParentMod!.OptionsTitleText}.PlayerOption.{TranslationController.Instance.GetString(StringName)}";
var filteredList = GetFilteredPlayers();
Values = filteredList.ToIl2CppList();
Values = GetFilteredPlayers();
playerOption.SetUpFromData(Data, 20);
playerOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
playerOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
playerOption.Title = StringName;
playerOption.TitleText.text = Title.Translate();
playerOption.Values = Values;
playerOption.Values = Values.ToIl2CppList();
playerOption.Value = Value;
playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value);
playerOption.playerIndex = Values.FindIndex(p => p.PlayerId == Value);
OptionBehaviour = playerOption;
ModdedOptionsManager.CreatedPlayerOptions.TryAdd(playerOption, this);
@@ -85,9 +90,16 @@ public class ModdedPlayerOption : ModdedOption<int>
/// Returns a player ran through the filter.
/// </summary>
/// <returns>A list of filtered players.</returns>
public System.Collections.Generic.List<NetworkedPlayerInfo> GetFilteredPlayers()
public List<NetworkedPlayerInfo> GetFilteredPlayers()
{
return [.. GameData.Instance.AllPlayers.ToArray().Where(x => PlayerFilter?.Invoke(x) ?? true)];
var players = GameData.Instance.AllPlayers.ToArray();
IEnumerable<NetworkedPlayerInfo> filtered = players;
if (PlayerFilter != null)
filtered = players.Where(PlayerFilter);
return [.. filtered.OrderBy(x => x.PlayerId)];
}
/// <summary>
@@ -25,6 +25,28 @@ public class ModdedStringOption : ModdedOption<string>
/// </summary>
public StringNames[] StringNameValues { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedStringOption"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as a string.</param>
/// <param name="values">An option list of string values to use.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedStringOption(string title, string defaultValue, string[] values, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset)
{
Values = values;
StringNameValues = [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)];
var data = ScriptableObject.CreateInstance<StringGameSetting>();
data.Title = StringName;
data.Type = global::OptionTypes.String;
data.Values = StringNameValues;
data.Index = GetIndex(Value);
Data = data;
}
/// <summary>
/// Gets the string's index.
/// </summary>
@@ -61,61 +83,37 @@ public class ModdedStringOption : ModdedOption<string>
StringNameValues = [.. newStrVals];
}
if (Data != null)
{
var data = (StringGameSetting)Data;
data.Values = StringNameValues;
data.Index = GetIndex(Value);
if (OptionBehaviour != null)
{
var strOpt = OptionBehaviour.TryCast<StringOption>();
if (strOpt != null)
{
strOpt.Values = StringNameValues;
strOpt.Value = GetIndex(Value);
Warning("StringOption is valid");
}
else
{
Error("StringOption is null.");
}
Warning("OptionBehaviour is valid");
}
else
{
Error("OptionBehaviour is null.");
}
Warning("Data is valid");
}
else
if (Data == null)
{
Error("Data is null.");
return;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedStringOption"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as a string.</param>
/// <param name="values">An option list of string values to use.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedStringOption(string title, string defaultValue, string[] values, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset)
{
Values = values;
Data = ScriptableObject.CreateInstance<StringGameSetting>();
var data = (StringGameSetting)Data;
data.Title = StringName;
data.Type = global::OptionTypes.String;
StringNameValues = [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)];
data.Values = StringNameValues;
data.Index = GetIndex(Value);
Warning("Data is valid");
if (OptionBehaviour == null)
{
Error("OptionBehaviour is null.");
return;
}
var strOpt = OptionBehaviour.TryCast<StringOption>();
Warning("OptionBehaviour is valid");
if (strOpt == null)
{
Error("StringOption is null.");
return;
}
strOpt.Values = StringNameValues;
strOpt.Value = GetIndex(Value);
Warning("StringOption is valid");
}
/// <inheritdoc />
@@ -126,7 +124,11 @@ public class ModdedStringOption : ModdedOption<string>
$"{ParentMod!.OptionsTitleText}.StringOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
stringOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
// Changing the button text to make them more distinct from number options
stringOption.MinusBtn.ChangeButtonText("<");
stringOption.PlusBtn.ChangeButtonText(">");
// SetUpFromData method doesn't work correctly so we must set the values manually
stringOption.Title = StringName;
@@ -11,6 +11,37 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// </summary>
public class ModdedToggleOption : ModdedOption<bool>
{
/// <summary>
/// Gets or sets the display value when the option is toggled on.
/// </summary>
public string OnValue { get; set; }
/// <summary>
/// Gets or sets the display value when the option is toggled off.
/// </summary>
public string OffValue { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOption"/> class.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="on">What the option displays when toggled on.</param>
/// <param name="off">What the option displays when toggled off.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedToggleOption(string title, bool defaultValue, string on, string off, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset)
{
OnValue = on;
OffValue = off;
var data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
data.Title = StringName;
data.Type = global::OptionTypes.Checkbox;
Data = data;
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOption"/> class.
/// </summary>
@@ -18,13 +49,8 @@ public class ModdedToggleOption : ModdedOption<bool>
/// <param name="defaultValue">The default value.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedToggleOption(string title, bool defaultValue, bool includeInPreset = true)
: base(title, defaultValue, includeInPreset)
: this(title, defaultValue, "On", "Off", includeInPreset)
{
Data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
var data = (CheckboxGameSetting)Data;
data.Title = StringName;
data.Type = global::OptionTypes.Checkbox;
}
/// <inheritdoc />
@@ -39,7 +65,7 @@ public class ModdedToggleOption : ModdedOption<bool>
toggleOption.Title = StringName;
toggleOption.TitleText.text = Title.Translate();
toggleOption.CheckMark.enabled = Value;
toggleOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
toggleOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
OptionBehaviour = toggleOption;
@@ -76,7 +102,7 @@ public class ModdedToggleOption : ModdedOption<bool>
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
newValue ? "On" : "Off",
(newValue ? OnValue : OffValue).Translate(),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
@@ -126,7 +126,7 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr)
currentFinalHideTime = totalFinalHideTime;
if (timerBar != null)
{
Object.Destroy(timerBar);
Destroy(timerBar);
}
finalHideAlertSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideAlertSFX;
+48 -119
View File
@@ -139,10 +139,7 @@ public abstract class CustomActionButton
/// <param name="parent">The parent of the button.</param>
public virtual void CreateButton(Transform parent)
{
if (Button)
{
return;
}
if (Button) return;
UsesLeft = MaxUses;
Timer = AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay ? 0 : InitialCooldown;
@@ -152,19 +149,15 @@ public abstract class CustomActionButton
Button = Object.Instantiate(HudManager.Instance.AbilityButton, parent);
Button.name = Name + "Button";
Button.OverrideText(Name.Translate().ToUpperInvariant());
Object.Destroy(Button.glyph.GetComponent<SpriteRenderer>());
Object.Destroy(Button.glyph.GetComponent<Image>());
Button.graphic.sprite = Sprite.LoadAsset();
Button.SetUsesRemaining(MaxUses);
if (MaxUses <= 0)
{
Button.SetInfiniteUses();
}
if (MaxUses <= 0) Button.SetInfiniteUses();
if (TextOutlineColor != Color.clear)
{
SetTextOutline(TextOutlineColor);
}
if (TextOutlineColor != Color.clear) SetTextOutline(TextOutlineColor);
var pb = Button.GetComponent<PassiveButton>();
pb.OnClick = new Button.ButtonClickedEvent();
@@ -173,10 +166,7 @@ public abstract class CustomActionButton
// Invoke the generic button click event.
var genericEvent = new MiraButtonClickEvent(this);
MiraEventManager.InvokeEvent(genericEvent);
if (genericEvent.IsCancelled)
{
MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this));
}
if (genericEvent.IsCancelled) MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this));
// Invoke the button click event for specific button.
var eventType = CustomButtonManager.ButtonEventTypes[GetType()];
@@ -191,68 +181,47 @@ public abstract class CustomActionButton
if (specificInvoked)
{
if (!@event.IsCancelled)
{
ClickHandler();
}
if (!@event.IsCancelled) ClickHandler();
}
else
{
if (!genericEvent.IsCancelled)
{
ClickHandler();
}
if (!genericEvent.IsCancelled) ClickHandler();
}
}));
if (Keybind == null) return;
Keybind.OnActivate(() =>
{
if (!Enabled(PlayerControl.LocalPlayer.Data.Role)) return;
Keybind.RemoveOnActivate(OnKeybind);
Keybind.OnActivate(OnKeybind);
// Invoke the generic button click event.
var genericEvent = new MiraButtonClickEvent(this);
MiraEventManager.InvokeEvent(genericEvent);
if (genericEvent.IsCancelled)
{
MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this));
}
// Invoke the button click event for specific button.
var eventType = CustomButtonManager.ButtonEventTypes[GetType()];
var @event = (MiraCancelableEvent)Activator.CreateInstance(eventType, this, genericEvent)!;
var specificInvoked = MiraEventManager.InvokeEvent(@event, eventType);
if (@event.IsCancelled)
{
var cancelEventType = CustomButtonManager.ButtonCancelledEventTypes[GetType()];
var cancelEvent = (MiraEvent)Activator.CreateInstance(cancelEventType, this)!;
MiraEventManager.InvokeEvent(cancelEvent, cancelEventType);
}
if (specificInvoked)
{
if (!@event.IsCancelled)
{
ClickHandler();
}
}
else
{
if (!genericEvent.IsCancelled)
{
ClickHandler();
}
}
});
KeybindIcon =
Helpers.CreateKeybindIcon(
Button.gameObject,
Keybind.CurrentKey,
new Vector3(0.4f, 0.45f, -9f));
KeybindIcon = Helpers.CreateKeybindIcon(Button.gameObject, Keybind.CurrentKey, new Vector3(0.4f, 0.45f, -9f));
KeybindText = KeybindIcon.transform.GetChild(0).GetComponent<TextMeshPro>();
HudManagerPatches.ModdedKeybindIcons.Add(KeybindText);
Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.45f, -0.1f);
KeybindText.fontSizeMax = KeybindText.fontSize;
KeybindText.fontSizeMin = KeybindText.fontSize * 0.5f;
KeybindText.enableAutoSizing = true;
Button.usesRemainingSprite.transform.localPosition = new Vector3(-0.341f, 0.45f, -0.1f);
UpdateKeybindIcon();
}
private void OnKeybind()
{
if (Button == null || !Enabled(PlayerControl.LocalPlayer.Data.Role)) return;
Button.GetComponent<PassiveButton>().OnClick.Invoke();
}
internal void UpdateKeybindIcon()
{
if (Keybind == null || KeybindText == null) return;
var text = ActiveInputManager.currentControlType switch
{
ActiveInputManager.InputType.Keyboard when Keybind.CurrentKey != KeyboardKeyCode.None => Keybind.CurrentKey.ToString(),
ActiveInputManager.InputType.Joystick => Keybind.CurrentControllerButton,
_ => string.Empty,
};
KeybindText.text = text;
KeybindIcon?.SetActive(text.Length > 0 && LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value);
}
/// <summary>
@@ -262,22 +231,13 @@ public abstract class CustomActionButton
/// <param name="moveButton">Whether the button's position should change in-game.</param>
public virtual void SetButtonLocation(ButtonLocation location, bool moveButton = true)
{
if (!HudManager.InstanceExists || Button == null)
{
return;
}
if (!HudManager.InstanceExists || Button == null) return;
Location = location;
if (!moveButton)
{
return;
}
if (!moveButton) return;
if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null)
{
return;
}
if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null) return;
switch (location)
{
@@ -305,10 +265,7 @@ public abstract class CustomActionButton
public virtual void ResetCooldownAndOrEffect()
{
Timer = Cooldown;
if (EffectActive)
{
OnEffectEnd();
}
if (EffectActive) OnEffectEnd();
EffectActive = false;
}
@@ -328,10 +285,7 @@ public abstract class CustomActionButton
/// <param name="sprite">The new <see cref="UnityEngine.Sprite"/> to override with.</param>
public virtual void OverrideSprite(Sprite sprite)
{
if (Button != null)
{
Button.graphic.sprite = sprite;
}
if (Button != null) Button.graphic.sprite = sprite;
}
/// <summary>
@@ -388,10 +342,7 @@ public abstract class CustomActionButton
UsesLeft = Mathf.Clamp(amount, 0, int.MaxValue);
Button?.SetUsesRemaining(UsesLeft);
if (Button != null)
{
Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white;
}
if (Button != null) Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white;
}
/// <summary>
@@ -469,8 +420,7 @@ public abstract class CustomActionButton
/// <returns>A value that represents whether the button should light up or not.</returns>
public virtual bool CanUse()
{
return PlayerControl.LocalPlayer.moveable &&
((EffectActive && IsEffectCancellable()) || (!EffectActive && (!LimitedUses || UsesLeft > 0)));
return PlayerControl.LocalPlayer.moveable && ((EffectActive && IsEffectCancellable()) || (!EffectActive && (!LimitedUses || UsesLeft > 0)));
}
/// <summary>
@@ -498,10 +448,7 @@ public abstract class CustomActionButton
return;
}
if (!CanClick())
{
return;
}
if (!CanClick()) return;
if (LimitedUses)
{
@@ -530,20 +477,9 @@ public abstract class CustomActionButton
/// <param name="playerControl">The local PlayerControl.</param>
public virtual void FixedUpdateHandler(PlayerControl playerControl)
{
if (Keybind != null && KeybindText != null)
{
KeybindText.text = Keybind.CurrentKey.ToString();
KeybindIcon?.SetActive(ActiveInputManager.currentControlType is ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value &&
Keybind.CurrentKey != KeyboardKeyCode.None);
}
if (Timer >= 0)
{
if (!TimerPaused && (!PauseTimerInVent || !playerControl.inVent))
{
Timer -= Time.deltaTime;
}
if (!TimerPaused && (!PauseTimerInVent || !playerControl.inVent)) Timer -= Time.deltaTime;
}
else if (HasEffect && EffectActive && EffectDuration > 0)
{
@@ -555,13 +491,9 @@ public abstract class CustomActionButton
if (Button)
{
if (CanUse())
{
Button!.SetEnabled();
}
else
{
Button!.SetDisabled();
}
if (EffectActive && EffectDuration > 0)
{
@@ -623,10 +555,7 @@ public abstract class CustomActionButton<T> : CustomActionButton
public override bool CanUse()
{
var newTarget = GetTarget();
if (newTarget != Target)
{
SetOutline(false);
}
if (newTarget != Target) SetOutline(false);
Target = IsTargetValid(newTarget) ? newTarget : null;
SetOutline(true);
+2 -3
View File
@@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using MiraAPI.Patches.Stubs;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
@@ -100,7 +101,6 @@ public abstract class CustomPaginableMenu : CustomPhoneMenu<CustomPaginableMenu.
: text.Trim().ToLowerInvariant();
}
[SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Creates a rather large ternary operation.")]
private List<MenuEntry> GetFilteredEntries()
{
var query = NormalizeForSearch(searchText);
@@ -248,8 +248,7 @@ public abstract class CustomPaginableMenu : CustomPhoneMenu<CustomPaginableMenu.
var wikiClickSound = HudManager.Instance?.MapButton?.ClickSound;
var searchFocusButton = searchTextbox.gameObject.GetComponent<PassiveButton>()
?? searchTextbox.gameObject.AddComponent<PassiveButton>();
var searchFocusButton = searchTextbox.gameObject.GetOrAddComponent<PassiveButton>();
if (wikiClickSound != null)
{
searchFocusButton.ClickSound = wikiClickSound;
+1 -1
View File
@@ -151,7 +151,7 @@ public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr)
var oldPos = settingsButton.transform.localPosition;
settingsButton.transform.localPosition = new Vector3(oldPos.x, oldPos.y, -100);
var chatButton = instance.Chat.chatButton.gameObject;
ClonedChatButton = Object.Instantiate(chatButton, chatButton.transform.parent);
ClonedChatButton = Instantiate(chatButton, chatButton.transform.parent);
ClonedChatButton.SetActive(false);
instance.Chat.chatButtonAspectPosition = ClonedChatButton.GetComponent<AspectPosition>();
chatButton.GetComponent<AspectPosition>().Destroy();
+6 -1
View File
@@ -1,4 +1,4 @@
using System;
using System;
using Rewired;
namespace MiraAPI.Keybinds;
@@ -23,6 +23,11 @@ public class BaseKeybind
/// </summary>
public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByKeybind(this);
/// <summary>
/// Gets the display label of the currently assigned button on the active controller.
/// </summary>
public string CurrentControllerButton => KeybindUtils.GetControllerButtonByKeybind(this);
/// <summary>
/// Gets or sets the handler of the keybind. Invoked when the keybind is activated.
/// </summary>
+87
View File
@@ -0,0 +1,87 @@
namespace MiraAPI.Keybinds;
/// <summary>
/// Controller buttons used for default keybinds.
/// </summary>
public enum ControllerButton
{
/// <summary>
/// The bottom face button (Cross on PlayStation, A on Xbox).
/// </summary>
South = 4,
/// <summary>
/// The right face button (Circle on PlayStation, B on Xbox).
/// </summary>
East = 5,
/// <summary>
/// The left face button (Square on PlayStation, X on Xbox).
/// </summary>
West = 7,
/// <summary>
/// The top face button (Triangle on PlayStation, Y on Xbox).
/// </summary>
North = 8,
/// <summary>
/// The left bumper (L1 on PlayStation, LB on Xbox).
/// </summary>
LeftBumper = 10,
/// <summary>
/// The right bumper (R1 on PlayStation, RB on Xbox).
/// </summary>
RightBumper = 12,
/// <summary>
/// The left trigger (L2 on PlayStation, LT on Xbox).
/// </summary>
LeftTrigger = 11,
/// <summary>
/// The right trigger (R2 on PlayStation, RT on Xbox).
/// </summary>
RightTrigger = 13,
/// <summary>
/// The left stick button (L3 on PlayStation).
/// </summary>
LeftStick = 17,
/// <summary>
/// The right stick button (R3 on PlayStation).
/// </summary>
RightStick = 18,
/// <summary>
/// The back button (Create on PlayStation, View on Xbox).
/// </summary>
Back = 14,
/// <summary>
/// The start button (Options on PlayStation, Menu on Xbox).
/// </summary>
Start = 15,
/// <summary>
/// The D-pad up button.
/// </summary>
DPadUp = 19,
/// <summary>
/// The D-pad right button.
/// </summary>
DPadRight = 20,
/// <summary>
/// The D-pad down button.
/// </summary>
DPadDown = 21,
/// <summary>
/// The D-pad left button.
/// </summary>
DPadLeft = 22,
}
+84 -16
View File
@@ -33,12 +33,40 @@ public static class KeybindManager
continue;
}
keybind.RewiredInputAction = instance.userData.RegisterModBind(
keybind.Id,
keybind.Name,
keybind.SourcePluginName,
keybind.DefaultKey,
modifiers: keybind.ModifierKeys);
keybind.RewiredInputAction = instance.userData.RegisterModBind(keybind.Id, keybind.Name, keybind.SourcePluginName, keybind.DefaultKey, modifiers: keybind.ModifierKeys);
if (keybind.DefaultControllerButton == null) continue;
var button = (int)keybind.DefaultControllerButton.Value;
var template = instance.dataFiles.GetJoystickTemplate(GamepadTemplate.typeGuid);
foreach (var map in instance.userData.joystickMaps)
{
if (map.categoryId != 1) continue;
var elementId = button;
var elementType = keybind.DefaultControllerButton is ControllerButton.LeftTrigger or ControllerButton.RightTrigger ? ControllerElementType.Axis : ControllerElementType.Button;
if (map.hardwareGuid != GamepadTemplate.typeGuid)
{
var controller = template?.joysticks.ToArray().FirstOrDefault(x => x.JoystickGuid == map.hardwareGuid);
elementId = controller?.GetJoystickElementId(button) ?? -1;
if (elementId < 0) continue;
var element = instance.dataFiles.GetHardwareJoystickMap(map.hardwareGuid)?.GetElementIdentifier(elementId);
if (element == null) continue;
elementType = element.elementType;
}
map.actionElementMaps.Add(new ActionElementMap
{
_actionId = keybind.RewiredInputAction.id,
_elementIdentifierId = elementId,
_elementType = elementType,
_axisRange = AxisRange.Positive,
_axisContribution = Pole.Positive,
});
}
}
}
catch (Exception e)
@@ -50,24 +78,66 @@ public static class KeybindManager
/// <summary>
/// Returns all conflicts where <see cref="MiraKeybind"/>s use the same key.
/// </summary>
/// <returns>A list of pairs of keybinds that conflict.</returns>
/// <returns>The conflicting keybinds grouped by keycode.</returns>
public static Dictionary<KeyboardKeyCode, List<MiraKeybind>> GetConflicts()
{
var conflicts = new Dictionary<KeyboardKeyCode, List<MiraKeybind>>();
foreach (var keybind in Keybinds)
{
var all = GetKeybindsForKey(keybind.CurrentKey, true);
var key = keybind.CurrentKey;
if (key == KeyboardKeyCode.None) continue;
var all = GetKeybindsForKey(key, true);
if (all.Length > 1)
{
if (!conflicts.TryGetValue(keybind.CurrentKey, out var group))
if (!conflicts.TryGetValue(key, out var group))
{
conflicts.Add(keybind.CurrentKey, []);
group = conflicts[keybind.CurrentKey];
group = [];
conflicts.Add(key, group);
}
if (!group.Contains(keybind))
if (!group.Contains(keybind)) group.Add(keybind);
}
}
return conflicts;
}
/// <summary>
/// Returns controller bindings shared by exclusive custom keybinds.
/// </summary>
/// <param name="enabledOnly">Whether to check only active controller maps and available HUD buttons.</param>
/// <returns>The conflicting custom bindings.</returns>
public static List<ActionElementMap> GetControllerConflicts(bool enabledOnly = false)
{
var conflicts = new List<ActionElementMap>();
var actions = Keybinds.Where(x => x.Exclusive && x.RewiredInputAction != null && (!enabledOnly || KeybindUtils.IsControllerActionActive(x.RewiredInputAction.id))).Select(x => x.RewiredInputAction!.id).ToHashSet();
var player = ReInput.players.GetPlayer(0);
for (var i = 0; i < player.controllers.joystickCount; i++)
{
var controller = player.controllers.Joysticks[i];
var maps = new CppCollections.List<ControllerMap>(player.controllers.maps.GetMaps(controller).Cast<CppCollections.IEnumerable<ControllerMap>>()).ToArray();
foreach (var map in maps)
{
if (enabledOnly && !map.enabled) continue;
foreach (var binding in map.GetElementMaps())
{
group.Add(keybind);
if (!binding.enabled || !actions.Contains(binding.actionId)) continue;
foreach (var otherMap in maps)
{
if (enabledOnly && !otherMap.enabled) continue;
if (!enabledOnly && map.categoryId != otherMap.categoryId && map.categoryId != 0 && otherMap.categoryId != 0) continue;
foreach (var other in otherMap.GetElementMaps())
{
if (!other.enabled || binding.actionId == other.actionId || !actions.Contains(other.actionId) || !binding.CheckForAssignmentConflict(other)) continue;
if (!conflicts.Contains(binding)) conflicts.Add(binding);
if (!conflicts.Contains(other)) conflicts.Add(other);
}
}
}
}
}
@@ -79,7 +149,7 @@ public static class KeybindManager
/// Returns all <see cref="MiraKeybind"/>s for a specified keycode.
/// </summary>
/// <param name="keyCode">The key to look for.</param>
/// /// <param name="exclusiveCheck">Should only exclusive keybinds be present.</param>
/// <param name="exclusiveCheck">Whether to return only exclusive keybinds.</param>
/// <returns>The list of <see cref="MiraKeybind"/>s.</returns>
public static MiraKeybind[] GetKeybindsForKey(KeyboardKeyCode keyCode, bool exclusiveCheck = false)
{
@@ -87,9 +157,7 @@ public static class KeybindManager
foreach (var keybind in Keybinds)
{
if (keybind.CurrentKey == keyCode && (keybind.Exclusive || !exclusiveCheck))
{
all.Add(keybind);
}
}
return [.. all];
+42 -4
View File
@@ -1,10 +1,12 @@
using System;
using System.Linq;
using MiraAPI.Hud;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Collection of keybind-related utility classes.
/// Collection of keybind-related utilities.
/// </summary>
public static class KeybindUtils
{
@@ -23,6 +25,25 @@ public static class KeybindUtils
return keybind.RewiredInputAction == null ? KeyboardKeyCode.None : GetKeycodeByActionId(keybind.RewiredInputAction.id);
}
/// <summary>
/// Returns the display label for the currently assigned controller button of a <see cref="BaseKeybind"/>.
/// </summary>
/// <param name="keybind">The keybind to get the controller button from.</param>
/// <returns>The button label, or an empty string if no controller binding is available.</returns>
public static string GetControllerButtonByKeybind(BaseKeybind keybind)
{
var map = keybind.RewiredInputAction == null ? null : GetControllerElementMap(keybind.RewiredInputAction.id);
var name = map?.elementIdentifierName ?? string.Empty;
return name.ToLowerInvariant() switch
{
"cross" => "X",
"square" => "□",
"circle" => "○",
"triangle" => "△",
_ => name,
};
}
/// <summary>
/// Gets the keycode for an action with <see cref="ReInput"/>.
/// </summary>
@@ -37,23 +58,40 @@ public static class KeybindUtils
/// Gets a <see cref="ActionElementMap"/> by id with <see cref="ReInput"/>.
/// </summary>
/// <param name="actionId">The action ID.</param>
/// <returns>The keyboard keycode.</returns>
/// <returns>The keyboard binding, or null if the action is unassigned.</returns>
public static ActionElementMap? GetActionElementMap(int actionId)
{
var player = ReInput.players.GetPlayer(0);
return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false);
}
/// <summary>
/// Gets the binding for an action on the active controller.
/// </summary>
/// <param name="actionId">The action ID.</param>
/// <returns>The controller binding, or null if no active controller or binding is available.</returns>
public static ActionElementMap? GetControllerElementMap(int actionId)
{
var player = ReInput.players.GetPlayer(0);
var controller = player.controllers.GetLastActiveController(ControllerType.Joystick);
return controller == null ? null : player.controllers.maps.GetFirstElementMapWithAction(controller, actionId, false);
}
/// <summary>
/// Gets an <see cref="InputAction"/> by id.
/// </summary>
/// <param name="id">The action identifier name.</param>
/// <returns>The keyboard keycode.</returns>
/// <param name="id">The action ID.</param>
/// <returns>The input action, or null if it is unavailable.</returns>
public static InputAction? GetInputActionById(int id)
{
return RewiredInputManager?.userData?.GetActionById(id);
}
internal static bool IsControllerActionActive(int actionId)
{
return CustomButtonManager.Buttons.Any(x => x.Keybind?.RewiredInputAction?.id == actionId && x.Button != null && x.Button.isActiveAndEnabled && x.Enabled(PlayerControl.LocalPlayer?.Data?.Role));
}
/// <summary>
/// Finds and returns an unused KeyCode that is not equal to the excluded key.
/// </summary>
+2 -1
View File
@@ -1,10 +1,11 @@
using Rewired;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Class containing all default <see cref="MiraKeybind"/>s.
/// Recommended for cross-mod compatibility.
/// Controller bindings are unassigned by default.
/// </summary>
[RegisterCustomKeybinds]
public static class MiraGlobalKeybinds
+22 -6
View File
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using Rewired;
namespace MiraAPI.Keybinds;
@@ -10,11 +10,7 @@ namespace MiraAPI.Keybinds;
/// <param name="defaultKeycode">The default keycode.</param>
/// <param name="modifierKeys">Up to 3 optional <see cref="ModifierKey"/>s.</param>
/// <param name="exclusive">Is exclusive.</param>
public class MiraKeybind(
string name,
KeyboardKeyCode? defaultKeycode,
ModifierKey[]? modifierKeys = null,
bool exclusive = true) : BaseKeybind(name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_'))
public class MiraKeybind(string name, KeyboardKeyCode? defaultKeycode, ModifierKey[]? modifierKeys = null, bool exclusive = true) : BaseKeybind(name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_'))
{
/// <summary>
/// Gets name of the keybind.
@@ -26,6 +22,11 @@ public class MiraKeybind(
/// </summary>
public KeyboardKeyCode DefaultKey { get; } = defaultKeycode ?? KeyboardKeyCode.None;
/// <summary>
/// Gets the default controller button. If <see langword="null"/>, the controller binding is left unassigned.
/// </summary>
public ControllerButton? DefaultControllerButton { get; }
/// <summary>
/// Gets the <see cref="ModifierKey"/>s assigned to this keybind.
/// Due to Rewired limitations, there can only be 3 <see cref="ModifierKey"/>s.
@@ -34,11 +35,26 @@ public class MiraKeybind(
/// <summary>
/// Gets a value indicating whether this keybind should be checked for conflicts with other exclusive keybinds using the same key.
/// Active exclusive controller bindings take priority over vanilla actions during gameplay.
/// </summary>
public bool Exclusive { get; } = exclusive;
internal string? SourcePluginName { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="MiraKeybind"/> class with a controller binding.
/// </summary>
/// <param name="name">The name of the keybind.</param>
/// <param name="defaultKeycode">The default keycode.</param>
/// <param name="defaultControllerButton">The default controller button.</param>
/// <param name="modifierKeys">Up to 3 optional <see cref="ModifierKey"/>s for the keyboard binding.</param>
/// <param name="exclusive">Is exclusive.</param>
public MiraKeybind(string name, KeyboardKeyCode? defaultKeycode, ControllerButton defaultControllerButton, ModifierKey[]? modifierKeys = null, bool exclusive = true)
: this(name, defaultKeycode, modifierKeys, exclusive)
{
DefaultControllerButton = defaultControllerButton;
}
/// <inheritdoc/>
public override string ToString()
{
@@ -1,4 +1,4 @@
using System;
using System;
namespace MiraAPI.Keybinds;
+1 -1
View File
@@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
namespace MiraAPI.Keybinds;
+1 -1
View File
@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
namespace MiraAPI.Keybinds;
+26
View File
@@ -4,6 +4,7 @@ using BepInEx;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.Patches;
using MiraAPI.Patches.Roles;
using MiraAPI.PluginLoading;
using MiraAPI.Translation;
@@ -63,9 +64,34 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
internal static ConfigEntry<SortingGroups> WikiSortingGroup { get; private set; }
internal static ConfigEntry<SortingMethod> WikiSortingMethod { get; private set; }
internal static ConfigEntry<SortingFilter> WikiSortingFilter { get; private set; }
internal static ConfigEntry<string> DistanceSuffix { get; private set; }
/// <inheritdoc />
public override void Load()
{
WikiSortingGroup = Config.Bind(
"Wiki",
"WikiSortingGroup",
SortingGroups.Ungrouped,
"Determines how clusters of roles / modifiers are grouped together, if at all.");
WikiSortingMethod = Config.Bind(
"Wiki",
"WikiSortingMethod",
SortingMethod.Alphabetical,
"Determines if the list of roles / modifiers are organized in ascending or descending order.");
WikiSortingFilter = Config.Bind(
"Wiki",
"WikiSortingFilter",
SortingFilter.AllRoles,
"Determines what roles / modifiers are displayed at once.");
DistanceSuffix = Config.Bind(
"Display",
"Distance",
"unit(s)",
"Dictates the unit of distance used in numeric option displays representing a distance.");
Harmony.PatchAll();
RoleOptionsCollectionPatch.PatchRoleMethods(Harmony);
UiResetEvents.Initialize();
+45 -1
View File
@@ -69,6 +69,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
OldUiButtonScaleFactor = TopRightButtonsFactorSlider.Value;
}
else if (configEntry == MatchInfoGuideScaleFactor && MatchInfoGuide.Instance)
{
RoleGuidePatches.ApplyGuideScale(MatchInfoGuide.Instance, MatchInfoGuideScaleFactor.Value);
}
else if (configEntry == WikiOnBottomRow)
{
SetUpButtonPositions();
@@ -216,11 +220,24 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
[LocalToggleSetting]
public ConfigEntry<bool> ShowKeybinds { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.ShowKeybindsOnButtons", true);
/// <summary>
/// Gets the scale of the match info guide.
/// </summary>
[LocalSliderSetting(min: 0.5f, max: 1f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)]
public ConfigEntry<float> MatchInfoGuideScaleFactor { get; private set; } = config.Bind("MiraApi.MatchInfoGuide", "MiraApi.MatchInfoGuideScaleFactor", 1f);
/// <summary>
/// Gets the flag indicating whether the wiki is on the bottom row.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> WikiOnBottomRow { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.WikiOnBottomRow", true);
public ConfigEntry<bool> WikiOnBottomRow { get; private set; } = config.Bind("MiraApi.MatchInfoGuide", "MiraApi.WikiOnBottomRow", true);
/// <summary>
/// Gets the flag indicating the position for the wiki's tabs.
/// </summary>
[LocalEnumSetting(names: ["MiraApi.WikiTabPosition.LeftSide", "MiraApi.WikiTabPosition.RightSide", "MiraApi.WikiTabPosition.Below"])]
public ConfigEntry<MatchInfoTabPos> WikiTabPosition { get; private set; } =
config.Bind("MiraApi.MatchInfoGuide", "MiraApi.WikiTabPosition", MiraApiPlugin.IsMobile ? MatchInfoTabPos.RightSide : MatchInfoTabPos.LeftSide);
/// <summary>
/// Gets whether to show the red flash from sabotages.
@@ -233,4 +250,31 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> EnableSabotageBlares { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableSabotageBlare", true);
/// <summary>
/// Gets whether variable colors will remain static or not.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> EnableVariableColors { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableVariableColors", true);
}
/// <summary>
/// An enum that dictates the location of the match info tab buttons.
/// </summary>
public enum MatchInfoTabPos
{
/// <summary>
/// The left side.
/// </summary>
LeftSide,
/// <summary>
/// The right side.
/// </summary>
RightSide,
/// <summary>
/// Below the menu.
/// </summary>
Below,
}
+94 -1
View File
@@ -1,7 +1,13 @@
using System;
using System.Collections.Generic;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.GameOptions;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
namespace MiraAPI.Modifiers;
@@ -44,16 +50,98 @@ public abstract class BaseModifier : IOptionable
x => x.InternalModifiers.Exists(y => y.TypeId == TypeId))
?? throw new InvalidOperationException("Modifier is not registered.");
/// <summary>
/// Gets the id part used to build the modifier's translation keys. It is recommended to simply call it what the modifier is, as other mods may utilize it.
/// </summary>
public virtual string IdPart => GetType().Name;
/// <summary>
/// Gets the id part used to build the modifier's translation keys. It is recommended to call it ModGuid.Modifier.Type.
/// </summary>
public virtual string IdPrefix => GetType().Namespace!;
/// <summary>
/// Gets the modifier name.
/// </summary>
public abstract string ModifierName { get; }
public virtual string ModifierName => MiraLocaleManager.Get(ModifierNameLocale);
/// <summary>
/// Gets the modifier's name id for localization.
/// </summary>
public virtual string ModifierNameLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart);
/// <summary>
/// Gets the medium description of the modifier. Used in the modifier guide and options menu.
/// </summary>
public virtual string ModifierMedDescription => MiraLocaleManager.Get(ModifierMedDescriptionLocale, GetDescription());
/// <summary>
/// Gets the modifier's medium description id for localization.
/// </summary>
public virtual string ModifierMedDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "MedDescription");
/// <summary>
/// Gets the wiki description of the modifier. Used in the wiki and normally appends the options text as well.
/// </summary>
public virtual string ModifierWikiDescription => MiraLocaleManager.Get(ModifierWikiDescriptionLocale, GetDescription()) +
Helpers.GetOptionsText(GetType());
/// <summary>
/// Gets the modifier's wiki description id for localization.
/// </summary>
public virtual string ModifierWikiDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "WikiDescription");
/// <summary>
/// Gets the category of the modifier. Used in the wiki.
/// </summary>
public virtual string ModifierCategoryTitle => MiraLocaleManager.Get("Modifier");
/// <summary>
/// Gets whether the modifier is forcibly shown or disabled in the wiki screen.
/// </summary>
/// <returns><see langword="true"/> if the modifier is always displayed, otherwise <see langword="false"/> if it is never displayable, or <see langword="null"/> if it is dictated by amount and chance.</returns>
public virtual bool? ForceShowModifierOnWiki => false;
/// <summary>
/// Gets the information to display in the wiki.
/// </summary>
/// <param name="guide">The guide object.</param>
/// <param name="titleText">The title text object.</param>
/// <param name="titleIcon">The title's icon.</param>
/// <param name="parent">The scroller parent.</param>
/// <returns>The <see cref="GameObject"/> of the wiki.</returns>
public virtual GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, SpriteRenderer titleIcon, Scroller parent)
{
var abilities = WikiAbilities;
return Helpers.CreateAdvancedWikiPage(
guide,
titleText,
titleIcon,
parent,
ModifierNameLocale,
$"{ModifierName} ({ModifierCategoryTitle})".TrimStart(),
ModifierIcon ?? MiraAssets.IconNeutral.LoadAsset(),
ModifierWikiDescription,
abilities);
}
/// <summary>
/// Gets the list of abilities or other similar information to display below the text of a modifier's wiki page.
/// </summary>
[HideFromIl2Cpp]
public virtual List<AdvancedWikiAbilityDescription> WikiAbilities => [];
/// <summary>
/// Gets the modifier icon. Useless if <see cref="HideOnUi"/> is <see langword="true"/>.
/// </summary>
public virtual LoadableAsset<Sprite>? ModifierIcon => null;
/// <summary>
/// Gets the <see cref="TMP_SpriteAsset"/> for the Modifier Icon.
/// </summary>
[HideFromIl2Cpp]
public virtual TMP_SpriteAsset IconTmp => null!;
/// <summary>
/// Gets a value indicating whether the modifier is hidden on the UI. Will be hidden either way if no description is provided.
/// </summary>
@@ -69,6 +157,11 @@ public abstract class BaseModifier : IOptionable
/// </summary>
public virtual Color FreeplayFileColor => Color.gray;
/// <summary>
/// Gets the <see cref="Color"/> of the modifier, used in the wiki and other UI outside freeplay.
/// </summary>
public virtual Color GeneralColor => FreeplayFileColor;
/// <summary>
/// Gets a value indicating whether the modifier is unique. If <see langword="true"/>, the player can only have one instance of this modifier.
/// </summary>
+3
View File
@@ -9,6 +9,9 @@ namespace MiraAPI.Modifiers.Types;
[MiraIgnore]
public abstract class GameModifier : BaseModifier
{
/// <inheritdoc />
public override bool? ForceShowModifierOnWiki => null;
/// <inheritdoc />
public override bool ShowInFreeplay => true;
+2 -2
View File
@@ -12,5 +12,5 @@ dotnet_diagnostic.SA1611.severity = none
# SA1615: Element return value should be documented.
dotnet_diagnostic.SA1615.severity = none
# S125: Sections of code should not be commented out.
dotnet_diagnostic.S125.severity = none
# SA1602: Enumeration items should be documented.
dotnet_diagnostic.SA1602.severity = none
+1 -1
View File
@@ -36,7 +36,7 @@ internal static class AmongUsClientSyncPatch
GameModeOption.RpcSyncGamemode(PlayerControl.LocalPlayer, GameModeOption.Value);
}
[HarmonyPostfix]
[HarmonyPrefix]
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerLeft))]
public static void PlayerLeftPatch(ClientData data, DisconnectReasons reason)
{
@@ -0,0 +1,110 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Colors;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using UnityEngine;
namespace MiraAPI.Patches.Colors;
[HarmonyPatch(typeof(PlayerMaterial))]
internal static class SetPlayerMaterialPatch
{
[HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(int), typeof(Renderer))]
public static bool Prefix(int colorId, Renderer rend)
{
rend.gameObject.GetOrAddComponent<VariableColorBehaviour>().SetColor(
PaletteManager.TryGetCustomColor(colorId, out var color) && color is VariableColor variable
? variable
: null);
return !PaletteManager.IsVariable(colorId);
}
[HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(Color), typeof(Renderer))]
public static void Prefix(Renderer rend)
{
rend.gameObject.GetOrAddComponent<VariableColorBehaviour>().SetColor(null);
}
}
[HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.Update))]
internal static class PlayerTabPatch
{
public static void Postfix(PlayerTab __instance)
{
for (var i = 0; i < __instance.ColorChips.Count; i++)
{
__instance.ColorChips[i].Inner.SpriteColor = PaletteManager.GetMainColor(i);
}
}
}
[HarmonyPatch(typeof(ChatNotification), nameof(ChatNotification.Update))]
internal static class ChatNotifRainbowPatch
{
private static readonly Regex RichTags = new(@"<[^>]*>", default, Regex.InfiniteMatchTimeout);
public static void Postfix(ChatNotification __instance)
{
if (!__instance.gameObject.active)
return;
var colorId = __instance.player.cosmetics.ColorId;
if (!PaletteManager.IsVariable(colorId))
return;
var color = PaletteManager.GetMainColor(colorId);
var str = color.ToHtmlStringRGBA();
var nameText = __instance.playerNameText;
nameText.text = "<color=#" + str + ">" + RichTags.Replace(nameText.text, string.Empty);
nameText.outlineColor = color.GetShadowColor(0.5f);
}
}
[HarmonyPatch(typeof(HostInfoPanel), nameof(HostInfoPanel.Update))]
internal static class RainbowLobbyInfoPanePatch
{
[SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Operator becomes too large.")]
public static void Postfix(HostInfoPanel __instance)
{
if (!__instance.gameObject.activeInHierarchy)
return;
var colorId = __instance.player.cosmetics.ColorId;
if (!PaletteManager.IsVariable(colorId))
return;
var host = GameData.Instance.GetHost();
if (!host)
return;
var text = PaletteManager.GetMainColor(colorId).ToHtmlStringRGBA();
var label = __instance.hostLabel;
label.text = TranslationController.Instance.GetString(StringNames.HostNounLabel);
if (__instance.ShouldBoldenHostLabel(DataManager.Settings.Language.CurrentLanguage))
{
label.text = "<b>" + label.text + "</b>";
}
if (AmongUsClient.Instance.AmHost)
{
__instance.playerName.text = (string.IsNullOrEmpty(host.PlayerName)
? "..."
: $"<color=#{text}>{host.PlayerName}</color>")
+ " <size=90%><b><font=\"Barlow-BoldItalic SDF\" material=\"Barlow-BoldItalic SDF Outline\">" +
TranslationController.Instance.GetString(StringNames.HostYouLabel);
}
else
{
__instance.playerName.text =
(string.IsNullOrEmpty(host.PlayerName) ? "..." : $"<color=#{text}>{host.PlayerName}</color>") +
" (" + __instance.player.ColorBlindName + ")";
}
}
}
@@ -23,10 +23,18 @@ public static class VanillaButtonPatches
{
playerTarget = role.Cast<TrackerRole>().currentTarget;
}
else if (role.Role is RoleTypes.Detective)
{
playerTarget = role.Cast<DetectiveRole>().currentTarget;
}
else if (role.Role is RoleTypes.GuardianAngel)
{
playerTarget = role.Cast<GuardianAngelRole>().currentTarget;
}
else if (role.Role is RoleTypes.SpiritGuide)
{
playerTarget = role.Cast<SpiritGuideRole>().currentTarget;
}
else if (role.Role is RoleTypes.Engineer)
{
ventTarget = role.Cast<EngineerRole>().currentTarget;
@@ -53,6 +61,10 @@ public static class VanillaButtonPatches
{
playerTarget = role.Cast<DetectiveRole>().currentTarget;
}
else if (role.Role is RoleTypes.SpiritGuide)
{
playerTarget = role.Cast<SpiritGuideRole>().currentTarget;
}
var genericEvent = new VanillaButtonClickEvent(__instance, playerTarget);
MiraEventManager.InvokeEvent(genericEvent);
+3 -4
View File
@@ -1,6 +1,5 @@
using System.Collections;
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.GameModes;
using MiraAPI.Translation;
using Reactor.Utilities;
@@ -16,7 +15,7 @@ internal static class HudPatches
[HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))]
public static void HudUpdatePatch(HudManager __instance)
{
if (GameManager.Instance != null && GameManager.Instance.GameHasStarted &&
if (GameManager.Instance && GameManager.Instance.GameHasStarted &&
CustomGameModeManager.ActiveMode != null)
{
CustomGameModeManager.ActiveMode.HudUpdate(__instance);
@@ -32,7 +31,7 @@ internal static class HudPatches
private static IEnumerator CoPostHudStart(HudManager instance)
{
yield return new WaitUntil((Func<bool>)(() => GameManager.Instance != null));
yield return new WaitUntil((Il2CppSystem.Func<bool>)(() => GameManager.Instance != null));
if (GameManager.Instance.IsHideAndSeek())
yield return null;
var infoPane = instance.gameObject.transform.FindChild("LobbyInfoPane");
@@ -42,7 +41,7 @@ internal static class HudPatches
var modelText = modeLabel!.Find("Text_TMP").gameObject;
var modelTextClone = Object.Instantiate(modelText, modeLabel);
Object.Destroy(modelTextClone.GetComponent<TextTranslatorTMP>());
modelTextClone.GetComponent<TextMeshPro>().text = "Gamemode";
modelTextClone.GetComponent<TextMeshPro>().text = MiraLocaleManager.Get("MiraApi.Gamemode", "Gamemode");
var gmText = modeValue!.Find("GameModeText").gameObject;
var gmTextClone = Object.Instantiate(gmText, modeValue);
gmText.SetActive(false);
+28 -112
View File
@@ -1,6 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using HarmonyLib;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
@@ -28,47 +27,29 @@ public static class HudManagerPatches
private static Dictionary<TextMeshPro, int> vanillaKeybindIcons = [];
[SuppressMessage("Critical Code Smell", "S2223:Non-constant static fields should not be visible", Justification = "This is internal and will never be used by user code.")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "This is internal and will never be used by user code.")]
internal static List<TextMeshPro> ModdedKeybindIcons = [];
public static IEnumerator CoResizeUI()
{
while (!HudManager.Instance)
{
yield return null;
}
while (!HudManager.Instance) yield return null;
yield return new WaitForSeconds(0.01f);
ResizeUI(LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value);
}
private static Transform _storedButtonsParent;
public static void ResizeUI(float scaleFactor)
{
if (!_storedButtonsParent)
{
_storedButtonsParent = HudManager.Instance.transform.FindChild("Buttons");
}
if (!_storedButtonsParent) _storedButtonsParent = HudManager.Instance.transform.FindChild("Buttons");
if (_storedButtonsParent)
{
foreach (var aspect in _storedButtonsParent.GetComponentsInChildren<AspectPosition>(true))
{
if (!aspect.gameObject)
{
continue;
}
if (!aspect.gameObject) continue;
if (aspect.gameObject.transform.parent.name == "TopRight")
{
continue;
}
if (aspect.gameObject.transform.parent.name == "TopRight") continue;
if (aspect.gameObject.name.Contains("TopRight"))
{
continue;
}
if (aspect.gameObject.name.Contains("TopRight")) continue;
aspect.gameObject.SetActive(!aspect.isActiveAndEnabled);
aspect.DistanceFromEdge *= new Vector2(scaleFactor, scaleFactor);
@@ -78,10 +59,7 @@ public static class HudManagerPatches
foreach (var button in HudManager.Instance.GetComponentsInChildren<ActionButton>(true))
{
if (!button.gameObject)
{
continue;
}
if (!button.gameObject) continue;
button.gameObject.SetActive(!button.isActiveAndEnabled);
button.gameObject.transform.localScale *= scaleFactor;
@@ -91,15 +69,9 @@ public static class HudManagerPatches
if (!_storedButtonsParent) return;
foreach (var arrange in _storedButtonsParent.GetComponentsInChildren<GridArrange>(true))
{
if (!arrange.gameObject || !arrange.transform)
{
continue;
}
if (!arrange.gameObject || !arrange.transform) continue;
if (arrange.gameObject.name.Contains("TopRight"))
{
continue;
}
if (arrange.gameObject.name.Contains("TopRight")) continue;
arrange.gameObject.SetActive(!arrange.isActiveAndEnabled);
arrange.CellSize = new Vector2(scaleFactor, scaleFactor);
@@ -143,10 +115,7 @@ public static class HudManagerPatches
[HarmonyPatch(typeof(ChatController), nameof(ChatController.Close))]
public static void TogglePrefix(ChatController __instance)
{
if (!MiraHudHelper.ClonedChatButton)
{
return;
}
if (!MiraHudHelper.ClonedChatButton) return;
__instance.chatButton.transform.localPosition = MiraHudHelper.ClonedChatButton.transform.localPosition + new Vector3(-0.3f, 0);
}
@@ -179,25 +148,13 @@ public static class HudManagerPatches
public static void StartPostfix(HudManager __instance)
{
__instance.gameObject.AddComponent<MiraHudHelper>();
if (Buttons == null)
{
Buttons = __instance.transform.Find("Buttons");
}
if (Buttons == null) Buttons = __instance.transform.Find("Buttons");
if (BottomRight == null)
{
BottomRight = Buttons.Find("BottomRight");
}
if (BottomRight == null) BottomRight = Buttons.Find("BottomRight");
if (BottomLeft == null)
{
BottomLeft = Object.Instantiate(BottomRight.gameObject, Buttons);
}
if (BottomLeft == null) BottomLeft = Object.Instantiate(BottomRight.gameObject, Buttons);
foreach (var t in BottomLeft.GetComponentsInChildren<ActionButton>(true))
{
t.gameObject.Destroy();
}
foreach (var t in BottomLeft.GetComponentsInChildren<ActionButton>(true)) t.gameObject.Destroy();
var gridArrange = BottomLeft.GetComponent<GridArrange>();
var aspectPosition = BottomLeft.GetComponent<AspectPosition>();
@@ -209,10 +166,7 @@ public static class HudManagerPatches
if (Constants.GetPlatformType() is Platforms.Android or Platforms.IPhone)
{
var fakeButton = Object.Instantiate(__instance.AbilityButton, BottomLeft.transform);
foreach (var renderer in fakeButton.GetComponentsInChildren<Renderer>())
{
renderer.enabled = false;
}
foreach (var renderer in fakeButton.GetComponentsInChildren<Renderer>()) renderer.enabled = false;
fakeButton.buttonLabelText.Destroy();
fakeButton.cooldownTimerText.Destroy();
@@ -222,8 +176,6 @@ public static class HudManagerPatches
fakeButton.Destroy();
}
ModdedKeybindIcons = [];
foreach (var button in CustomButtonManager.CustomButtons)
{
var location = button.Location switch
@@ -233,10 +185,7 @@ public static class HudManagerPatches
_ => null,
};
if (location is null)
{
continue;
}
if (location is null) continue;
try
{
@@ -272,25 +221,21 @@ public static class HudManagerPatches
var buttonObj = kvp.Key;
var actionId = kvp.Value;
var key = KeybindUtils.GetKeycodeByActionId(actionId);
if (key == KeyboardKeyCode.None)
{
continue;
}
var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos);
vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent<TextMeshPro>(), actionId);
if (!MiraApiPlugin.IsMobile)
{
var comp = buttonObj.GetComponent<ActionButton>();
KeybindManager.VanillaKeybinds[comp.GetType()].Button = comp;
}
var key = KeybindUtils.GetKeycodeByActionId(actionId);
if (key == KeyboardKeyCode.None) continue;
var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos);
vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent<TextMeshPro>(), actionId);
}
MiraApiSettings.OldButtonScaleFactor =
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value;
MiraApiSettings.OldUiButtonScaleFactor =
LocalSettingsTabSingleton<MiraApiSettings>.Instance.TopRightButtonsFactorSlider.Value;
MiraApiSettings.OldButtonScaleFactor = LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value;
MiraApiSettings.OldUiButtonScaleFactor = LocalSettingsTabSingleton<MiraApiSettings>.Instance.TopRightButtonsFactorSlider.Value;
Coroutines.Start(CoResizeUI());
}
@@ -306,10 +251,7 @@ public static class HudManagerPatches
public static void SetHudActivePostfix(HudManager __instance, PlayerControl localPlayer, RoleBehaviour role, bool isActive)
{
__instance.AdminButton.ToggleVisible(isActive && role.IsImpostor && GameOptionsManager.Instance.CurrentGameOptions.GameMode == AmongUs.GameOptions.GameModes.HideNSeek);
if (localPlayer.Data == null)
{
return;
}
if (localPlayer.Data == null) return;
foreach (var button in CustomButtonManager.CustomButtons)
{
@@ -328,8 +270,7 @@ public static class HudManagerPatches
[HarmonyPostfix]
public static void UpdatePostfix()
{
var canSeeBinds = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value;
var canSeeBinds = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard && LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value;
foreach (var btnIcon in vanillaKeybindIcons)
{
@@ -337,44 +278,19 @@ public static class HudManagerPatches
btnIcon.Key.transform.parent.gameObject.SetActive(canSeeBinds);
}
foreach (var btnIcon in ModdedKeybindIcons)
{
btnIcon.transform.parent.gameObject.SetActive(canSeeBinds);
}
foreach (var button in CustomButtonManager.CustomButtons) button.UpdateKeybindIcon();
var player = ReInput.players.GetPlayer(0);
var keyboard = player.controllers.Keyboard;
foreach (var entry in KeybindManager.Keybinds)
{
var modKeys = entry.ModifierKeys;
bool modifierKeysPressed;
if (modKeys.Length == 0)
{
modifierKeysPressed = true;
}
else
{
modifierKeysPressed = true;
foreach (var key in modKeys)
{
if (keyboard.GetModifierKey(key)) continue;
modifierKeysPressed = false;
break;
}
}
if (player.GetButtonDown(entry.Id) && modifierKeysPressed)
{
if (entry.RewiredInputAction != null && player.GetButtonDown(entry.RewiredInputAction.id))
entry.Invoke();
}
}
foreach (var entry in KeybindManager.VanillaKeybinds.Values)
{
if (player.GetButtonDown(entry.Id))
{
entry.Invoke();
}
}
}
}
@@ -1,6 +1,7 @@
using System.Linq;
using System.Linq;
using HarmonyLib;
using MiraAPI.Keybinds;
using Rewired;
using Rewired.UI.ControlMapper;
using UnityEngine;
using UnityEngine.UI;
@@ -10,53 +11,71 @@ namespace MiraAPI.Patches.Keybinds;
[HarmonyPatch(typeof(ControlMapper))]
public static class ControlMapperPatches
{
// Patching this crashes for Epic Games users
/*[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Start))]
private static void StartPrefix(ControlMapper __instance)
[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Initialize))]
private static void InitializePrefix(ControlMapper __instance)
{
var doneButton = GameObject.Find("DoneButton")?.GetComponent<CustomButton>();
if (doneButton == null)
{
return;
}
__instance.showControllers = true;
__instance.showGlyphs = false;
}
var toggle = Object.Instantiate(doneButton, doneButton.transform.parent);
var text = toggle.GetComponentInChildren<TextMeshProUGUI>();
var entry = LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds;
toggle.gameObject.name = "KeybindsVisibleToggle";
text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}";
toggle.onClick.RemoveAllListeners();
toggle.GetComponent<ButtonInfo>().identifier = "KeybindsVisibleToggle";
toggle.onClick.AddListener((UnityAction)(() =>
{
entry.Value = !entry.Value;
text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}";
}));
}*/
[HarmonyPostfix]
[HarmonyPatch(nameof(ControlMapper.CreateLayout))]
private static void CreateLayoutPostfix(ControlMapper __instance)
{
__instance.references.controllerGroup.gameObject.SetActive(false);
__instance.references.assignedControllersGroup.gameObject.SetActive(false);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.GetControllerMap))]
private static bool GetControllerMapPrefix(ControlMapper __instance, ControllerType type, ref ControllerMap? __result)
{
if (type != ControllerType.Joystick) return true;
__result = __instance.currentPlayer?.controllers.maps.GetFirstMapInCategory(type, __instance.currentJoystickId, 1);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.HasElementAssignmentConflicts))]
private static bool HasElementAssignmentConflictsPrefix(ControlMapper.InputMapping mapping, ref bool __result)
{
if (mapping == null || mapping.controllerType != ControllerType.Joystick || !KeybindManager.Keybinds.Any(x => x.RewiredInputAction?.id == mapping.fieldInfo.actionId)) return true;
__result = false;
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Update))]
private static void UpdatePrefix(ControlMapper __instance)
{
var conflicts = KeybindManager.GetConflicts();
var controllerConflicts = KeybindManager.GetControllerConflicts();
foreach (var element in __instance.themedElements)
{
var info = element.GetComponent<InputFieldInfo>();
if (info == null)
{
continue;
}
if (info == null) continue;
var key = info.glyphOrText?.actionElementMap?.keyboardKeyCode;
if (key == null)
{
continue;
}
var map = ReInput.mapping.GetActionElementMap(info.actionElementMapId);
var image = element.GetComponent<Image>();
if (image == null) continue;
var conflicts = KeybindManager.GetConflicts();
if (conflicts.Any(x => x.Key.ToString() == key.ToString()))
var hasConflict = map?.controllerMap.controllerType switch
{
element.GetComponent<Image>().color = Color.red;
ControllerType.Keyboard => conflicts.ContainsKey(map.keyboardKeyCode),
ControllerType.Joystick => controllerConflicts.Any(x => x.controllerMap.id == map.controllerMap.id && x.id == map.id),
_ => false,
};
if (hasConflict)
{
image.color = Color.red;
}
else if (image.color == Color.red)
{
image.color = Color.white;
element.ApplyTheme();
}
}
}
+6 -5
View File
@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using HarmonyLib;
using MiraAPI.Keybinds;
using Rewired;
@@ -10,19 +12,18 @@ public static class KeybindMenuPatch
private static bool _registered;
[HarmonyPrefix]
// ReSharper disable once InconsistentNaming
private static void AwakePrefix(InputManager_Base __instance)
{
if (_registered)
{
return;
}
if (_registered) return;
KeybindUtils.RewiredInputManager = __instance;
KeybindManager.RewiredInit();
_registered = true;
// Set these here right after input manager initializes
KeybindManager.VanillaKeybinds = new()
KeybindManager.VanillaKeybinds = new Dictionary<Type, VanillaKeybind>
{
{ typeof(KillButton), new VanillaKeybind(8) },
{ typeof(UseButton), new VanillaKeybind(6) },
@@ -1,4 +1,5 @@
using HarmonyLib;
using System.Linq;
using HarmonyLib;
using MiraAPI.Keybinds;
using UnityEngine;
@@ -10,40 +11,42 @@ public static class OptionsMenuBehaviourPatch
private static ButtonRolloverHandler? _remapRollover;
private static SpriteRenderer? _remapBackground;
private static bool Conflicts => KeybindManager.GetConflicts().Count > 0;
private static bool Conflicts => KeybindManager.GetConflicts().Count > 0 || KeybindManager.GetControllerConflicts().Count > 0;
[HarmonyPostfix]
[HarmonyPatch(nameof(OptionsMenuBehaviour.Open))]
private static void OpenPostfix()
private static void OpenPostfix(OptionsMenuBehaviour __instance)
{
_remapRollover = GameObject.Find("Remap Controls")?.GetComponent<ButtonRolloverHandler>()!;
try
{
_remapBackground = _remapRollover.transform.FindChild("Background").GetComponent<SpriteRenderer>();
_remapBackground.color = Conflicts ? Color.red : Color.white;
}
catch
{
// ignored, this normally breaks when on mobile since there's no remap button screen.
}
_remapRollover = __instance.GetComponentsInChildren<ButtonRolloverHandler>(true).FirstOrDefault(x => x.name == "Remap Controls");
_remapBackground = _remapRollover?.transform.FindChild("Background")?.GetComponent<SpriteRenderer>();
if (_remapBackground != null) _remapBackground.color = Conflicts ? Color.red : Color.white;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(OptionsMenuBehaviour.Update))]
private static bool UpdatePrefix(OptionsMenuBehaviour __instance)
{
if (MiraApiPlugin.IsMobile || ActiveInputManager.currentControlType != ActiveInputManager.InputType.Joystick) return true;
if (Input.GetKeyUp(KeyCode.Escape) && !__instance.KeyboardOptions.activeSelf) __instance.Close();
__instance.KeyboardOptions.SetActive(true);
__instance.MouseAndKeyboardOptions.SetActive(true);
return false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(OptionsMenuBehaviour.Update))]
private static void UpdatePostfix()
{
if (_remapRollover == null)
{
return;
}
if (_remapRollover == null) return;
if (_remapBackground == null)
{
return;
}
if (_remapBackground == null) return;
_remapRollover.OutColor = Conflicts ? Color.red : Color.white;
_remapRollover.UnselectedColor = Conflicts ? Color.red : Color.white;
_remapRollover.OverColor = Conflicts ? new Color32(255, 55, 55, 255) : Palette.AcceptedGreen;
var conflicts = Conflicts;
_remapRollover.OutColor = conflicts ? Color.red : Color.white;
_remapRollover.UnselectedColor = conflicts ? Color.red : Color.white;
_remapRollover.OverColor = conflicts ? new Color32(255, 55, 55, 255) : Palette.AcceptedGreen;
if (!conflicts && _remapBackground.color == Color.red) _remapBackground.color = Color.white;
}
}
@@ -0,0 +1,64 @@
using System.Linq;
using HarmonyLib;
using MiraAPI.Keybinds;
using Rewired;
namespace MiraAPI.Patches.Keybinds;
[HarmonyPatch(typeof(Player))]
public static class PlayerKeybindPatches
{
[HarmonyPostfix]
[HarmonyPatch(nameof(Player.GetButtonDown), typeof(int))]
private static void GetButtonPostfix(Player __instance, int __0, ref bool __result)
{
if (!__result || __instance.id != 0 || PlayerControl.LocalPlayer == null || ConsoleJoystick.inputState != ConsoleJoystick.ConsoleInputState.Gameplay || ControllerManager.Instance.IsUiControllerActive) return;
var keybind = KeybindManager.Keybinds.FirstOrDefault(x => x.RewiredInputAction?.id == __0);
if (keybind is { Exclusive: false }) return;
var sources = new CppCollections.List<InputActionSourceData>(__instance.GetCurrentInputSources(__0).Cast<CppCollections.IEnumerable<InputActionSourceData>>()).ToArray();
if (sources.Length == 0 || sources.Any(x => x.controllerType != ControllerType.Joystick)) return;
foreach (var source in sources)
{
var claimed = false;
foreach (var custom in KeybindManager.Keybinds)
{
if (custom == keybind) break;
if (!custom.Exclusive || custom.RewiredInputAction == null || !KeybindUtils.IsControllerActionActive(custom.RewiredInputAction.id)) continue;
var inputs = new CppCollections.List<InputActionSourceData>(__instance.GetCurrentInputSources(custom.RewiredInputAction.id).Cast<CppCollections.IEnumerable<InputActionSourceData>>()).ToArray();
if (!inputs.Any(x => x.controllerType == ControllerType.Joystick && x.controller.id == source.controller.id && x.actionElementMap.CheckForAssignmentConflict(source.actionElementMap))) continue;
claimed = true;
break;
}
if (!claimed) return;
}
__result = false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(Player.GetButton), typeof(int))]
private static void GetButtonHeldPostfix(Player __instance, int __0, ref bool __result)
{
GetButtonPostfix(__instance, __0, ref __result);
}
[HarmonyPostfix]
[HarmonyPatch(nameof(Player.GetButtonDown), typeof(string))]
private static void GetButtonByNamePostfix(Player __instance, string __0, ref bool __result)
{
if (__result) GetButtonPostfix(__instance, ReInput.mapping.GetActionId(__0), ref __result);
}
[HarmonyPostfix]
[HarmonyPatch(nameof(Player.GetButton), typeof(string))]
private static void GetButtonHeldByNamePostfix(Player __instance, string __0, ref bool __result)
{
if (__result) GetButtonPostfix(__instance, ReInput.mapping.GetActionId(__0), ref __result);
}
}
@@ -2,9 +2,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
@@ -15,12 +15,18 @@ namespace MiraAPI.Patches.LocalSettings;
public static class OptionsMenuPatches
{
private static readonly Dictionary<int, List<GameObject>> TabButtons = [];
private static int currentPage = 1;
private static int _currentPage = 1;
internal static OptionsMenuBehaviour? Instance { get; private set; }
[SuppressMessage("Critical Code Smell", "S2223:Non-constant static fields should not be visible", Justification = "Internal behaviour that does not need property-level validation.")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
[SuppressMessage(
"Critical Code Smell",
"S2223:Non-constant static fields should not be visible",
Justification = "Internal behaviour that does not need property-level validation.")]
[SuppressMessage(
"StyleCop.CSharp.MaintainabilityRules",
"SA1401:Fields should be private",
Justification = "Read above.")]
internal static BoxCollider2D MaskCollider = null!;
private static SpriteRenderer? background;
@@ -69,7 +75,7 @@ public static class OptionsMenuPatches
MaskCollider.isTrigger = true;
MaskCollider.enabled = true;
currentPage = 1;
_currentPage = 1;
TabButtons.Clear();
float yOffset = 0;
var i = 0;
@@ -83,7 +89,7 @@ public static class OptionsMenuPatches
{
var button = settings.CreateTabButton(__instance, ref tabIdx, ref yOffset);
button.GetComponent<TabGroup>().Content = tab;
button.SetActive(page == currentPage);
button.SetActive(page == _currentPage);
if (!TabButtons.TryGetValue(page, out var list))
{
@@ -105,20 +111,6 @@ public static class OptionsMenuPatches
}
}
static void ChangePage(int increase)
{
currentPage += increase;
if (currentPage > TabButtons.Count)
{
currentPage = 1;
}
if (currentPage < 1)
{
currentPage = TabButtons.Count;
}
}
// Create page buttons
// TODO: In-game menu has no close button. I tried making one from scratch but failed. Add one later plz
if (TabButtons.Count <= 1)
@@ -126,44 +118,56 @@ public static class OptionsMenuPatches
return;
}
var nextButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent<PassiveButton>();
nextButton.transform.localPosition = new Vector3(3.2f, 2.6f, -10);
nextButton.transform.localScale = new Vector2(0.25f, 0.25f);
var nextRend = nextButton.GetComponent<SpriteRenderer>();
nextRend.sprite = MiraAssets.NextButton.LoadAsset();
nextButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
CreatePageButton(__instance, 3.2f, false, 1);
CreatePageButton(__instance, 2.9f, true, -1);
}
var nextRollover = nextButton.gameObject.AddComponent<ButtonRolloverHandler>();
nextRollover.Target = nextRend;
nextRollover.OverColor = Palette.AcceptedGreen;
private static void CreatePageButton(OptionsMenuBehaviour menu, float x, bool flip, int direction)
{
var obj = new GameObject("PageButton") { layer = 5 };
obj.transform.SetParent(menu.transform);
obj.transform.localPosition = new Vector3(x, 2.6f, -10);
obj.transform.localScale = new Vector3(0.25f, 0.25f, 1);
nextButton.OnClick = new Button.ButtonClickedEvent();
nextButton.OnClick.AddListener(
var rend = obj.AddComponent<SpriteRenderer>();
rend.sprite = MiraAssets.NextButton.LoadAsset();
rend.flipX = flip;
var col = obj.AddComponent<BoxCollider2D>();
col.size = rend.sprite.bounds.size;
var rollover = obj.AddComponent<ButtonRolloverHandler>();
rollover.Target = rend;
rollover.OverColor = Palette.AcceptedGreen;
rollover.OutColor = Color.white;
var button = obj.AddComponent<PassiveButton>();
button.Colliders = new Il2CppReferenceArray<Collider2D>([col]);
button.OnClick = new Button.ButtonClickedEvent();
button.OnClick.AddListener(
(UnityAction)(() =>
{
ChangePage(1);
ChangePage(direction);
UpdatePages();
}));
button.OnMouseOver = new UnityEvent();
button.OnMouseOver.AddListener((UnityAction)(() => rollover.DoMouseOver()));
button.OnMouseOut = new UnityEvent();
button.OnMouseOut.AddListener((UnityAction)(() => rollover.DoMouseOut()));
}
var backButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent<PassiveButton>();
backButton.transform.localPosition = new Vector3(2.9f, 2.6f, -10);
backButton.transform.localScale = new Vector2(0.25f, 0.25f);
var backRend = backButton.GetComponent<SpriteRenderer>();
backRend.sprite = MiraAssets.NextButton.LoadAsset();
backRend.flipX = true;
backButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
private static void ChangePage(int increase)
{
_currentPage += increase;
if (_currentPage > TabButtons.Count)
{
_currentPage = 1;
}
var backRollover = backButton.gameObject.AddComponent<ButtonRolloverHandler>();
backRollover.Target = backRend;
backRollover.OverColor = Palette.AcceptedGreen;
backButton.OnClick = new Button.ButtonClickedEvent();
backButton.OnClick.AddListener(
(UnityAction)(() =>
{
ChangePage(-1);
UpdatePages();
}));
if (_currentPage < 1)
{
_currentPage = TabButtons.Count;
}
}
/// <summary>
@@ -194,7 +198,7 @@ public static class OptionsMenuPatches
public static void OpenPostfix()
{
LocalSettingsManager.AvailableTabs.ForEach(CustomClose);
currentPage = 1;
_currentPage = 1;
UpdatePages();
}
@@ -204,9 +208,7 @@ public static class OptionsMenuPatches
{
pages.Value?
.Where(x => x != null)
.Do(
x => x?.SetActive(
pages.Key == currentPage));
.Do(x => x?.SetActive(pages.Key == _currentPage));
}
}
-193
View File
@@ -1,193 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(HatsTab))]
public static class HatsTabPatches
{
private static readonly Dictionary<int, string> StoreNames = [];
private static SortedList<string, List<HatData>> sortedHats = [];
private static int currentPage;
private static void PreviousPage(HatsTab hatsTab)
{
currentPage--;
currentPage = currentPage < 0 ? sortedHats.Count - 1 : currentPage;
GenerateHats(hatsTab, currentPage);
}
private static void NextPage(HatsTab hatsTab)
{
currentPage++;
currentPage = currentPage > sortedHats.Count - 1 ? 0 : currentPage;
GenerateHats(hatsTab, currentPage);
}
[HarmonyPatch(nameof(HatsTab.OnEnable))]
[HarmonyPrefix]
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
public static bool OnEnablePrefix(HatsTab __instance)
{
if (!AddressablesLoader.AddressableHatsExist)
{
return true;
}
__instance.currentHat = HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat);
var allHats = HatManager.Instance.GetUnlockedHats().ToImmutableList();
if (sortedHats.Count == 0)
{
var num = 0;
var comparer = new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture);
sortedHats = new SortedList<string, List<HatData>>(comparer);
foreach (var hat in allHats)
{
if (!sortedHats.ContainsKey(hat.StoreName)) sortedHats[hat.StoreName] = [];
sortedHats[hat.StoreName].Add(hat);
if (!StoreNames.ContainsValue(hat.StoreName))
{
StoreNames.Add(num, hat.StoreName);
num++;
}
}
}
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(HatsTab), nameof(HatsTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(HatsTab __instance)
{
if (!AddressablesLoader.AddressableHatsExist)
{
return;
}
if (sortedHats.Count == 0) return;
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
NextPage(__instance);
}
}
private static IEnumerator? loadRoutine;
private static int hatIndex;
private static void GenerateHats(HatsTab __instance, int page)
{
if (loadRoutine != null) Coroutines.Stop(loadRoutine);
hatIndex = 0;
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
var group = sortedHats.Where(x => x.Key == StoreNames[page]);
foreach (var (groupName, hats) in group)
{
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName} ({currentPage + 1}/{sortedHats.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
var xLerp = __instance.XRange.Lerp(0.5f);
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
var yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
loadRoutine = Coroutines.Start(CoGenerateChips(__instance, hats));
}
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.currentHatIsEquipped = true;
}
private static IEnumerator CoGenerateChips(HatsTab __instance, List<HatData> hats)
{
__instance.scroller.ScrollToTop();
var batchSize = 5;
for (var i = 0; i < hats.Count; i += batchSize)
{
var batch = hats.Skip(i).Take(batchSize).ToList();
foreach (var hat in batch.OrderBy(HatManager.Instance.allHats.IndexOf))
{
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), hat);
hatIndex += 1;
yield return null;
}
__instance.SetScrollerBounds();
yield return new WaitForSeconds(0.01f);
}
__instance.currentHatIsEquipped = true;
loadRoutine = null;
}
private static void GenerateColorChip(HatsTab __instance, Vector2 position, HatData hat)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = hat.ProductId;
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectHat(hat)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectHat(HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectHat(hat)));
}
colorChip.Inner.SetHat(hat, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.SelectionHighlight.gameObject.SetActive(false);
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, hat);
colorChip.Inner.SetMaskType(PlayerMaterial.MaskType.SimpleUI);
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Inner.transform.localPosition = hat.ChipOffset + new Vector2(0f, -0.3f);
colorChip.Tag = hat;
__instance.ColorChips.Add(colorChip);
}
}
-55
View File
@@ -1,55 +0,0 @@
using System;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
public static class InventoryUtility
{
public static void CreateNextBackButtons<T>(T tab, Action<T> previousPage, Action<T> nextPage)
where T : InventoryTab
{
var title = tab.transform.FindChild("Text");
title.localPosition = new Vector3(.9123f, -.23f, -55);
title.GetComponent<TextMeshPro>().alignment = TextAlignmentOptions.Center;
tab.scroller.Hitbox.transform.localScale -= new Vector3(0, 0.1f, 0);
tab.scroller.Hitbox.transform.localPosition -= new Vector3(0, 0.1f, 0);
var leftButtonElement = Object.Instantiate(PlayerCustomizationMenu.Instance.BackButton.gameObject, tab.transform);
var leftSpriteRenderer = leftButtonElement.GetComponent<SpriteRenderer>();
var leftButton = leftButtonElement.GetComponent<PassiveButton>();
leftButtonElement.transform.localPosition = new Vector3(-1.1f, -0.2f, -55);
leftSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset();
leftSpriteRenderer.flipX = true;
leftButton.OnMouseOver = new UnityEvent();
leftButton.OnMouseOver.AddListener((Action)(() => leftSpriteRenderer.sprite = MiraAssets.NextButtonActive.LoadAsset()));
leftButton.OnMouseOut = new UnityEvent();
leftButton.OnMouseOut.AddListener((Action)(() => leftSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset()));
leftButton.OnClick = new Button.ButtonClickedEvent();
leftButton.OnClick.AddListener((Action)(() => previousPage(tab)));
var rightButtonElement = Object.Instantiate(leftButtonElement, tab.transform);
var rightSpriteRenderer = rightButtonElement.GetComponent<SpriteRenderer>();
var rightButton = rightButtonElement.GetComponent<PassiveButton>();
rightButtonElement.transform.localPosition = new Vector3(2.8f, -0.2f, -55);
rightSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset();
rightSpriteRenderer.flipX = false;
rightButton.OnMouseOver = new UnityEvent();
rightButton.OnMouseOver.AddListener((Action)(() => rightSpriteRenderer.sprite = MiraAssets.NextButtonActive.LoadAsset()));
rightButton.OnMouseOut = new UnityEvent();
rightButton.OnMouseOut.AddListener((Action)(() => rightSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset()));
rightButton.OnClick = new Button.ButtonClickedEvent();
rightButton.OnClick.AddListener((Action)(() => nextPage(tab)));
}
}
@@ -2,7 +2,6 @@
using System.Collections;
using HarmonyLib;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
@@ -23,10 +22,9 @@ public static class MainMenuManagerPatches
[HarmonyPostfix]
public static void AwakePostfix()
{
var requiredVersion = new Version(2026, 6, 5);
var requiredVersion = new Version(2026, 9, 29);
var version = Version.Parse(Application.version);
NeedsDeepDestroy = version >= requiredVersion;
AddressablesLoader.LoadAll();
Coroutines.Start(SetFps());
}
@@ -1,161 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.Data;
using HarmonyLib;
using Innersloth.Assets;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(NameplatesTab))]
public static class NameplatesTabPatches
{
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
private static readonly SortedList<string, List<NamePlateData>> SortedNameplates = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
private static int currentPage;
internal static void AddRange(IEnumerable<(string Key, NamePlateData Visor)> data)
{
foreach (var (key, visor) in data)
{
if (!SortedNameplates.ContainsKey(key)) SortedNameplates.Add(key, []);
SortedNameplates[key].Add(visor);
}
}
private static void PreviousPage(NameplatesTab tab)
{
currentPage--;
currentPage = currentPage < 0 ? SortedNameplates.Count - 1 : currentPage;
GenerateHats(tab, currentPage);
}
private static void NextPage(NameplatesTab tab)
{
currentPage++;
currentPage = currentPage > SortedNameplates.Count - 1 ? 0 : currentPage;
GenerateHats(tab, currentPage);
}
[HarmonyPatch(nameof(NameplatesTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(NameplatesTab __instance)
{
if (!AddressablesLoader.AddressableNameplatesExist)
{
return true;
}
__instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId;
if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(HatManager.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x)));
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(NameplatesTab), nameof(NameplatesTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(NameplatesTab __instance)
{
if (!AddressablesLoader.AddressableNameplatesExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
NextPage(__instance);
}
}
private static void GenerateHats(NameplatesTab __instance, int page)
{
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
var hatIndex = 0;
var (groupName, nameplates) = SortedNameplates.ToArray()[page];
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName} ({currentPage + 1}/{SortedNameplates.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
var xLerp = __instance.XRange.Lerp(0.5f);
var yLerp = __instance.YStart - __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 2;
foreach (var visor in nameplates.OrderBy(HatManager.Instance.allNamePlates.IndexOf))
{
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), visor);
hatIndex += 1;
}
__instance.SetScrollerBounds();
__instance.currentNameplateIsEquipped = true;
}
private static void GenerateColorChip(NameplatesTab __instance, Vector2 position, NamePlateData namePlate)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = namePlate.ProductId;
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
colorChip.Button.OnMouseOut.AddListener(
(Action)(() => __instance.SelectNameplate(
HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.NamePlate))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
}
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = namePlate.ProdId;
__instance.StartCoroutine(__instance.CoLoadAssetAsync<NamePlateViewData>(namePlate.GetAssetReference(), (Action<NamePlateViewData>?)LoadNameplate));
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Tag = namePlate;
__instance.ColorChips.Add(colorChip);
return;
void LoadNameplate(NamePlateViewData viewData)
{
colorChip.transform.GetChild(1).GetComponent<SpriteRenderer>().sprite = viewData.Image;
// (colorChip as NameplateChip).image.sprite = viewData?.Image;
}
}
}
-155
View File
@@ -1,155 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(SkinsTab))]
public static class SkinsTabPatches
{
private static SortedList<string, List<SkinData>> sortedSkins = [];
private static int currentPage;
private static void PreviousPage(SkinsTab tab)
{
currentPage--;
currentPage = currentPage < 0 ? sortedSkins.Count - 1 : currentPage;
GenerateHats(tab, currentPage);
}
private static void NextPage(SkinsTab tab)
{
currentPage++;
currentPage = currentPage > sortedSkins.Count - 1 ? 0 : currentPage;
GenerateHats(tab, currentPage);
}
[HarmonyPatch(nameof(SkinsTab.OnEnable))]
[HarmonyPrefix]
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
public static bool OnEnablePrefix(SkinsTab __instance)
{
if (!AddressablesLoader.AddressableSkinsExist)
{
return true;
}
__instance.skinId = HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin).ProdId;
var allSkins = HatManager.Instance.GetUnlockedSkins().ToImmutableList();
if (sortedSkins.Count == 0)
{
var comparer = new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture);
sortedSkins = new SortedList<string, List<SkinData>>(comparer);
foreach (var skin in allSkins)
{
if (!sortedSkins.ContainsKey(skin.StoreName)) sortedSkins[skin.StoreName] = [];
sortedSkins[skin.StoreName].Add(skin);
}
}
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(SkinsTab), nameof(SkinsTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(SkinsTab __instance)
{
if (!AddressablesLoader.AddressableSkinsExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
NextPage(__instance);
}
}
private static void GenerateHats(SkinsTab __instance, int page)
{
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
var hatIndex = 0;
var (groupName, skins) = sortedSkins.ToArray()[page];
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName} ({currentPage + 1}/{sortedSkins.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
var xLerp = __instance.XRange.Lerp(0.5f);
var yLerp = __instance.YStart - __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
foreach (var skin in skins.OrderBy(HatManager.Instance.allSkins.IndexOf))
{
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), skin);
hatIndex += 1;
}
__instance.SetScrollerBounds();
__instance.currentSkinIsEquipped = true;
}
private static void GenerateColorChip(SkinsTab __instance, Vector2 position, SkinData skin)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = skin.ProductId;
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectSkin(skin)));
colorChip.Button.OnMouseOut.AddListener(
(Action)(() =>
__instance.SelectSkin(HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectSkin(skin)));
}
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = skin.ProductId;
colorChip.SelectionHighlight.gameObject.SetActive(false);
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, skin);
skin.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Inner.transform.localPosition = skin.ChipOffset + new Vector2(0f, -0.3f);
colorChip.Tag = skin;
__instance.ColorChips.Add(colorChip);
}
}
-154
View File
@@ -1,154 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(VisorsTab))]
public static class VisorsTabPatches
{
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
private static readonly SortedList<string, List<VisorData>> SortedVisors = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
private static int currentPage;
internal static void AddRange(IEnumerable<(string Key, VisorData Visor)> data)
{
foreach (var (key, visor) in data)
{
if (!SortedVisors.ContainsKey(key)) SortedVisors.Add(key, []);
SortedVisors[key].Add(visor);
}
}
private static void PreviousPage(VisorsTab tab)
{
currentPage--;
currentPage = currentPage < 0 ? SortedVisors.Count - 1 : currentPage;
GenerateHats(tab, currentPage);
}
private static void NextPage(VisorsTab tab)
{
currentPage++;
currentPage = currentPage > SortedVisors.Count - 1 ? 0 : currentPage;
GenerateHats(tab, currentPage);
}
[HarmonyPatch(nameof(VisorsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(VisorsTab __instance)
{
if (!AddressablesLoader.AddressableVisorsExist)
{
return true;
}
__instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId;
if (!SortedVisors.ContainsKey("Vanilla")) AddRange(HatManager.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x)));
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(VisorsTab), nameof(VisorsTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(VisorsTab __instance)
{
if (!AddressablesLoader.AddressableVisorsExist)
{
return;
}
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
NextPage(__instance);
}
}
private static void GenerateHats(VisorsTab __instance, int page)
{
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
var hatIndex = 0;
var (groupName, visors) = SortedVisors.ToArray()[page];
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName} ({currentPage + 1}/{SortedVisors.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
var xLerp = __instance.XRange.Lerp(0.5f);
var yLerp = __instance.YStart - __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
foreach (var visor in visors.OrderBy(HatManager.Instance.allVisors.IndexOf))
{
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), visor);
hatIndex += 1;
}
__instance.SetScrollerBounds();
__instance.currentVisorIsEquipped = true;
}
private static void GenerateColorChip(VisorsTab __instance, Vector2 position, VisorData visor)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = visor.ProductId;
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectVisor(visor)));
colorChip.Button.OnMouseOut.AddListener(
(Action)(() =>
__instance.SelectVisor(HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectVisor(visor)));
}
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = visor.ProductId;
colorChip.SelectionHighlight.gameObject.SetActive(false);
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, visor);
visor.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Inner.transform.localPosition = visor.ChipOffset + new Vector2(0f, -0.3f);
colorChip.Tag = visor;
__instance.ColorChips.Add(colorChip);
}
}
+116 -81
View File
@@ -1,10 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BepInEx.Unity.IL2CPP.Utils.Collections;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppSystem.Collections.Generic;
using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
@@ -25,12 +25,12 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(GameOptionsMenu))]
internal static class GameOptionsMenuPatch
{
private static readonly List<CategoryHeaderMasked> VanillaHeaders = new();
private static readonly List<OptionBehaviour> VanillaOptions = new();
private static readonly List<OptionBehaviour> MainOptions = new();
private static readonly System.Collections.Generic.Dictionary<AbstractGameMode, System.Collections.Generic.List<AbstractOptionGroup>> GameModeGroups = [];
private static readonly System.Collections.Generic.Dictionary<AbstractGameMode, System.Collections.Generic.List<OptionBehaviour>> GameModeOptions = [];
private static readonly System.Collections.Generic.Dictionary<AbstractGameMode, System.Collections.Generic.List<CategoryHeaderMasked>> GameModeHeaders = [];
private static readonly List<CategoryHeaderMasked> VanillaHeaders = [];
private static readonly List<OptionBehaviour> VanillaOptions = [];
private static readonly List<OptionBehaviour> MainOptions = [];
private static readonly Dictionary<AbstractGameMode, List<AbstractOptionGroup>> GameModeGroups = [];
private static readonly Dictionary<AbstractGameMode, List<OptionBehaviour>> GameModeOptions = [];
private static readonly Dictionary<AbstractGameMode, List<CategoryHeaderMasked>> GameModeHeaders = [];
private static TextMeshPro _gamemodeDescription = null!;
private static SpriteRenderer _modIcon = null!;
@@ -38,7 +38,8 @@ internal static class GameOptionsMenuPatch
[HarmonyPatch(nameof(GameOptionsMenu.Initialize))]
public static bool InitPatch(GameOptionsMenu __instance)
{
__instance.Children ??= new List<OptionBehaviour>();
Helpers.CacheAdvancedSettingsRowPrefab(__instance.numberOptionOrigin);
__instance.Children ??= new();
__instance.Children.Clear();
if (MenuState.Instance.CurrentModIdx == 0)
@@ -187,6 +188,66 @@ internal static class GameOptionsMenuPatch
instance.scrollBar.SetYBoundsMax(-num - 1.65f);
}
internal static void RefreshForGameMode()
{
if (CustomGameModeManager.ActiveMode == null ||
!GameSettingMenu.Instance ||
!GameSettingMenu.Instance.GameSettingsTab ||
!MenuState.Instance ||
MenuState.Instance.CurrentModIdx != 0 ||
GameModeGroups.Count == 0)
{
return;
}
ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, GameSettingMenu.Instance.GameSettingsTab);
GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(
CustomGameModeManager.ActiveMode.ShowNormalRoleSettings);
}
internal static List<KeyValuePair<string, string>> GetActiveGameSettingsRows(Func<bool, string> formatBool)
{
var menu = GameSettingMenu.Instance ? GameSettingMenu.Instance.GameSettingsTab : null;
if (!menu || menu!.Children == null)
{
return [];
}
var rows = new List<KeyValuePair<string, string>>();
foreach (var option in menu.Children)
{
if (!option || !option.gameObject.activeInHierarchy ||
option == menu.MapPicker ||
(GameModeOption.OptionBehaviour && option == GameModeOption.OptionBehaviour) ||
!option.Data)
{
continue;
}
var title = option switch
{
ToggleOption toggle => toggle.TitleText.text,
NumberOption number => number.TitleText.text,
StringOption str => str.TitleText.text,
PlayerOption player => player.TitleText.text,
_ => TranslationController.Instance.GetString(option.Data.Title),
};
var value = option switch
{
ToggleOption toggle => formatBool(toggle.GetBool()),
NumberOption number => number.ValueText.text,
StringOption str => str.ValueText.text,
PlayerOption player => player.ValueText.text,
_ => option.Data.GetValueString(option.GetFloat()),
};
rows.Add(new KeyValuePair<string, string>(title, value));
}
return rows;
}
private static void CreateSettings(GameOptionsMenu instance, Transform container)
{
var num = 0.713f;
@@ -324,8 +385,8 @@ internal static class GameOptionsMenuPatch
.ToList();
GameModeGroups.Add(mode, [.. filteredGroups]);
var optionBehaviours = new System.Collections.Generic.List<OptionBehaviour>();
var categoryHeaders = new System.Collections.Generic.List<CategoryHeaderMasked>();
var optionBehaviours = new List<OptionBehaviour>();
var categoryHeaders = new List<CategoryHeaderMasked>();
var newNum = mode.ShowNormalGameSettings ? num : num - additionalVanillaScrollNum;
foreach (var group in filteredGroups)
{
@@ -406,7 +467,7 @@ internal static class GameOptionsMenuPatch
__instance.scrollBar.SetYBoundsMax(-num - 1.65f);
}
private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref System.Collections.Generic.List<OptionBehaviour> optionBehaviours, ref System.Collections.Generic.List<CategoryHeaderMasked> categoryHeaders)
private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref List<OptionBehaviour> optionBehaviours, ref List<CategoryHeaderMasked> categoryHeaders)
{
var categoryHeaderMasked = Object.Instantiate(
menu.categoryHeaderOrigin,
@@ -618,6 +679,8 @@ internal static class GameOptionsMenuPatch
{
menu.MapPicker.gameObject.SetActive(false);
var mod = MenuState.Instance.CurrentMod;
var container = MenuState.Instance.CurrentContainer.transform;
PresetManager.DefaultPresets.TryGetValue(mod, out var defaultPreset);
var filteredGroups = MenuState.Instance.CurrentMenu switch
{
@@ -643,15 +706,13 @@ internal static class GameOptionsMenuPatch
{
foreach (var group in filteredGroups)
{
yield return CoCreateGroup(menu, group);
yield return CoCreateGroup(menu, group, container, defaultPreset);
}
}
}
private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group)
private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, OptionPreset? defaultPreset)
{
var container = MenuState.Instance.CurrentContainer.transform;
group.Ready = false;
if (group.Header == null || !group.Header)
{
@@ -713,12 +774,6 @@ internal static class GameOptionsMenuPatch
menu.playerOptionOrigin,
container));
OptionPreset? defaultPreset = null;
if (PresetManager.DefaultPresets.TryGetValue(MenuState.Instance.CurrentMod, out var preset))
{
defaultPreset = preset;
}
yield return options.CoLoopWithBudget(newOpt =>
{
newOpt.SetClickMask(menu.ButtonClickMask);
@@ -765,73 +820,53 @@ internal static class GameOptionsMenuPatch
}
menu.Children.Add(newOpt);
var resetBtn = new GameObject("ResetOption")
if (defaultPreset != null && defaultPreset.IsOptionInPreset(newOpt) &&
newOpt is ToggleOption or NumberOption or StringOption)
{
transform =
var resetBtn = new GameObject("ResetOption")
{
parent = newOpt.transform,
localScale = new(.5f, .5f, 1),
localPosition = new Vector3(-3.1f, 0f, -2f),
},
layer = LayerMask.NameToLayer("UI"),
};
var resetRend = resetBtn.AddComponent<SpriteRenderer>();
resetRend.sprite = MiraAssets.ResetButton.LoadAsset();
resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
var resetBoxCol = resetBtn.gameObject.AddComponent<BoxCollider2D>();
resetBoxCol.size = new Vector2(1f, 1f);
resetBoxCol.offset = new Vector2(0, 0);
var passiveButton = resetBtn.AddComponent<PassiveButton>();
passiveButton.OnClick = new Button.ButtonClickedEvent();
passiveButton.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
passiveButton.OnMouseOver = new UnityEvent();
passiveButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
}));
passiveButton.OnMouseOut = new UnityEvent();
passiveButton.OnMouseOut.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
}));
if (newOpt is ToggleOption toggleOpt)
{
transform =
{
parent = newOpt.transform,
localScale = new(.5f, .5f, 1),
localPosition = new Vector3(-3.1f, 0f, -2f),
},
layer = LayerMask.NameToLayer("UI"),
};
var resetRend = resetBtn.AddComponent<SpriteRenderer>();
resetRend.sprite = MiraAssets.ResetButton.LoadAsset();
resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
var resetBoxCol = resetBtn.gameObject.AddComponent<BoxCollider2D>();
resetBoxCol.size = new Vector2(1f, 1f);
resetBoxCol.offset = new Vector2(0, 0);
var passiveButton = resetBtn.AddComponent<PassiveButton>();
passiveButton.OnClick = new Button.ButtonClickedEvent();
passiveButton.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
passiveButton.OnMouseOver = new UnityEvent();
passiveButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
}));
passiveButton.OnMouseOut = new UnityEvent();
passiveButton.OnMouseOut.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
}));
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(toggleOpt);
defaultPreset.ResetOption(newOpt);
}));
}
else if (newOpt is NumberOption numOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(numOpt);
}));
}
else if (newOpt is StringOption strOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(strOpt);
}));
}
if (!defaultPreset!.IsOptionInPreset(newOpt))
{
resetBtn.Destroy();
}
newOpt.Initialize();
newOpt.gameObject.SetActive(false);
@@ -1,4 +1,5 @@
using HarmonyLib;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Options;
@@ -13,6 +14,11 @@ internal static class GameSettingMenuPatches
[HarmonyPatch(nameof(GameSettingMenu.OnEnable))]
public static void OnEnablePrefix(GameSettingMenu __instance)
{
if (__instance.GameSettingsTab && __instance.GameSettingsTab.numberOptionOrigin)
{
Helpers.CacheAdvancedSettingsRowPrefab(__instance.GameSettingsTab.numberOptionOrigin);
}
// Add Menu State object
if (!__instance.gameObject.GetComponent<MenuState>())
{
+23 -5
View File
@@ -1,11 +1,9 @@
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.GameOptions;
using MiraAPI.Roles;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using UnityEngine;
using Object = Il2CppSystem.Object;
namespace MiraAPI.Patches.Options;
@@ -34,6 +32,11 @@ public static class OptionsPatches
__instance.roleChance = 0;
}
if (__instance.roleChance == 0)
{
__instance.roleMaxCount = 0;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
@@ -60,6 +63,11 @@ public static class OptionsPatches
__instance.roleChance = 100;
}
if (__instance.roleChance == 0)
{
__instance.roleMaxCount = 0;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
@@ -84,6 +92,11 @@ public static class OptionsPatches
__instance.roleMaxCount = 0;
}
if (__instance.roleMaxCount == 0)
{
__instance.roleChance = 0;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
@@ -108,6 +121,11 @@ public static class OptionsPatches
__instance.roleMaxCount = __instance.role.MaxCount;
}
if (__instance.roleMaxCount == 0)
{
__instance.roleChance = 0;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
@@ -165,7 +183,7 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>()).Translate();
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate();
return false;
}
@@ -314,8 +332,8 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>()).Translate();
__instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value], Array.Empty<Object>());
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate();
__instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value]);
return false;
}
@@ -26,8 +26,8 @@ public static class PlayerOptionPatches
{
filteredPlayers.Sort((a, b) => a.PlayerId.CompareTo(b.PlayerId));
moddedPlrOpt.Values = filteredPlayers;
__instance.Values = filteredPlayers.ToIl2CppList();
moddedPlrOpt.Values = __instance.Values;
__instance.playerIndex = filteredPlayers.FindIndex(p => p.PlayerId == __instance.Value);
__instance.SetValueText();
}
+13 -15
View File
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Globalization;
using System.Linq;
using HarmonyLib;
using MiraAPI.GameOptions;
@@ -24,7 +23,6 @@ public static class SettingPatches
/// <returns><see langword="false"/> to skip original method.</returns>
[HarmonyPrefix]
[HarmonyPatch(typeof(FloatGameSetting), nameof(FloatGameSetting.GetValueString))]
[SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Warning cascades into forcing the entire tree to be ternary operators.")]
public static bool ValueStringPatch(
FloatGameSetting __instance,
ref string __result,
@@ -48,14 +46,14 @@ public static class SettingPatches
}
else
{
var part = value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo);
result = suffix switch
{
MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo),
MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x",
MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%",
_ => TranslationController.Instance.GetString(
StringNames.GameSecondsAbbrev,
(Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
MiraNumberSuffixes.None => part,
MiraNumberSuffixes.Multiplier => part + "x",
MiraNumberSuffixes.Percent => part + "%",
MiraNumberSuffixes.Distance => string.Concat(part, " ", MiraApiPlugin.DistanceSuffix.Value),
_ => TranslationController.Instance.GetString(StringNames.GameSecondsAbbrev, part),
};
}
}
@@ -65,14 +63,14 @@ public static class SettingPatches
}
else
{
var part = value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo);
result = suffix switch
{
MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo),
MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x",
MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%",
_ => TranslationController.Instance.GetString(
StringNames.GameSecondsAbbrev,
(Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
MiraNumberSuffixes.None => part,
MiraNumberSuffixes.Multiplier => part + "x",
MiraNumberSuffixes.Percent => part + "%",
MiraNumberSuffixes.Distance => string.Concat(part, " ", MiraApiPlugin.DistanceSuffix.Value),
_ => TranslationController.Instance.GetString(StringNames.GameSecondsAbbrev, part),
};
}
File diff suppressed because it is too large. Load diff
+2 -3
View File
@@ -1,5 +1,4 @@
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.Networking;
using MiraAPI.Roles;
using UnityEngine;
@@ -31,13 +30,13 @@ public static class KillButtonPatches
if (__instance.currentTarget && __instance.currentTarget != target)
{
__instance.currentTarget.cosmetics.SetOutline(false, new Nullable<Color>(Color.clear));
__instance.currentTarget.cosmetics.SetOutline(false, new Il2CppSystem.Nullable<Color>(Color.clear));
}
__instance.currentTarget = target;
if (__instance.currentTarget)
{
__instance.currentTarget.cosmetics.SetOutline(true, new Nullable<Color>(customRole.Configuration.KillButtonOutlineColor));
__instance.currentTarget.cosmetics.SetOutline(true, new Il2CppSystem.Nullable<Color>(customRole.Configuration.KillButtonOutlineColor));
__instance.SetEnabled();
return false;
}
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppSystem.Collections.Generic;
using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
@@ -14,7 +14,7 @@ public static class LogicRoleSelectionHnsPatch
[HarmonyPatch(nameof(LogicRoleSelectionHnS.AssignRolesForTeam))]
public static bool AssignRolesForTeam(
LogicRoleSelectionHnS __instance,
List<NetworkedPlayerInfo> players,
Il2CppSystem.Collections.Generic.List<NetworkedPlayerInfo> players,
IGameOptions opts,
RoleTeamTypes team,
int teamMax,
@@ -58,7 +58,7 @@ public static class LogicRoleSelectionHnsPatch
case RoleTeamTypes.Impostor:
Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment");
var newImpostors = new List<NetworkedPlayerInfo>();
var newImpostors = new Il2CppSystem.Collections.Generic.List<NetworkedPlayerInfo>();
// Specified Seeker
if (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() &&
@@ -113,7 +113,7 @@ public static class LogicRoleSelectionHnsPatch
return false;
}
public static void AssignRolesFromList(List<NetworkedPlayerInfo> players, int teamMax, List<RoleTypes> roleList, ref int rolesAssigned)
public static void AssignRolesFromList(Il2CppSystem.Collections.Generic.List<NetworkedPlayerInfo> players, int teamMax, List<RoleTypes> roleList, ref int rolesAssigned)
{
while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax)
{
@@ -128,7 +128,7 @@ public static class LogicRoleSelectionHnsPatch
}
private static void AddGuaranteedRoles(
System.Collections.Generic.IEnumerable<RoleBehaviour> guaranteedRoles,
IEnumerable<RoleBehaviour> guaranteedRoles,
IGameOptions opts,
List<RoleTypes> list)
{
@@ -146,7 +146,7 @@ public static class LogicRoleSelectionHnsPatch
}
}
private static void AddPotentialRoles(System.Collections.Generic.IEnumerable<RoleBehaviour> source, IGameOptions opts, List<RoleTypes> list)
private static void AddPotentialRoles(IEnumerable<RoleBehaviour> source, IGameOptions opts, List<RoleTypes> list)
{
var roleOptions = opts.RoleOptions;
var potentialRoles = source.Where(x => !x.IsDead).Select(role => new RoleManager.RoleAssignmentData(
+4 -5
View File
@@ -2,7 +2,6 @@
using System.Runtime.CompilerServices;
using HarmonyLib;
using Hazel;
using Il2CppSystem.Text;
namespace MiraAPI.Patches.Stubs;
@@ -40,14 +39,14 @@ public static class RoleBehaviourStubs
}
/// <summary>
/// Stub method for <see cref="RoleBehaviour.AppendTaskHint(StringBuilder)"/>.
/// Stub method for <see cref="RoleBehaviour.AppendTaskHint(Il2CppSystem.Text.StringBuilder)"/>.
/// </summary>
/// <param name="instance">The <see cref="RoleBehaviour"/> object.</param>
/// <param name="taskStringBuilder">The <see cref="StringBuilder"/> to append the task hint to.</param>
/// <param name="taskStringBuilder">The <see cref="Il2CppSystem.Text.StringBuilder"/> to append the task hint to.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(StringBuilder))]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(Il2CppSystem.Text.StringBuilder))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void AppendTaskHint(RoleBehaviour instance, StringBuilder taskStringBuilder)
public static void AppendTaskHint(RoleBehaviour instance, Il2CppSystem.Text.StringBuilder taskStringBuilder)
{
// nothing needed
}
+14 -7
View File
@@ -313,9 +313,12 @@ public sealed class MiraPluginManager
foreach (var property in type.GetProperties())
{
if (property.GetCustomAttribute<MiraIgnoreAttribute>() != null)
continue;
if (property.GetMethod?.IsStatic == true)
{
Error($"Option property {property.Name} in {type.Name} must not be static.");
Error($"Non-ignored option property {property.Name} in {type.Name} must not be static.");
continue;
}
@@ -325,6 +328,12 @@ public sealed class MiraPluginManager
continue;
}
if (property.PropertyType.IsAssignableTo(typeof(IModdedOptionList)))
{
ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo);
continue;
}
var attribute = property.GetCustomAttribute<ModdedOptionAttribute>();
if (attribute != null)
{
@@ -332,11 +341,6 @@ public sealed class MiraPluginManager
continue;
}
if (property.PropertyType.IsAssignableTo(typeof(IModdedOptionList)))
{
ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo);
}
var listAttr = property.GetCustomAttribute<ModdedOptionListAttribute>();
if (listAttr != null)
{
@@ -404,7 +408,7 @@ public sealed class MiraPluginManager
foreach (var property in type.GetProperties())
{
if (property.PropertyType != typeof(CustomColor))
if (!property.PropertyType.IsAssignableTo(typeof(CustomColor)))
{
continue;
}
@@ -416,6 +420,9 @@ public sealed class MiraPluginManager
}
PaletteManager.CustomColors.Add(color);
if (color is VariableColor)
PaletteManager.VariableColorNames.Add(color.Name);
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(CustomColor))))
+8 -6
View File
@@ -10,9 +10,6 @@ namespace MiraAPI.Presets;
/// <summary>
/// Represents a preset of game options that can be applied to the game.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="OptionPreset"/> class with the specified name and configuration file.
/// </remarks>
/// <param name="name">The name of the preset.</param>
/// <param name="plugin">The plugin associated with the preset.</param>
/// <param name="presetConfig">The configuration file for the preset.</param>
@@ -70,7 +67,12 @@ public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetC
/// <param name="baseOption">The option to reset.</param>
public void ResetOption(OptionBehaviour baseOption)
{
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption);
if (selectedOpt == null)
{
return;
}
selectedOpt.LoadFromPreset(PresetConfig);
/*ModdedOptionsManager.SyncAllOptions();
@@ -84,7 +86,7 @@ public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetC
/// <returns>The value of whether the option is in the preset.</returns>
public bool IsOptionInPreset(OptionBehaviour baseOption)
{
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
return selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition);
var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption);
return selectedOpt != null && selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition);
}
}
+4
View File
@@ -9,10 +9,14 @@ using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
// TODO: for the love of fucking god please add a namespace on the asset bundle AND here - Atony.
/*
namespace MiraAPI.Presets;
*/
[RegisterInIl2Cpp]
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")]
[SuppressMessage("Design", "CA1050:Declare types in namespaces", Justification = "Unity script.")]
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Unity fields; ignore.)
public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr)
{
+37 -1
View File
@@ -2,21 +2,29 @@
<resources>
<string name="MiraApi">Mira API</string>
<string name="MiraApi.VisualsUi">Visuals/UI</string>
<string name="MiraApi.MatchInfoGuide">Match Info Guide</string>
<string name="MiraApi.Accessibility">Accessibility</string>
<string name="MiraApi.MiraApiBrand">MiraAPI</string>
<string name="MiraApi.ShowModifiersHudOnLeftSide">Show Modifiers HUD on Left Side</string>
<string name="MiraApi.ShowKeybindsOnButtons">Show Keybinds on Buttons</string>
<string name="MiraApi.ButtonScaleFactor">Button Scale Factor</string>
<string name="MiraApi.TopRightButtonScaleFactor">Top-Right UI Scale Factor</string>
<string name="MiraApi.MatchInfoGuideScaleFactor">Match Info Guide Scale Factor</string>
<string name="MiraApi.WikiTabPosition">Tab Buttons Grid Placement</string>
<string name="MiraApi.WikiTabPosition.LeftSide">Left-Side</string>
<string name="MiraApi.WikiTabPosition.RightSide">Right-Side</string>
<string name="MiraApi.WikiTabPosition.Below">Below UI</string>
<string name="MiraApi.MaxFps">Max FPS</string>
<string name="MiraApi.WikiOnBottomRow">Display Wiki on Bottom Row</string>
<string name="MiraApi.WikiOnBottomRow">Display Button on Bottom Row</string>
<string name="MiraApi.EnableSabotageFlashes">Enable Sabotage Flashes</string>
<string name="MiraApi.EnableSabotageBlare">Enable Sabotage Blare</string>
<string name="MiraApi.EnableVariableColors">Enable Variable Color Changes</string>
<string name="MiraApi.ReturnToRoleSettings">RETURN TO ROLE SETTINGS</string>
<string name="MiraApi.CustomCategory1">Custom Category 1</string>
<string name="MiraApi.CustomCategory2">Custom Category 2</string>
<string name="MiraApi.ModifierMenuDescription">Configure modifiers and their settings here!</string>
<string name="MiraApi.CustomOptionMenuDescription">Apply game settings for this mod!</string>
<string name="MiraApi.Gamemode">Gamemode</string>
<string name="MiraApi.Gamemode.Classic">Classic</string>
<string name="MiraApi.Gamemode.Classic.Description">The classic Among Us experience!
Find the Impostors or deceive the Crew.</string>
@@ -43,4 +51,32 @@ Outlive the timer or eliminate the crew.</string>
<string name="MiraApi.ModifierDisplay.Page">Page</string>
<string name="MiraApi.Page">Page</string>
<string name="MiraApi.Main">Main</string>
<string name="MiraApi.AbilityType.Basic">Ability</string>
<string name="MiraApi.AbilityType.Indirect">Indirect Ability</string>
<string name="MiraApi.AbilityType.Interaction">Interaction Ability</string>
<string name="MiraApi.AbilityType.Radius">Radius Ability</string>
<string name="MiraApi.AbilityType.Menu">Menu Ability</string>
<string name="MiraApi.AbilityType.Meeting">Meeting Ability</string>
<string name="MiraApi.AbilityType.SelectiveMeeting">Selective Meeting Ability</string>
<string name="MiraApi.AbilityType.MeetingMenu">Meeting Menu Ability</string>
<string name="Options">Options</string>
<string name="MiraApi.Wiki.PlayersTab">Player List</string>
<string name="MiraApi.Wiki.SettingsTab">Game Settings</string>
<string name="MiraApi.Wiki.SearchPlaceholder">Search...</string>
<string name="MiraApi.Wiki.FilterLabel">Filter</string>
<string name="MiraApi.Wiki.GroupingLabel">Grouping</string>
<string name="MiraApi.Wiki.OrderLabel">Order</string>
<string name="MiraApi.Wiki.VanillaSettings">Vanilla</string>
<string name="MiraApi.Wiki.SearchResults">Search Results</string>
<string name="MiraApi.Wiki.PreviousPage">Previous</string>
<string name="MiraApi.Wiki.NextPage">Next</string>
<string name="MiraApi.Wiki.NoResultsFound">No Results Found</string>
<string name="On">On</string>
<string name="Off">Off</string>
<string name="Enabled">Enabled</string>
<string name="Disabled">Disabled</string>
<string name="Active">Active</string>
<string name="Inactive">Inactive</string>
<string name="Activated">Activated</string>
<string name="Deactivated">Deactivated</string>
</resources>
Loaded 100 of 163 files, more files were not shown because too many files have changed in this diff. Show more