Compare commits

..
375 Commits
Author SHA1 Message Date
Atony 056a9b6102 Merge pull request #72 from All-Of-Us-Mods/dev
Fix Logger error for v0.3.3
2025-10-14 13:53:21 -05:00
Atony 4b144a782d Fix Logger error 2025-10-14 13:53:02 -05:00
Atony ae917a9c77 Merge pull request #71 from All-Of-Us-Mods/dev
MiraAPI v0.3.3
2025-10-14 13:46:58 -05:00
Atony 7a8d608b17 Bump Version (again) 2025-10-14 13:46:08 -05:00
XtraCube 6503bbecb7 use reactor static logging pattern 2025-10-14 14:37:20 -04:00
XtraCube cb794c3fc2 make kill anim patch update resilient 2025-10-14 14:35:20 -04:00
XtraCube 88329bef79 update freeplay patch to be version resistant 2025-10-14 14:14:25 -04:00
XtraCube eb7abecd75 update to latest among us 2025-10-14 14:06:43 -04:00
Atony fc9e9740be Merge pull request #70 from All-Of-Us-Mods/dev
MiraAPI v0.3.2
2025-10-14 09:08:53 -05:00
Atony 3c51c88963 Bump version 2025-10-14 09:05:25 -05:00
Atony e42f5e6029 Log exception 2025-10-12 19:44:30 -05:00
Atony 61be7dda82 Maybe fix the issues with kill animations? 2025-10-12 18:33:45 -05:00
xChipseq 3220a4a820 change texture wrap mode to clamp 2025-10-11 00:46:35 +02:00
XtraCube 8fd142a92e fix custom murder dead body 2025-10-04 14:07:00 -04:00
XtraCube fcec6ad6e5 fix new role fields 2025-10-01 22:17:40 -04:00
XtraCube 10a7ffe886 add new role information to try and fix broken gamemanager 2025-10-01 20:59:44 -04:00
Atony 7e541733b7 More fixes for role names 2025-09-30 18:15:29 -05:00
Atony 99a275a97c Adjust edit pane for self-adjusting role names 2025-09-30 18:08:56 -05:00
XtraCube 67a5f0dd67 fixing inner scuff problems 2025-09-30 11:22:43 -04:00
XtraCube f13d46ec18 android support 2025-09-30 11:13:23 -04:00
Atony f9096b6160 Merge pull request #69 from All-Of-Us-Mods/dev
MiraAPI v0.3.1
2025-09-28 13:17:06 -05:00
Atony 8bf101534f Bump version 2025-09-28 13:13:21 -05:00
Atony 479323acdd Merge pull request #68 from yanpla/dev
Fix DetectiveNotesMinigame
2025-09-28 13:06:21 -05:00
Atony 50c5899f4d Merge branch 'dev' into dev 2025-09-28 13:06:12 -05:00
XtraCube fd719ae42f Merge remote-tracking branch 'origin/dev' into dev 2025-09-26 12:18:18 -04:00
XtraCube e050b043cb use reflection for intro cutscene patches 2025-09-26 12:18:13 -04:00
XtraCube e78c456636 match android 2025-09-25 13:47:54 -04:00
XtraCube 7e8d07672c fix mira asset 2025-09-25 12:27:26 -04:00
XtraCube 77ae01c66a improve player customization tabs 2025-09-25 12:25:12 -04:00
XtraCube ce824f05fa fix selection problem on android 2025-09-25 11:36:34 -04:00
yanpla b5fe3c7afd Remove custom roles from DetectiveNotesMinigame 2025-09-24 19:53:24 +02:00
XtraCube 1750cd3db9 Merge pull request #67 from All-Of-Us-Mods/dev
0.3.0
2025-09-24 08:54:52 -04:00
XtraCube 4dd2554b38 bump version 2025-09-24 08:53:09 -04:00
XtraCube e5cfd8a2fc fix game never ending 2025-09-24 08:24:14 -04:00
XtraCube 6d387cae96 fix round start events for airship 2025-09-24 07:35:50 -04:00
XtraCube 59f3058685 update comment 2025-09-24 07:21:31 -04:00
XtraCube 9dfb40d7bf add new role stubs 2025-09-24 07:12:34 -04:00
XtraCube ffca13e6e6 Merge remote-tracking branch 'origin/dev' into dev 2025-09-21 16:57:06 -04:00
XtraCube c1d2868e77 fix server dropdown 2025-09-21 16:57:00 -04:00
Atony 18c52e0138 Expose certain local option overrides and tabs 2025-09-20 22:17:51 -05:00
XtraCube b64bf7c9de bump version 2025-09-20 22:14:07 -04:00
XtraCube e98c16b9f5 fix role settings tab 2025-09-20 22:12:51 -04:00
xChipseq e166efa666 ignore custom roles in vanilla role options 2025-09-20 23:47:36 +02:00
xChipseq 8da80be027 add HideIconOnHover for local settings 2025-09-20 21:58:30 +02:00
XtraCube af75cc3606 revert un-inline 2025-09-20 11:16:56 -04:00
XtraCube 0f1962394a un-inline rpcendgame 2025-09-19 21:07:22 -04:00
XtraCube 36991f34ee fix index of task adder patch 2025-09-19 20:52:01 -04:00
Atony ae5f3783b4 Add locale to task adder 2025-09-18 05:07:05 -05:00
Atony 12a1908c3e Merge branch 'dev' of https://github.com/All-Of-Us-Mods/MiraAPI into dev 2025-09-18 04:47:11 -05:00
xChipseq 8246e05da6 Merge remote-tracking branch 'origin/dev' into dev 2025-09-17 21:04:24 +02:00
xChipseq c75e039141 fixes 2025-09-17 21:04:05 +02:00
Atony ac66e0d62b Clear up warnings 2025-09-17 12:23:40 -05:00
xChipseq c4cbf0b18d use the old system instead of role children 2025-09-17 19:19:47 +02:00
Pietro420 f3175990d3 ACTUALLY fix Viper 2025-09-14 20:07:17 -03:00
XtraCube 790d868759 fix viper 2025-09-14 15:26:58 -04:00
XtraCube 9f11874189 fix role settings 2025-09-14 15:15:06 -04:00
xChipseq 1b6a2c4eb3 part 1 of trying to fix the task adder 2025-09-12 23:49:24 +02:00
xChipseq 81887c34d9 fix misaligned settings (again) 2025-09-12 23:23:10 +02:00
xChipseq 810fd54e10 i love useless using thingies 2025-09-12 22:33:05 +02:00
xChipseq 5492c2b3cc fix how to play role tutorial 2025-09-12 22:02:21 +02:00
XtraCube f7a515882a Merge pull request #65 from All-Of-Us-Mods/2025.9.9
2025.9.9 Support
2025-09-11 21:00:00 -04:00
Atony 89eb774501 Merge pull request #66 from WanderingPix/fix-roleicons
Fix role icons now appearing if there are no settings for the role.
2025-09-11 05:26:17 -05:00
WanderingPixel 5fd57633ff Update RoleSettingMenuPatches.cs 2025-09-11 11:01:35 +01:00
xChipseq 834d1d878e fix 2025-09-10 22:45:48 +02:00
xChipseq 790b37e8d3 cool role file icons 2025-09-10 22:40:09 +02:00
xChipseq dfa8b29710 update task adder (and improvements) 2025-09-10 22:07:47 +02:00
Atony da7aab9aa2 Merge pull request #64 from All-Of-Us-Mods/vanilla-keybind-bindings
Vanilla keybindings
2025-09-09 18:44:18 -05:00
xChipseq 2c0042e6a8 bump to 2025.9.9 2025-09-09 22:58:18 +02:00
xChipseq 70f63f8b16 custom role fixes 2025-09-09 22:58:02 +02:00
xChipseq 03a3733966 fix body prefab 2025-09-09 22:57:19 +02:00
Atony 3d274c2f61 KEYBINDS WORK NOW 2025-09-09 05:05:56 -05:00
Atony b94e20c6f7 Add Ability Button bind 2025-09-08 20:29:33 -05:00
Chipseq 5a0428c7c0 Merge branch 'dev' into vanilla-keybind-bindings 2025-09-08 21:08:53 +02:00
xChipseq c41dd27f88 init vanilla keybinds after input manager init 2025-09-08 19:38:07 +02:00
xChipseq 6872ca6866 basekeybind class, vanilla keybindings 2025-09-08 18:37:09 +02:00
xChipseq 4b7fa906b9 fix misaligned options after labels 2025-09-07 22:49:23 +02:00
xChipseq b7173d18ab change mira config 2025-09-07 22:45:06 +02:00
xChipseq ffc7a1a4fd fix keybind icon clipping 2025-09-07 22:43:08 +02:00
XtraCube a1611f5f7a fix merge errors 2025-09-07 16:34:57 -04:00
XtraCube 1664aa1003 Merge branch 'fork/xChipseq/local-settings-rework' into dev
# Conflicts:
#	MiraAPI/MiraApiConfig.cs
#	MiraAPI/PluginLoading/MiraPluginManager.cs
#	MiraAPI/Utilities/Helpers.cs
2025-09-07 16:29:33 -04:00
xChipseq af3d99eafe wip 2025-09-07 21:25:15 +02:00
xChipseq 94197e38dd cleanup 2025-09-07 20:47:59 +02:00
angxl wtf 97a0b2084a Merge pull request #61 from WanderingPix/timed-modifiers-taskadder-icon
Timed modifier QOL changes in Task Adder
2025-09-07 13:50:48 -04:00
Atony 1d87790212 Merge pull request #62 from All-Of-Us-Mods/vote-event-tweaks
Add Before and After Vote Events
2025-09-06 22:54:04 -05:00
xChipseq 9d130a53de fix typo 2025-09-07 00:54:44 +02:00
Atony d8afc6887d Merge branch 'dev' into vote-event-tweaks 2025-09-06 14:36:22 -05:00
Atony c0a406610d Merge pull request #60 from WanderingPix/Roleicons-in-options-menu
Role icons in options menu
2025-09-06 14:32:14 -05:00
Atony d73c21936c Add Before and After vote events
Useful for local checks before a vote is placed (like with TOU Mira Prosecutor)
2025-09-06 14:19:17 -05:00
xChipseq 449192f0ef remove this annoying log 2025-09-06 19:02:40 +02:00
xChipseq f3614b9a5c fix null errors 2025-09-05 23:33:01 +02:00
angxl wtf a62c6d496c Merge pull request #57 from xChipseq/button-improvements
Custom Button Improvements + Major Keybind API rewrite
2025-09-04 19:02:07 -04:00
xChipseq bebd648691 remove null check, it's always none when null 2025-09-04 21:51:45 +02:00
xChipseq ecadfa86dd remove invoke log 2025-09-04 21:51:26 +02:00
WanderingPixel 5ad53f695f Update TaskAdderPatches.cs 2025-09-04 14:29:39 +01:00
WanderingPixel 31a9c0c98d Update TaskAdderPatches.cs 2025-09-04 13:23:31 +01:00
WanderingPixel ea389b986f Update TaskAdderPatches.cs 2025-09-04 12:58:09 +01:00
WanderingPixel ef85fab6a6 Update TaskAdderPatches.cs 2025-09-04 12:35:40 +01:00
WanderingPixel b2fdaaf89f Timed Modifier Enhancements in TaskAdderGame 2025-09-04 12:29:19 +01:00
WanderingPixel e6043b6f5f Fix Masking issue 2025-09-03 21:27:38 +01:00
WanderingPixel ffe0db0e9e Add role icons in role settings menu 2025-09-03 21:14:07 +01:00
xChipseq 705ef64b4d fix helper method returning null if no keybind 2025-09-02 22:27:04 +02:00
xChipseq ebdb4a09ca make number options accept floats 2025-09-02 19:24:34 +02:00
xChipseq ac113b6a7f add tab open method and fixes 2025-09-02 18:55:27 +02:00
xChipseq 1f6962efe9 add local setting buttons 2025-09-01 23:34:17 +02:00
xChipseq 6b37b9c09c add an error 2025-09-01 22:28:30 +02:00
xChipseq f515516937 remove the two million test tabs 2025-09-01 22:24:58 +02:00
xChipseq 8b2d24ccc1 more stuff 2025-09-01 22:22:32 +02:00
xChipseq 57bab258e5 enum and int options and lots of other stuff i forgot 2025-09-01 21:54:13 +02:00
ang-xd 04d5e72caa Add slider float value 2025-08-31 18:45:22 -04:00
ang-xd b4e6e448f2 Major work/rewrite on local settings 2025-08-31 18:04:52 -04:00
xChipseq b19a0178ad add a limit to multitarget buttons 2025-08-31 23:41:35 +02:00
xChipseq 8a71f13874 fix for keybind loading 2025-08-31 20:26:22 +02:00
xChipseq 809e7a07ab literally one dot 2025-08-31 20:21:50 +02:00
xChipseq 47394c6afe fix example mod 2025-08-31 18:06:53 +02:00
xChipseq a2a1e91439 replace EffectCancelable with a method 2025-08-31 18:03:17 +02:00
xChipseq 2f5ca32475 multitarget buttons 2025-08-31 17:59:38 +02:00
xChipseq 5fb59408fa wip 2025-08-31 17:51:02 +02:00
xChipseq 57c842ece6 Merge remote-tracking branch 'origin/local-settings-rework' into local-settings-rework 2025-08-30 23:08:55 +02:00
Chipseq 63e552245d Sync local settings branch with dev 2025-08-30 23:05:03 +02:00
xChipseq 43d7f80660 add show keybinds setting 2025-08-30 21:41:04 +02:00
xChipseq 45291b0e98 change default keybinds to make sense 2025-08-30 21:31:34 +02:00
xChipseq 45972f71a9 major rework of the keybinds api 2025-08-30 21:13:42 +02:00
xChipseq a6f7171b71 add PauseTimerInVent 2025-08-29 14:32:13 +02:00
xChipseq e6d3819517 add uses mode for buttons 2025-08-29 14:20:19 +02:00
xChipseq b97ada22bc allow the duration to be 0 2025-08-29 13:58:06 +02:00
xChipseq 5f67960b28 cancelable abilities 2025-08-29 13:38:35 +02:00
xChipseq b4e30b2b68 wip 2025-08-29 13:04:18 +02:00
ang-xd 2237d3a2df Additional changes to keybind icons 2025-08-28 12:33:19 -04:00
ang-xd e708c23f81 Fix checks and positions for keybinds 2025-08-28 11:26:45 -04:00
ang-xd 3ca33b1d86 Merge branch 'master' into dev 2025-08-28 11:08:57 -04:00
angxl wtf 8967348cd3 Merge pull request #55 from CallOfCreator/keybinds
Keybind Icons
2025-08-28 11:08:26 -04:00
angxl wtf 305d2c3714 Merge pull request #54 from AtonyGit/dev
Adjust Keybinds for Buttons + Add IntroRoleRevealEvent
2025-08-28 11:08:14 -04:00
Atony 85a0275eea Add IntroRoleRevealEvent
Also needs to be changed for the android branch
2025-08-24 19:48:43 -05:00
Atony f006deab3f Fix Send Immediately warning from Reactor 2025-08-24 18:33:52 -05:00
CallOfCreator a274f23e65 Merge pull request #1 from WanderingPix/keybinds
Add Keybind Icons
2025-08-21 11:36:08 -07:00
Atony f1a5aef581 Remove requirement but allow blank keybind 2025-08-17 20:52:46 -05:00
Atony ce15e93e59 Fix DefaultKeybind name + remove requirement
By default, buttons will use no keybind, and will not add a new keybind option in doing so.
2025-08-17 20:40:03 -05:00
XtraCube 08ca995eb1 Merge pull request #52 from All-Of-Us-Mods/dev
mira api 0.2.1
2025-08-17 16:39:35 -04:00
XtraCube 453259df98 Merge branch 'master' into dev 2025-08-17 16:36:16 -04:00
XtraCube 2ec3eec370 bump version 2025-08-17 16:34:56 -04:00
ang-xd 2ee8730ba4 WIP Left side Modifiers HUD option 2025-08-14 00:56:29 -04:00
XtraCube c89e5938cc Merge pull request #43 from CallOfCreator/keybinds
Implement Keybind API
2025-08-13 09:32:42 -04:00
WanderingPixel 513be01c02 Add Keybind icons for custom buttons 2025-08-09 22:12:15 +01:00
CallOfCreator 01fa1f0915 Update conflict system 2025-08-09 11:41:43 +00:00
CallOfCreator 355eef509b Add support for modifier keys 2025-08-06 18:03:48 +00:00
XtraCube 763a493283 Merge pull request #51 from xChipseq/dummy-improvements
Dummies Fix
2025-08-05 16:50:09 -04:00
xChipseq 68e421bf91 literally just a dot 2025-08-05 22:35:39 +02:00
xChipseq c343ab5e9f dummy fix 2025-08-05 22:34:00 +02:00
XtraCube 9c1ef70ea7 Merge pull request #50 from xChipseq/dummy-improvements
Dummy Voting Improvements
2025-08-04 21:19:18 -04:00
xChipseq c0c8fa08ff changed DidVote to check VotesRemaining instead of Votes 2025-08-04 20:58:24 +02:00
xChipseq e7fba386aa dummy changes 2025-08-04 20:53:50 +02:00
XtraCube 72e5e0c137 Merge pull request #49 from xChipseq/round-start-event
Invoke RoundStartEvent when starting freeplay
2025-08-04 12:30:09 -04:00
XtraCube 4422072060 Merge pull request #48 from AtonyGit/dev
Add BeforeRoundEvent for edge case scenarios
2025-08-04 12:29:51 -04:00
xChipseq 8145c8e599 patch 2025-08-04 15:33:04 +02:00
Atony afd6fbc3b6 Add BeforeRoundStartEvent 2025-08-02 14:24:53 -05:00
XtraCube 74741b41a7 Merge pull request #47 from WanderingPix/Ghosts-included-in-GetClosestPlayer
Add argument for PlayerControl.GetClosestPlayer() to include ghosts
2025-07-31 09:13:04 -04:00
WanderingPixel f98429b5f9 Update GetClosestPlayer method 2025-07-31 09:09:37 -04:00
XtraCube bf7edb30ab fix warnings 2025-07-20 07:30:38 -04:00
XtraCube b4527014b7 Merge pull request #44 from AtonyGit/dev
Allow modifiers to have freeplay colors
2025-07-16 14:41:01 -04:00
Atony 277391ef1c Add freeplay modifier colors 2025-07-11 22:30:09 -05:00
CallOfCreator dfe124d67a Add missing credit from previous commit 2025-07-11 18:06:06 +00:00
CallOfCreator 2b06633488 Cleanup 2025-07-11 18:00:58 +00:00
CallOfCreator 4784b1f0ad Rename CurrentKeybind to DefaultKeybind 2025-07-09 13:48:38 +00:00
CallOfCreator d9d0d6bc62 Implement Keybind API 2025-07-08 20:41:20 +00:00
XtraCube 6b3038f397 shuffle modifiers before assigning 2025-07-07 18:47:12 -04:00
XtraCube 4e1db039a0 Merge pull request #42 from AtonyGit/dev
Fix Regions Dropdown in "Find Game" & Add Tab Count for Cosmetics Menu
2025-07-05 15:36:08 -04:00
Atony 62bee24789 Fix Server Dropdown for Game Finder 2025-07-04 22:43:13 -05:00
Atony 2156c80467 Cosmetic Tab Count Improvements 2025-07-04 22:42:47 -05:00
XtraCube 0ff60071c2 fix canuse event 2025-06-30 20:47:17 -04:00
XtraCube de2f3f0d95 update assetbundle 2025-06-29 22:31:21 -04:00
XtraCube 05f132c506 expose modifiers 2025-06-29 20:03:20 -04:00
XtraCube ab54c44434 expose custom roles in CustomRoleManager.cs 2025-06-29 19:57:58 -04:00
XtraCube bf652b68c3 fix warnings 2025-06-28 23:04:30 -04:00
XtraCube 00377c7926 Merge remote-tracking branch 'origin/dev' into dev 2025-06-28 22:57:09 -04:00
XtraCube 55555604f3 error proof buttons 2025-06-28 22:57:04 -04:00
ang-xd de77a9fa92 Add page counter in options screen 2025-06-28 20:55:22 -04:00
ang-xd eba2a037d3 Bug fixes and chat pasting 2025-06-28 19:21:47 -04:00
ang-xd bc7f772de9 Add ModdedPlayerOption and fix role options scroll 2025-06-28 18:44:31 -04:00
ang-xd f43191c4f2 Fix role option visibility & shift 5 increments on chance 2025-06-27 19:16:56 -04:00
XtraCube cd818a18cf Merge pull request #40 from All-Of-Us-Mods/dev
0.2.0 Update
2025-06-27 15:52:24 -04:00
XtraCube 56d4e399d9 Merge branch 'master' into dev 2025-06-27 15:50:58 -04:00
XtraCube 0d41ebbc13 adjust server dropdown to fit 2 columns 2025-06-27 15:47:38 -04:00
XtraCube 9388ff487a fix scrolling bug on role menu 2025-06-27 15:21:58 -04:00
XtraCube 720491e5cf make role options wrap around 2025-06-27 15:16:14 -04:00
XtraCube 9884df356e hide hidden role settings in advanced menu 2025-06-27 14:28:09 -04:00
XtraCube 7ec4addacc cleanup 2025-06-24 23:13:00 -04:00
XtraCube bc9bc0ce24 brace style 2025-06-24 21:38:36 -04:00
XtraCube aff1c311fa update readme 2025-06-24 21:24:40 -04:00
XtraCube 0d2a9d9700 fix warnings 2025-06-24 15:06:45 -04:00
XtraCube 669c541279 fix advanced role settings scroll 2025-06-24 14:57:48 -04:00
XtraCube 2e5f9639b3 fix role settings menu 2025-06-24 12:32:13 -04:00
XtraCube e6d1f440c8 adjust it 2025-06-24 12:27:02 -04:00
XtraCube 2ac8bdc240 fix lobby view settings scroll issues 2025-06-24 12:26:22 -04:00
XtraCube d5d99001ea privacy change 2025-06-23 19:14:22 -04:00
XtraCube 5628b79814 Merge remote-tracking branch 'origin/presets' into dev
# Conflicts:
#	MiraAPI/GameOptions/IModdedOption.cs
#	MiraAPI/GameOptions/ModdedOptionsManager.cs
#	MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
#	MiraAPI/Patches/Options/GameSettingMenuPatches.cs
#	MiraAPI/Patches/Options/LobbyViewPanePatches.cs
#	MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
#	MiraAPI/Utilities/Assets/MiraAssets.cs
#	MiraAPI/Utilities/Assets/SpriteTools.cs
#	MiraAPI/Utilities/Extensions.cs
2025-06-23 11:44:30 -04:00
XtraCube 72a44f65f1 fix role menu scrolling bug 2025-06-23 11:31:49 -04:00
XtraCube db6a336a63 Add modifier tab scroll positions and fix scroll positions not saving in some cases. 2025-06-23 11:07:28 -04:00
XtraCube 90b23ec2ca fix role scroll position issue 2025-06-23 10:31:47 -04:00
XtraCube 20e927208c make options wrap around in values 2025-06-21 20:17:57 -04:00
XtraCube 13ef3bdd49 add format string for cooldown timers 2025-06-21 19:32:28 -04:00
XtraCube b3ff6755bb suppress naming warnings 2025-06-21 18:52:18 -04:00
XtraCube 67604600cd remove obsolete code 2025-06-21 18:52:09 -04:00
XtraCube 6e35de6185 add BeforeGameEndEvent.cs 2025-06-21 18:43:56 -04:00
XtraCube a26ec51c7d send murder RPC immediately 2025-06-21 18:24:32 -04:00
XtraCube 3f6ae4cc41 send gameover immediately 2025-06-21 18:21:37 -04:00
XtraCube 15a2cdcd5b Merge pull request #36 from All-Of-Us-Mods/2025.5.20
Update to 2025.5.20
2025-06-21 17:27:23 -04:00
XtraCube cf0bd6ec38 fixed presets 2025-06-20 08:43:11 -04:00
XtraCube 130236e899 oops fix bad code 2025-06-20 08:38:31 -04:00
XtraCube 3b7b247ac0 refresh presets when opening preset menu 2025-06-20 08:32:24 -04:00
XtraCube 52de9a22de fix preset loading networking 2025-06-19 23:24:02 -04:00
XtraCube 372a9e3aa9 suppress RPC warnings 2025-06-16 21:31:58 -04:00
XtraCube 10a9dcfa34 suppress RPC warnings 2025-06-11 21:26:08 -04:00
XtraCube d46fdb0185 comments about rpcmurder inlining 2025-05-30 17:15:52 -04:00
XtraCube a361afbcbf add comment about meetinghud inline issue 2025-05-30 17:12:03 -04:00
XtraCube 24cdac0243 Fix role options collection inlining 2025-05-30 17:07:11 -04:00
XtraCube 5fe75843ad fix haunt menu inlining 2025-05-30 17:01:43 -04:00
XtraCube a0436dcbba Fix ToggleOption inlining 2025-05-30 16:58:32 -04:00
XtraCube 35be381a77 Fix LobbyViewPane inlining 2025-05-30 16:53:15 -04:00
XtraCube 1298beb517 fix kill anim inlining 2025-05-30 16:06:21 -04:00
XtraCube d41c19a176 Adjust kill anim patch for inlining 2025-05-30 15:45:59 -04:00
XtraCube 476f4400ad Fix ReportBodyEventPatch.cs 2025-05-29 22:04:34 -04:00
XtraCube 2b46d90570 Merge branch 'dev' into 2025.5.20 2025-05-27 16:09:29 -04:00
XtraCube ae4aaf7af9 adjust mirabundle name to work on epic 2025-05-27 16:09:14 -04:00
XtraCube 757b9436ca adjust role message 2025-05-27 16:09:05 -04:00
XtraCube 7ea10afffb dont need reactor dev nuget anymore 2025-05-21 18:18:10 -04:00
XtraCube 0ab80c42f9 Update discord badge 2025-05-20 22:25:21 -04:00
XtraCube bc144568f4 fix harmony error due to renamed parameter 2025-05-20 18:51:55 -04:00
XtraCube 07f402e659 update reactor 2025-05-20 18:42:59 -04:00
XtraCube 1a6aed539a fix lobbyview modifier options showing in game options 2025-05-18 14:49:40 -04:00
XtraCube 023cdaa470 fix role setting menu bug 2025-05-18 14:46:24 -04:00
XtraCube 98dd5ca056 fix more warnings 2025-05-18 12:40:40 -04:00
XtraCube 1f3e21ce19 cleanup more warnings 2025-05-18 10:01:48 -04:00
XtraCube 15e441dd71 add documentation in mira assets 2025-05-18 09:37:27 -04:00
XtraCube 8c67bbbd44 cleanup warnings 2025-05-18 09:32:33 -04:00
XtraCube aa584a8f44 update role notifications to match custom role colors 2025-05-18 09:25:26 -04:00
XtraCube a8a584b61b Fix bug with CustomRoleSingleton.cs 2025-05-18 09:09:14 -04:00
XtraCube acc4b1c8dd Save scroll positions on options and roles pages 2025-05-17 23:27:25 -04:00
XtraCube f9600bc906 fix chameleon role again 2025-05-17 23:27:14 -04:00
XtraCube f3da06870f fix chameleon role errors 2025-05-17 23:20:26 -04:00
XtraCube 9180dec9e1 update example mod to fit new role update 2025-05-17 23:16:01 -04:00
XtraCube ef6dedbd55 add errors for incorrect usage of registrations 2025-05-17 22:40:02 -04:00
XtraCube 5d3c3ed319 Update example events and catch exceptions in events 2025-05-17 22:32:29 -04:00
XtraCube 5b22d667fd remove redundant ICustomRole methods 2025-05-17 22:31:34 -04:00
XtraCube fb894c9f2c add wrappers around modifiers to catch exceptions 2025-05-17 17:42:19 -04:00
XtraCube d0449f57b5 adjust naming in plugin manager 2025-05-17 17:40:05 -04:00
XtraCube 5ffd591258 add dead body to death and murder events 2025-05-17 17:34:04 -04:00
XtraCube cc0ac1b988 Merge remote-tracking branch 'origin/dev' into dev 2025-05-17 17:28:08 -04:00
XtraCube 00b96f6e61 Merge pull request #34 from All-Of-Us-Mods/2025.3.25
2025.3.25 (16.0.0) support
2025-05-17 17:28:06 -04:00
XtraCube 6cb7f9f145 add unregistration of events 2025-05-17 17:26:29 -04:00
XtraCube b94472ee07 save options scroll states 2025-05-16 19:42:59 -04:00
XtraCube f8056c29da Merge branch 'dev' into presets 2025-05-16 19:27:26 -04:00
XtraCube e7f9a3bb60 hide roles in lobby view pane 2025-05-16 19:27:12 -04:00
XtraCube 8d4f2baf1e Merge remote-tracking branch 'origin/presets' into presets 2025-05-16 19:24:52 -04:00
XtraCube 11ba3dae64 save scroll position in roles menu 2025-05-16 19:24:47 -04:00
AlchlcDvl 86145f94f4 Merge branch 'dev' into presets 2025-05-04 22:29:23 -04:00
AlchlcDvl a9ded0b313 Modifier display improvement 2025-05-04 22:15:08 -04:00
XtraCube e0da386567 fix lag and broken roles saving/loading 2025-05-04 18:10:34 -04:00
XtraCube 6f1da2de9f add roles to presets 2025-05-02 19:25:03 -04:00
XtraCube b85358e304 Merge branch '2025.3.25' into presets 2025-05-02 19:19:33 -04:00
XtraCube 2c901bb9b9 remove unused thingy 2025-05-02 14:56:28 -04:00
XtraCube 68f40a96b7 fix preset system 2025-05-02 14:54:42 -04:00
XtraCube a96ffc42f2 fix saving default preset only 2025-04-29 17:23:05 -04:00
XtraCube 0907b14893 Merge branch 'feat-addressables-2' into presets 2025-04-29 17:22:57 -04:00
ang-xd 7a7204b0e0 Fix minigame crashes 2025-04-22 18:14:45 -04:00
ang-xd e57e88c179 Update AbstractOptionGroup.cs 2025-04-19 13:35:03 -04:00
ang-xd 878eab25b8 Merge branch 'dev' into 2025.3.25 2025-04-16 20:11:51 -04:00
angxl wtf e4f15f2ed9 Merge pull request #33 from All-Of-Us-Mods/feat-addressables-2
Addressables Loading
2025-04-16 19:51:27 -04:00
ang-xd bdc9fa28ac oops 2025-04-15 18:20:24 -04:00
ang-xd 64604aff98 Fix bug if you change hideonui 2025-04-15 18:05:20 -04:00
ang-xd 32a44d646d Futher progress on presets UI 2025-04-12 23:29:09 -04:00
ang-xd 8f590b94a8 Merge branch 'feat-addressables-2' into presets 2025-04-12 20:54:35 -04:00
ang-xd 3ba71d8f19 Add pagination 2025-04-12 16:22:55 -04:00
ang-xd 96a47d480a Add Modifiers HUD 2025-04-12 12:35:09 -04:00
XtraCube bb72d885cf Merge remote-tracking branch 'origin/feat-addressables-2' into feat-addressables-2 2025-04-09 22:06:37 -04:00
XtraCube e242975d75 fix modifier errors in end game win patch 2025-04-09 22:06:28 -04:00
ang-xd be6bc07a62 Merge branch 'dev' into feat-addressables-2 2025-04-06 19:53:32 -04:00
ang-xd a6d55ec90f Fix confirm ejects 2025-04-06 19:52:47 -04:00
ang-xd e5b6400eb8 Fix CastVote event issue 2025-04-06 14:50:29 -04:00
XtraCube 0d1d1891a8 Merge branch 'dev' into 2025.3.25 2025-04-05 16:30:17 -04:00
XtraCube 54d85a6996 Merge branch 'dev' into feat-addressables-2 2025-04-05 16:28:41 -04:00
XtraCube 531a799a24 fix double initialization of roles 2025-04-05 16:28:34 -04:00
XtraCube 3e6eb49d93 fix scroller in freeplay role menu 2025-04-05 15:00:51 -04:00
XtraCube 3fabe85a18 fix scroller in freeplay role menu 2025-04-05 15:00:35 -04:00
XtraCube a3fc847d07 Merge branch 'dev' into 2025.3.25 2025-04-05 14:32:26 -04:00
XtraCube ddfad0752d proper fix for meeting vote issue (I HATE IL2CPP) 2025-04-05 14:31:46 -04:00
ang-xd f1846375ca Fix end game stuff 2025-04-05 14:31:46 -04:00
XtraCube fcb693e899 fix modifier adding/removing AGAIN 2025-04-05 14:31:46 -04:00
XtraCube 21bec074fa improve GetAlivePlayers 2025-04-05 14:31:45 -04:00
ang-xd 30516104aa Merge branch 'feat-addressables-2' of https://github.com/All-Of-Us-Mods/MiraAPI into feat-addressables-2 2025-04-05 14:31:43 -04:00
ang-xd e7508e299b Faster loading of hats 2025-04-05 14:31:37 -04:00
XtraCube b0cd1d30b3 Merge remote-tracking branch 'origin/feat-addressables-2' into feat-addressables-2 2025-04-05 14:30:57 -04:00
XtraCube 9f3a40b48f proper fix for meeting vote issue (I HATE IL2CPP) 2025-04-05 14:30:52 -04:00
XtraCube 31ffe6274c fix silly mistake with settings 2025-04-05 14:19:52 -04:00
ang-xd 91c9a27df8 Fix end game stuff 2025-04-05 13:06:35 -04:00
MyDragonBreath 7484dd1a9f possible pagination... 2025-04-05 23:17:55 +11:00
XtraCube 9c9964eecf fix modifier adding/removing AGAIN 2025-04-04 18:26:16 -04:00
XtraCube 78fad65b97 improve GetAlivePlayers 2025-04-04 18:25:57 -04:00
XtraCube aef4d6daf1 actually fix meetinghud vote issue 2025-04-04 13:09:11 -04:00
XtraCube 87b94b1b6d actually fix meetinghud vote issue 2025-04-04 13:08:35 -04:00
XtraCube cf92b97e8b slight logic adjustment to fix inactive modifier checks 2025-04-04 12:42:24 -04:00
XtraCube bd0aa68cb8 slight logic adjustment to fix inactive modifier checks 2025-04-04 12:42:12 -04:00
XtraCube f43a542134 fix voting never ends bug 2025-04-04 12:35:26 -04:00
XtraCube efd176f7a4 fix timer bug related to effects 2025-04-04 12:35:26 -04:00
XtraCube b2fabdb139 add check inactive variants of hasmodifier 2025-04-04 12:35:26 -04:00
XtraCube a651099217 fix voting never ends bug 2025-04-04 12:34:41 -04:00
XtraCube e2f39c29a8 fix timer bug related to effects 2025-04-04 12:18:37 -04:00
XtraCube 140b697aaf add check inactive variants of hasmodifier 2025-04-04 09:33:59 -04:00
XtraCube a56b6d52ef rework modifier assignment 2025-04-03 21:05:44 -04:00
XtraCube 3a36acd131 rework modifier assignment 2025-04-03 20:37:09 -04:00
XtraCube 3adb7ef018 Merge branch 'dev' into feat-addressables-2 2025-04-01 21:22:41 -04:00
XtraCube 8c763e2a57 fix mira event priorities 2025-04-01 21:22:34 -04:00
XtraCube 2df50112eb Merge branch 'dev' into feat-addressables-2 2025-04-01 17:03:29 -04:00
XtraCube ce330325e4 fix broken submerged compat patch 2025-04-01 17:03:13 -04:00
XtraCube 516cc83170 Merge branch 'dev' into feat-addressables-2 2025-04-01 13:09:10 -04:00
XtraCube 89cd32d4a4 fix round start event 2025-04-01 13:09:00 -04:00
XtraCube f0e37daf0e Merge branch 'dev' into 2025.3.25 2025-04-01 12:25:59 -04:00
XtraCube 71e2f2e375 Merge branch 'dev' into feat-addressables-2 2025-03-31 22:16:38 -04:00
XtraCube a5ddec48c8 attempt at better config I/O during setup 2025-03-31 22:16:32 -04:00
XtraCube 9e6524817a Merge branch 'dev' into feat-addressables-2 2025-03-30 20:25:39 -04:00
XtraCube 5f4461f7e5 suppress il2cpp warnings 2025-03-30 16:26:22 -04:00
XtraCube 5e2ca191ba proper submerged compat 2025-03-30 16:19:52 -04:00
XtraCube 5af10b52a4 Merge branch 'dev' into feat-addressables-2 2025-03-30 15:44:47 -04:00
XtraCube a0d3afc7c7 add onmeeting event 2025-03-30 15:38:05 -04:00
XtraCube 2e769d79f2 try fix 2025-03-30 14:56:18 -04:00
XtraCube 41305ad0e2 Merge branch 'dev' into presets 2025-03-30 12:09:32 -04:00
XtraCube a8ac00d270 Merge branch 'dev' into feat-addressables-2 2025-03-29 22:07:24 -04:00
XtraCube 3cd7acbc70 fix ejection patch for submerged and airship 2025-03-29 22:07:12 -04:00
XtraCube ce583d11de Merge branch 'dev' into feat-addressables-2 2025-03-29 21:51:43 -04:00
XtraCube 31132e6e65 add RoundStartEvent.cs 2025-03-29 21:51:31 -04:00
XtraCube 1a53e2b045 add new meeting and intro events 2025-03-29 21:39:44 -04:00
XtraCube 2418e17af6 this doesnt work 2025-03-29 21:29:46 -04:00
XtraCube 6fb0afe14e add folder button 2025-03-29 21:29:40 -04:00
XtraCube 32b368c1ef update other addressable related things 2025-03-29 17:58:05 -04:00
XtraCube 103c32f386 finish AddressablesLoader.cs 2025-03-29 17:56:50 -04:00
XtraCube 662b867dc5 Merge remote-tracking branch 'origin/feat-addressables-2' into feat-addressables-2 2025-03-29 17:40:17 -04:00
XtraCube adae0225e9 fix merge error 2025-03-29 17:40:00 -04:00
XtraCube 0d347b8b1b Merge branch 'dev' into feat-addressables-2
# Conflicts:
#	MiraAPI.Example/ExampleEventHandlers.cs
2025-03-29 17:39:46 -04:00
XtraCube ab0063dd99 update preset system 2025-03-29 14:56:07 -04:00
XtraCube a35bf60870 add rounded box sprite 2025-03-29 14:26:15 -04:00
XtraCube 692b9d7180 improve spritetools 2025-03-29 14:26:06 -04:00
XtraCube b99efaa420 create loadable asset wrapper 2025-03-29 14:25:57 -04:00
XtraCube 2ecfa267d6 Merge branch 'dev' into presets
# Conflicts:
#	MiraAPI/Patches/Options/LobbyViewPanePatches.cs
2025-03-29 13:55:18 -04:00
XtraCube 4fad586aa7 use new alternate color in lobby 2025-03-29 13:53:58 -04:00
XtraCube 6d51e6806a fix warning in role stubs 2025-03-29 13:37:06 -04:00
XtraCube b28a0a1253 cleanup CustomPlayerMenu.cs 2025-03-29 13:35:16 -04:00
XtraCube 51b54d4340 reorganize vent patch 2025-03-29 13:12:19 -04:00
XtraCube ea9bf162ea rework modifier component caching 2025-03-29 13:12:12 -04:00
XtraCube 57105f487b cleanup 2025-03-29 13:11:15 -04:00
XtraCube c5e424e8ff hide empty role groups in settings 2025-03-29 12:26:38 -04:00
MyDragonBreath d3b40aec5a remove tou and me specific example 2025-03-29 23:25:55 +11:00
XtraCube 521e5a12fd more accurate doc 2025-03-29 00:27:07 -04:00
XtraCube e6e4a86546 fix stupid innerscuff code 2025-03-28 15:20:31 -04:00
XtraCube 9afe821b0f fix stupid innerscuff code 2025-03-27 18:09:27 -04:00
XtraCube a8d076656d more WIP presets 2025-03-26 18:45:48 -04:00
XtraCube be46ee7329 fix custom murder stats 2025-03-25 17:11:51 -04:00
XtraCube 75a15dc090 fix minigame patch 2025-03-25 17:09:16 -04:00
XtraCube cffb104392 update libs 2025-03-25 17:06:02 -04:00
XtraCube bb0b678747 fix VERY stupid issue with LobbyViewPanePatches.cs 2025-03-24 18:57:06 -04:00
XtraCube 429b64fc03 preset v2 2025-03-23 17:28:22 -04:00
XtraCube bf5dadc6a0 that def doesnt work 2025-03-23 15:34:20 -04:00
XtraCube b2137230da Merge branch 'dev' into presets 2025-03-23 15:19:51 -04:00
XtraCube f48143610c fix incorrect chunking algorithm 2025-03-23 15:15:21 -04:00
XtraCube 9e8c05f21d text input WIP 2025-03-23 15:01:31 -04:00
XtraCube 2cfc5ac039 update example 2025-03-23 14:00:12 -04:00
XtraCube 2f56c95b39 Merge branch 'dev' into presets
# Conflicts:
#	MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
2025-03-23 13:45:49 -04:00
XtraCube 38f72c42a3 presets v1 2025-03-23 13:44:40 -04:00
MyDragonBreath 71b537dba9 final addressables 2025-03-23 22:59:11 +11:00
XtraCube e9beb3b423 fix weird behaviour 2025-03-21 15:24:48 -04:00
XtraCube 5b608f5b3a preset WIP 2025-03-21 15:21:18 -04:00
MyDragonBreath 23c0397afe an approach to addressables 2025-03-19 23:07:09 +11:00
XtraCube 2248726450 IncludeInPreset property 2025-03-17 09:12:26 -04:00
XtraCube c60537cbd1 add comments to imoddedoption 2025-03-17 09:02:24 -04:00
XtraCube f5b4bdd1d4 Update discord links 2025-01-26 15:25:27 -05:00
XtraCube cd6c4f5390 Update README.md 2025-01-26 14:01:44 -05:00
XtraCube 5cc29d903a Update README.md 2025-01-26 14:01:30 -05:00
198 changed files with 8892 additions and 1307 deletions

No files matched your search

+1 -2
View File
@@ -1,2 +1 @@
[*.{cs,vb}]
dotnet_diagnostic.CA1707.severity = none
[*.{cs,vb}]
+1
View File
@@ -3,6 +3,7 @@
.vs/
obj/
bin/
.qodo/
*.user
*.lock.json
+4 -4
View File
@@ -1,13 +1,13 @@
<Project>
<ItemGroup>
<PackageReference Include="Reactor" Version="2.3.1" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.697" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2024.10.29" PrivateAssets="all" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.10.14" PrivateAssets="all" />
<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.0.1" PrivateAssets="all" ExcludeAssets="runtime" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime" />
</ItemGroup>
<Target Name="Copy" AfterTargets="Build" Condition="'$(AmongUs)' != ''">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(AmongUs)/BepInEx/plugins/" UseSymboliclinksIfPossible="true" />
</Target>
+2 -2
View File
@@ -8,7 +8,7 @@
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.2.0</VersionPrefix>
<VersionPrefix>0.3.3</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>All Of Us, XtraCube, Angxl</Authors>
@@ -34,7 +34,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)\stylecop.json" Link="stylecop.json" Visible="false" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.32.0.97167">
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
@@ -3,6 +3,7 @@ using MiraAPI.Example.Options.Roles;
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Modifiers;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
@@ -15,10 +16,10 @@ public class FreezeButton : CustomActionButton<PlayerControl>
public override string Name => "Freeze";
public override float Cooldown => OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeDuration;
public override int MaxUses => (int)OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeUses;
public override bool PauseTimerInVent => true;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override MiraKeybind? Keybind => MiraGlobalKeybinds.PrimaryAbility;
protected override void OnClick()
{
+2
View File
@@ -1,5 +1,6 @@
using MiraAPI.Example.Modifiers;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Modifiers;
using MiraAPI.Utilities.Assets;
using UnityEngine;
@@ -15,6 +16,7 @@ public class MeetingButton : CustomActionButton
public override int MaxUses => 3;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override MiraKeybind? Keybind => MiraGlobalKeybinds.ModifierPrimaryAbility;
public override bool Enabled(RoleBehaviour? role)
{
@@ -2,6 +2,7 @@
using MiraAPI.Example.Roles;
using MiraAPI.GameEnd;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Utilities.Assets;
using UnityEngine;
@@ -12,6 +13,8 @@ public class NeutralKillerButton : CustomActionButton
public override string Name => "Win Game";
public override float Cooldown => 0f;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override MiraKeybind Keybind => ExampleKeybinds.NeutralWinKeybind;
protected override void OnClick()
{
CustomGameOver.Trigger<NeutralKillerGameOver>([PlayerControl.LocalPlayer.Data]);
@@ -3,6 +3,7 @@ using MiraAPI.Example.Options.Roles;
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
@@ -21,6 +22,7 @@ public class TeleportButton : CustomActionButton
public override LoadableAsset<Sprite> Sprite => ExampleAssets.TeleportButton;
public override Color TextOutlineColor => new Color32(221, 176, 152, 255);
public static bool IsZoom { get; private set; }
public override MiraKeybind? Keybind => MiraGlobalKeybinds.PrimaryAbility;
public override bool Enabled(RoleBehaviour? role)
{
+35
View File
@@ -0,0 +1,35 @@
using MiraAPI.Hud;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Buttons;
public class ThinkButton : CustomActionButton
{
public override string Name => "Think";
public override float Cooldown => 15f;
public override float EffectDuration => 10f;
public override int MaxUses => 1;
public override ButtonUsesMode UsesMode => ButtonUsesMode.PerRound;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
protected override void OnClick()
{
// This button does absolutely nothing besides changing its text
Button!.OverrideText("Thinking...");
}
public override void OnEffectEnd()
{
Button!.OverrideText("Think");
}
public override bool IsEffectCancellable()
{
return true;
}
public override bool Enabled(RoleBehaviour? role)
{
return role != null && role.IsImpostor;
}
}
+1
View File
@@ -5,6 +5,7 @@ namespace MiraAPI.Example;
public static class ExampleAssets
{
public static LoadableResourceAsset ExampleButton { get; } = new("MiraAPI.Example.Resources.ExampleButton.png");
public static LoadableResourceAsset CallMeetingButton { get; } = new("MiraAPI.Example.Resources.CallMeeting.png");
// Credit to EpicHorrors for the teleport button asset.
public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png");
+4 -1
View File
@@ -6,7 +6,10 @@ namespace MiraAPI.Example;
[RegisterCustomColors]
public static class ExampleColors
{
public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f));
public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f))
{
ColorBrightness = CustomColorBrightness.Lighter,
};
public static CustomColor Rose { get; } = new("Rose", new Color(0.98f, 0.26f, 0.62f));
+6 -6
View File
@@ -18,7 +18,7 @@ public static class ExampleEventHandlers
public static void Initialize()
{
// You can register event handlers with the MiraEventManager class.
MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event =>
var handle = MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event =>
{
Logger<ExamplePlugin>.Info($"{@event.Source.Data.PlayerName} is about to kill {@event.Target.Data.PlayerName}");
});
@@ -32,18 +32,18 @@ public static class ExampleEventHandlers
{
Logger<ExamplePlugin>.Info($"{@event.Player.Data.PlayerName} completed {@event.Task.TaskType.ToString()}");
});
// You can also unregister event handlers using the handle returned from RegisterEventHandler.
MiraEventManager.UnregisterEventHandler(handle);
}
// If you want to add extra votes to a player, do something like this.
[RegisterEvent]
public static void StartMeetingEvent(StartMeetingEvent _)
{
foreach (var plr in PlayerControl.AllPlayerControls)
foreach (var plr in PlayerControl.AllPlayerControls.ToArray().Where(player => player.Data.Role is MayorRole))
{
if (plr.Data.Role is MayorRole)
{
plr.GetVoteData().IncreaseRemainingVotes(1);
}
plr.GetVoteData().IncreaseRemainingVotes(1);
}
}
+10
View File
@@ -0,0 +1,10 @@
using MiraAPI.Keybinds;
using Rewired;
namespace MiraAPI.Example;
[RegisterCustomKeybinds]
public static class ExampleKeybinds
{
public static MiraKeybind NeutralWinKeybind { get; } = new("Neutral Win", KeyboardKeyCode.C, [ModifierKey.Shift]);
}
+53
View File
@@ -0,0 +1,53 @@
using BepInEx.Configuration;
using MiraAPI.LocalSettings;
using MiraAPI.LocalSettings.Attributes;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Example;
public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config)
{
public override string TabName => "Mira Example";
protected override bool ShouldCreateLabels => false;
public override LocalSettingTabAppearance TabAppearance => new()
{
ToggleActiveColor = Color.blue,
ToggleInactiveColor = Color.red,
};
[LocalSettingsButton]
public LocalSettingsButton ExampleButton { get; private set; } = new("Example Button", OnExampleButtonClick);
[LocalToggleSetting]
public ConfigEntry<bool> ExampleToggle { get; private set; } = config.Bind("General", "Example Bool", true);
[LocalToggleSetting]
public ConfigEntry<bool> ExampleToggle2 { get; private set; } = config.Bind("General", "Example Bool 2", false);
[LocalSliderSetting(suffixType: MiraNumberSuffixes.Percent, formatString: "0", roundValue: true)]
public ConfigEntry<float> ExampleSlider { get; private set; } = config.Bind("General", "Slider", 50f);
[LocalNumberSetting]
public ConfigEntry<float> ExampleNumber { get; private set; } = config.Bind("General", "Example Number", 4f);
[LocalNumberSetting(min: 0.1f, max: 2.5f, increment: 0.1f, formatString: "0.0", suffixType: MiraNumberSuffixes.Multiplier)]
public ConfigEntry<float> ExampleFloatNumber { get; private set; } = config.Bind("General", "Example Multplier", 1f);
[LocalEnumSetting]
public ConfigEntry<ExampleEnumSetting> ExampleEnum { get; private set; } = config.Bind("General", "Example Enum", ExampleEnumSetting.Fries);
private static void OnExampleButtonClick()
{
LocalSettingsTabSingleton<MiraApiSettings>.Instance.Open();
}
}
public enum ExampleEnumSetting
{
Cheeseburger,
Sandwich,
Fries,
ChickenNuggets,
}
+2 -1
View File
@@ -1,4 +1,5 @@
using BepInEx;
using System.Linq;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
+3 -5
View File
@@ -9,11 +9,9 @@
<ProjectReference Include="..\MiraAPI\MiraAPI.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\ExampleButton.png" />
<EmbeddedResource Include="Resources\FortniteBanner.jpeg" />
<EmbeddedResource Include="Resources\TeleportButton.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup>
<Import Project="..\AmongUs.props" />
</Project>
@@ -1,12 +1,26 @@
using MiraAPI.Example.Options.Modifiers;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Modifiers;
public class CaptainModifier : GameModifier
{
public override string ModifierName => "Captain";
public override LoadableAsset<Sprite>? ModifierIcon => ExampleAssets.CallMeetingButton;
public override void OnDeath(DeathReason reason)
{
Player.RemoveModifier(this);
}
public override string GetDescription()
{
return $"You can call a meeting from anywhere on the map.";
}
public override int GetAssignmentChance()
{
@@ -6,6 +6,11 @@ public class HighPriorityModifier : GameModifier
{
public override string ModifierName => "High Priority";
public override string GetDescription()
{
return "You are high priority! Idrk what this means either tbh...";
}
public override int GetAssignmentChance()
{
return 100;
@@ -10,6 +10,8 @@ public class ExampleOptions2 : AbstractOptionGroup
public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue.
public ModdedPlayerOption PlayerOption2 { get; } = new("YOU MUST CHOSE A PLAYER", false);
public ModdedPlayerOption PlayerOption { get; } = new("Eh whatever");
public ModdedToggleOption ToggleOpt1 { get; } = new("Toggle Option 1", false);
public ModdedToggleOption ToggleOpt2 { get; } = new("Toggle Option 2", false)
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+40 -12
View File
@@ -1,6 +1,7 @@
using AmongUs.GameOptions;
using MiraAPI.Patches.Stubs;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
@@ -14,37 +15,64 @@ public class ChameloenRole : CrewmateRole, ICustomRole
public Color RoleColor => Palette.AcceptedGreen;
public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
private bool _shouldHide;
public CustomRoleConfiguration Configuration => new(this)
{
OptionsScreenshot = ExampleAssets.Banner,
IntroSound = CustomRoleUtils.GetIntroSound(RoleTypes.Shapeshifter),
};
public void PlayerControlFixedUpdate(PlayerControl playerControl)
public override void Initialize(PlayerControl player)
{
if (playerControl.MyPhysics.Velocity.magnitude > 0)
Logger<ExamplePlugin>.Info("Initializing ChamelonRole for player: " + player.PlayerId);
RoleBehaviourStubs.Initialize(this, player);
_shouldHide = true;
}
public void FixedUpdate()
{
if (!Player || !_shouldHide)
{
SpriteRenderer rend = playerControl.cosmetics.currentBodySprite.BodySprite;
TextMeshPro tmp = playerControl.cosmetics.nameText;
return;
}
if (Player.MyPhysics.Velocity.magnitude > 0)
{
var rend = Player.cosmetics.currentBodySprite.BodySprite;
var tmp = Player.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, 1), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, 1), Time.deltaTime * 4f);
foreach (var cosmetic in playerControl.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
{
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, 1), Time.deltaTime * 4f);
}
}
else
{
SpriteRenderer rend = playerControl.cosmetics.currentBodySprite.BodySprite;
TextMeshPro tmp = playerControl.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
SpriteRenderer rend = Player.cosmetics.currentBodySprite.BodySprite;
TextMeshPro tmp = Player.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
foreach (var cosmetic in playerControl.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
{
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
}
}
}
public override void Deinitialize(PlayerControl targetPlayer)
{
Logger<ExamplePlugin>.Info("Deinitializing ChamelonRole for player: " + targetPlayer.PlayerId);
RoleBehaviourStubs.Deinitialize(this, targetPlayer);
_shouldHide = false;
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>(true))
{
cosmetic.color = Color.white;
}
Player.cosmetics.currentBodySprite.BodySprite.color = Color.white;
Player.cosmetics.nameText.color = Color.white;
}
}
+3 -1
View File
@@ -1,4 +1,5 @@
using MiraAPI.Example.GameOver;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Example.GameOver;
using MiraAPI.GameEnd;
using MiraAPI.Roles;
using UnityEngine;
@@ -22,6 +23,7 @@ public class NeutralKillerRole : ImpostorRole, ICustomRole
public RoleOptionsGroup RoleOptionsGroup { get; } = new("Outcast", Color.gray);
[HideFromIl2Cpp]
public TeamIntroConfiguration? IntroConfiguration { get; } = new(
Color.gray,
"OUTCAST",
+27 -6
View File
@@ -13,19 +13,24 @@ namespace MiraAPI.Colors;
public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
{
/// <summary>
/// Gets the main color.
/// Gets or sets the main color.
/// </summary>
public Color32 MainColor { get; } = mainColor;
public Color32 MainColor { get; set; } = mainColor;
/// <summary>
/// Gets the shadow color.
/// Gets or sets the shadow color.
/// </summary>
public Color32 ShadowColor { get; } = shadowColor;
public Color32 ShadowColor { get; set; } = shadowColor;
/// <summary>
/// Gets the name of the color.
/// Gets or sets the name of the color.
/// </summary>
public StringNames Name { get; } = name;
public StringNames Name { get; set; } = name;
/// <summary>
/// Gets or sets whether the color is lighter or darker. Can be used to help with colorblind stuff.
/// </summary>
public CustomColorBrightness ColorBrightness { get; set; } = CustomColorBrightness.Darker;
/// <inheritdoc />
public CustomColor(StringNames name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60))
@@ -42,3 +47,19 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha
{
}
}
/// <summary>
/// Can be used to help with colorblind stuff.
/// </summary>
public enum CustomColorBrightness
{
/// <summary>
/// A lighter color.
/// </summary>
Lighter,
/// <summary>
/// A darker color.
/// </summary>
Darker,
}
+22
View File
@@ -0,0 +1,22 @@
using System;
namespace MiraAPI.Events;
/// <summary>
/// Event handle to use when unregistering an event.
/// </summary>
public class MiraEventHandle
{
/// <summary>
/// Gets the type of event.
/// </summary>
public Type EventType { get; }
internal MiraEventWrapper EventWrapper { get; }
internal MiraEventHandle(Type type, MiraEventWrapper eventWrapper)
{
EventType = type;
EventWrapper = eventWrapper;
}
}
+69 -24
View File
@@ -21,14 +21,21 @@ public static class MiraEventManager
public static bool InvokeEvent<T>(T eventInstance) where T : MiraEvent
{
EventWrappers.TryGetValue(typeof(T), out var handlers);
if (handlers == null)
if (handlers == null || handlers.Count == 0)
{
return false;
}
foreach (var handler in handlers)
{
((Action<T>)handler.EventHandler).Invoke(eventInstance);
try
{
((Action<T>)handler.EventHandler).Invoke(eventInstance);
}
catch (Exception ex)
{
Error($"Error invoking event handler for {typeof(T).Name}: {ex.ToString()}");
}
}
return true;
@@ -43,14 +50,21 @@ public static class MiraEventManager
public static bool InvokeEvent(MiraEvent eventInstance, Type type)
{
EventWrappers.TryGetValue(type, out var handlers);
if (handlers == null)
if (handlers == null || handlers.Count == 0)
{
return false;
}
foreach (var handler in handlers)
{
handler.EventHandler.DynamicInvoke(eventInstance);
try
{
handler.EventHandler.DynamicInvoke(eventInstance);
}
catch (Exception ex)
{
Error($"Error invoking event handler for {type.Name}: {ex.ToString()}");
}
}
return true;
@@ -61,47 +75,78 @@ public static class MiraEventManager
/// </summary>
/// <param name="type">The type of event.</param>
/// <param name="methodInfo">The MethodInfo of the event handler.</param>
/// <param name="priority">The priority of the event handler. Higher values are called first.</param>
public static void RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0)
/// <param name="priority">The priority of the event handler. Lower values are called first.</param>
/// <returns>An event handle to use when unregistering the event.</returns>
public static MiraEventHandle RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0)
{
if (!type.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Type must be a subclass of MiraEvent: {nameof(type)}");
return;
throw new InvalidOperationException($"Type must be a subclass of MiraEvent: {type.FullName}");
}
if (!EventWrappers.TryGetValue(type, out var handlers))
{
handlers = [];
EventWrappers.Add(type, handlers);
}
EventWrappers.TryAdd(type, []);
var handlers = EventWrappers[type];
var @delegate = Delegate.CreateDelegate(typeof(Action<>).MakeGenericType(type), methodInfo);
handlers.Add(new MiraEventWrapper(@delegate, priority));
var eventWrapper = new MiraEventWrapper(@delegate, priority);
var index = handlers.BinarySearch(eventWrapper, Comparer<MiraEventWrapper>.Create((a, b) => a.Priority.CompareTo(b.Priority)));
if (index < 0)
{
index = ~index;
}
handlers.Insert(index, eventWrapper);
return new MiraEventHandle(type, eventWrapper);
}
/// <summary>
/// Register an event.
/// </summary>
/// <param name="handler">The callback method/handler for the event.</param>
/// <param name="priority">The priority of the event handler. Higher values are called first.</param>
/// <param name="priority">The priority of the event handler. Lower values are called first.</param>
/// <typeparam name="T">Type of event.</typeparam>
public static void RegisterEventHandler<T>(Action<T> handler, int priority = 0) where T : MiraEvent
/// <returns>An event handle to use when unregistering the event.</returns>
public static MiraEventHandle RegisterEventHandler<T>(Action<T> handler, int priority = 0) where T : MiraEvent
{
if (!EventWrappers.ContainsKey(typeof(T)))
{
EventWrappers.Add(typeof(T), []);
}
EventWrappers.TryAdd(typeof(T), []);
var handlers = EventWrappers[typeof(T)];
handlers.Add(new MiraEventWrapper(handler, priority));
var eventWrapper = new MiraEventWrapper(handler, priority);
var index = handlers.BinarySearch(eventWrapper, Comparer<MiraEventWrapper>.Create((a, b) => a.Priority.CompareTo(b.Priority)));
if (index < 0)
{
index = ~index;
}
handlers.Insert(index, eventWrapper);
return new MiraEventHandle(typeof(T), eventWrapper);
}
internal static void SortAllHandlers()
/// <summary>
/// Unregister an event using the event handle.
/// </summary>
/// <param name="eventHandle">A handle to the event.</param>
/// <returns>True if the event was unregistered successfully, false otherwise.</returns>
public static bool UnregisterEventHandler(MiraEventHandle eventHandle)
{
foreach (var handlers in EventWrappers.Values)
if (!EventWrappers.TryGetValue(eventHandle.EventType, out var handlers))
{
handlers.Sort((a, b) => a.Priority.CompareTo(b.Priority));
return false;
}
if (!handlers.Remove(eventHandle.EventWrapper))
{
return false;
}
if (handlers.Count == 0)
{
EventWrappers.Remove(eventHandle.EventType);
}
return true;
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ public class MiraEventWrapper
public Delegate EventHandler { get; }
/// <summary>
/// Gets the priority of the handler. Higher values are called first.
/// Gets the priority of the handler. Lower values are called first.
/// </summary>
public int Priority { get; }
+2 -2
View File
@@ -9,14 +9,14 @@ namespace MiraAPI.Events;
public class RegisterEventAttribute : Attribute
{
/// <summary>
/// Gets the priority of the event.
/// Gets the priority of the event. Lower values are called first.
/// </summary>
public int Priority { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RegisterEventAttribute"/> class.
/// </summary>
/// <param name="priority">The priority of the event.</param>
/// <param name="priority">The priority of the event. Lower values are called first.</param>
public RegisterEventAttribute(int priority = 0)
{
Priority = priority;
@@ -15,14 +15,21 @@ public class AfterMurderEvent : MiraEvent
/// </summary>
public PlayerControl Target { get; }
/// <summary>
/// Gets the player's body, if it exists.
/// </summary>
public DeadBody? DeadBody { get; }
/// <summary>
/// Initializes a new instance of the <see cref="AfterMurderEvent"/> class.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The killed player.</param>
public AfterMurderEvent(PlayerControl source, PlayerControl target)
/// <param name="deadBody">The player's vody, if it exists.</param>
public AfterMurderEvent(PlayerControl source, PlayerControl target, DeadBody? deadBody)
{
Source = source;
Target = target;
DeadBody = deadBody;
}
}
@@ -0,0 +1,21 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Invoked before RpcEndGame is called, allowing cancellation of the game end entirely.
/// </summary>
public class BeforeGameEndEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the reason for the game end.
/// </summary>
public GameOverReason Reason { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BeforeGameEndEvent"/> class.
/// </summary>
/// <param name="reason">The reason for the game end.</param>
public BeforeGameEndEvent(GameOverReason reason)
{
Reason = reason;
}
}
@@ -0,0 +1,21 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Event invoked on IntroCutscene.OnDestroy and ExileController.WrapUp to determine if it should run the round event or not.
/// </summary>
public class BeforeRoundStartEvent : MiraCancelableEvent
{
/// <summary>
/// Gets a value indicating whether the event was triggered by the IntroCutscene or ExileController.
/// </summary>
public bool TriggeredByIntro { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BeforeRoundStartEvent"/> class.
/// </summary>
/// <param name="triggeredByIntro">Whether the event was triggered by the intro or not.</param>
public BeforeRoundStartEvent(bool triggeredByIntro)
{
TriggeredByIntro = triggeredByIntro;
}
}
@@ -0,0 +1,12 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// The event that is invoked when the intro cutscene has finished playing.
/// </summary>
public class IntroEndEvent(IntroCutscene introCutscene) : MiraEvent
{
/// <summary>
/// Gets the instance of the IntroCutscene.
/// </summary>
public IntroCutscene IntroCutscene { get; } = introCutscene;
}
@@ -0,0 +1,21 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// The event that is invoked when the player's role is shown on the intro cutscene. Non cancelable.
/// </summary>
public class IntroRoleRevealEvent : MiraEvent
{
/// <summary>
/// Gets the instance of the IntroCutscene.
/// </summary>
public IntroCutscene IntroCutscene { get; }
/// <summary>
/// Initializes a new instance of the <see cref="IntroRoleRevealEvent"/> class.
/// </summary>
/// <param name="introCutscene">The intro cutscene.</param>
public IntroRoleRevealEvent(IntroCutscene introCutscene)
{
IntroCutscene = introCutscene;
}
}
@@ -0,0 +1,21 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Start Round event, invoked on IntroCutscene.OnDestroy and ExileController.WrapUp if the event isn't cancelled.
/// </summary>
public class RoundStartEvent : MiraEvent
{
/// <summary>
/// Gets a value indicating whether the event was triggered by the IntroCutscene or ExileController.
/// </summary>
public bool TriggeredByIntro { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RoundStartEvent"/> class.
/// </summary>
/// <param name="triggeredByIntro">Whether the event was triggered by the intro or not.</param>
public RoundStartEvent(bool triggeredByIntro)
{
TriggeredByIntro = triggeredByIntro;
}
}
@@ -24,6 +24,7 @@ public class UpdateSystemEvent : MiraCancelableEvent
/// Initializes a new instance of the <see cref="UpdateSystemEvent"/> class.
/// </summary>
/// <param name="systemType">The SystemType being updated.</param>
/// <param name="player">The player that is updating the system.</param>
/// <param name="amount">Amount to update System to.</param>
public UpdateSystemEvent(SystemTypes systemType, PlayerControl player, byte amount)
{
@@ -0,0 +1,12 @@
namespace MiraAPI.Events.Vanilla.Meeting;
/// <summary>
/// The event that is invoked when the meeting has ended.
/// </summary>
public class EndMeetingEvent(MeetingHud meetingHud) : MiraEvent
{
/// <summary>
/// Gets the MeetingHud instance.
/// </summary>
public MeetingHud MeetingHud { get; } = meetingHud;
}
@@ -0,0 +1,30 @@
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Event that is invoked after the local player successfully votes a player or skips. This event is not cancelable.
/// </summary>
public class AfterVoteEvent : MiraEvent
{
/// <summary>
/// Gets the instance of the voter's vote data.
/// </summary>
public PlayerVoteArea VoteArea { get; }
/// <summary>
/// Gets the player who voted.
/// </summary>
public PlayerControl Player { get; }
/// <summary>
/// Initializes a new instance of the <see cref="AfterVoteEvent"/> class.
/// </summary>
/// <param name="playerVoteArea">The player vote area that was voted on.</param>
/// <param name="voter">The player who voted.</param>
public AfterVoteEvent(PlayerVoteArea playerVoteArea, PlayerControl voter)
{
VoteArea = playerVoteArea;
Player = voter;
}
}
@@ -0,0 +1,30 @@
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Event that is invoked before the local player confirms a vote on a player or when skipping. This event is cancelable.
/// </summary>
public class BeforeVoteEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the instance of the voter's vote data.
/// </summary>
public PlayerVoteArea VoteArea { get; }
/// <summary>
/// Gets the player who voted.
/// </summary>
public PlayerControl Player { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BeforeVoteEvent"/> class.
/// </summary>
/// <param name="playerVoteArea">The player vote area that was voted on.</param>
/// <param name="voter">The player who voted.</param>
public BeforeVoteEvent(PlayerVoteArea playerVoteArea, PlayerControl voter)
{
VoteArea = playerVoteArea;
Player = voter;
}
}
@@ -0,0 +1,44 @@
using System;
using MiraAPI.Utilities;
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Invoked before the dummy selects their vote. If canceled, the dummy won't vote.
/// </summary>
public class DummyVoteEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the dummy that needs to vote.
/// </summary>
public DummyBehaviour Dummy { get; }
/// <summary>
/// Gets the instance of the dummy's vote data.
/// </summary>
public PlayerVoteData VoteData { get; }
/// <summary>
/// Gets or sets a value indicating whether the dummy can vote to skip the meeting.
/// </summary>
public bool CanSkip { get; set; }
/// <summary>
/// Gets or sets a predicate for validating whether the dummy can vote on a player.
/// By default, excludes dead and disconnected players.
/// </summary>
public Predicate<PlayerControl> PlayerIsValid { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="DummyVoteEvent"/> class.
/// </summary>
/// <param name="dummy">The voting dummy.</param>
public DummyVoteEvent(DummyBehaviour dummy)
{
Dummy = dummy;
VoteData = dummy.myPlayer.GetVoteData();
CanSkip = true;
PlayerIsValid = x => !x.Data.IsDead && !x.Data.Disconnected;
}
}
@@ -24,6 +24,11 @@ public class HandleVoteEvent : MiraCancelableEvent
/// </summary>
public byte TargetId { get; }
/// <summary>
/// Gets a value indicating whether to prevent the vote from commencing.
/// </summary>
public bool PreventVote { get; }
/// <summary>
/// Gets the player info of the target.
/// </summary>
@@ -39,6 +44,7 @@ public class HandleVoteEvent : MiraCancelableEvent
VoteData = playerVoteData;
Player = playerVoteData.Owner;
TargetId = targetId;
PreventVote = false;
TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId);
}
}
@@ -0,0 +1,13 @@
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// The event that is invoked when the voting phase of a meeting is complete.
/// </summary>
/// <param name="meetingHud">The MeetingHud instance.</param>
public class VotingCompleteEvent(MeetingHud meetingHud) : MiraEvent
{
/// <summary>
/// Gets the MeetingHud instance.
/// </summary>
public MeetingHud MeetingHud { get; } = meetingHud;
}
@@ -15,14 +15,21 @@ public class PlayerDeathEvent : MiraEvent
/// </summary>
public DeathReason DeathReason { get; }
/// <summary>
/// Gets the dead body associated with the player, if any.
/// </summary>
public DeadBody? DeadBody { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PlayerDeathEvent"/> class.
/// </summary>
/// <param name="player">The player who died.</param>
/// <param name="reason">The reason the player died.</param>
public PlayerDeathEvent(PlayerControl player, DeathReason reason)
/// <param name="deadBody">The player's body, if it exists.</param>
public PlayerDeathEvent(PlayerControl player, DeathReason reason, DeadBody? deadBody)
{
Player = player;
DeathReason = reason;
DeadBody = deadBody;
}
}
+2 -2
View File
@@ -30,13 +30,13 @@ public static class GameOverManager
if (gameOverType.IsAbstract)
{
Logger<MiraApiPlugin>.Error("The type must not be abstract.");
Error("The type must not be abstract.");
return false;
}
if (gameOverType.GetConstructor(Type.EmptyTypes) == null)
{
Logger<MiraApiPlugin>.Error("The type must have a parameterless constructor.");
Error("The type must have a parameterless constructor.");
return false;
}
+2
View File
@@ -0,0 +1,2 @@
[*.cs]
dotnet_diagnostic.CS1591.severity = none
+5 -5
View File
@@ -21,7 +21,7 @@ public static class CustomGameModeManager
}
/// <summary>
/// Current gamemode.
/// Gets the current gamemode.
/// </summary>
public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode();
@@ -37,7 +37,7 @@ public static class CustomGameModeManager
return;
}
Logger<MiraApiPlugin>.Error($"No gamemode with id {id} found!");
Error($"No gamemode with id {id} found!");
}
/// <summary>
@@ -48,7 +48,7 @@ public static class CustomGameModeManager
{
if (!typeof(CustomGameMode).IsAssignableFrom(gameModeType))
{
Logger<MiraApiPlugin>.Warning($"{gameModeType.Name} does not inherit CustomGameMode!");
Warning($"{gameModeType.Name} does not inherit CustomGameMode!");
return;
}
@@ -56,13 +56,13 @@ public static class CustomGameModeManager
if (modeObj is not CustomGameMode gameMode)
{
Logger<MiraApiPlugin>.Error($"Failed to create instance of {gameModeType.Name}");
Error($"Failed to create instance of {gameModeType.Name}");
return;
}
if (GameModes.Any(x => x.Key == gameMode.Id))
{
Logger<MiraApiPlugin>.Error($"ID for gamemode {gameMode.Name} already exists!");
Error($"ID for gamemode {gameMode.Name} already exists!");
return;
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using UnityEngine;
namespace MiraAPI.GameOptions;
@@ -11,6 +12,11 @@ public abstract class AbstractOptionGroup
{
internal List<IModdedOption> Options { get; } = [];
/// <summary>
/// Gets a list of the options which are a part of this group.
/// </summary>
public ReadOnlyCollection<IModdedOption> Children => new(Options);
/// <summary>
/// Gets the name of the group. Visible in options menu.
/// </summary>
+84 -1
View File
@@ -6,18 +6,101 @@ using UnityEngine;
namespace MiraAPI.GameOptions;
/// <summary>
/// Interface for modded options.
/// </summary>
public interface IModdedOption
{
/// <summary>
/// Gets the unique identifier for the option.
/// </summary>
uint Id { get; }
/// <summary>
/// Gets the title of the option.
/// </summary>
string Title { get; }
/// <summary>
/// Gets the StringName for the option, used for localization.
/// </summary>
StringNames StringName { get; }
/// <summary>
/// Gets or sets the MiraPlugin that created this option.
/// </summary>
IMiraPlugin? ParentMod { get; set; }
/// <summary>
/// Gets the game setting data for the option.
/// </summary>
BaseGameSetting Data { get; }
/// <summary>
/// Gets the OptionBehaviour object of the option.
/// </summary>
OptionBehaviour? OptionBehaviour { get; }
/// <summary>
/// Gets or sets the visibility function for the option.
/// </summary>
Func<bool> Visible { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the option should be included with presets.
/// </summary>
bool IncludeInPreset { get; set; }
/// <summary>
/// Gets or sets the ConfigDefinition for the option, used for BepInEx configuration.
/// </summary>
ConfigDefinition? ConfigDefinition { get; set; }
OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container);
/// <summary>
/// Creates the option behaviour for the modded option.
/// </summary>
/// <param name="toggleOpt">The ToggleOption template.</param>
/// <param name="numberOpt">The NumberOption template.</param>
/// <param name="stringOpt">The StringOption template.</param>
/// <param name="playerOpt">The PlayerOption template.</param>
/// <param name="container">>The Transform container for the option.</param>
/// <returns>>The created OptionBehaviour object.</returns>
OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container);
/// <summary>
/// Gets the value as a float.
/// </summary>
/// <returns>>The value of the option as a float.</returns>
float GetFloatData();
/// <summary>
/// Gets the NetData for the option, used for network synchronization.
/// </summary>
/// <returns>>Returns the NetData object for the option.</returns>
NetData GetNetData();
/// <summary>
/// Handles incoming network data for the option.
/// </summary>
/// <param name="data">>The byte array representing the network data.</param>
void HandleNetData(byte[] data);
/// <summary>
/// Saves the option to a preset configuration file.
/// </summary>
/// <param name="presetConfig">The ConfigFile representing the preset configuration.</param>
/// <param name="saveDefault">Indicates whether to save the default value instead of the current value.</param>
void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false);
/// <summary>
/// Binds the option to a configuration file.
/// </summary>
/// <param name="config">The ConfigFile to bind the option to.</param>
void Bind(ConfigFile config);
/// <summary>
/// Loads the option from a preset configuration file, applying the values to the option's configuration.
/// </summary>
/// <param name="presetConfig">The ConfigFile representing the preset configuration.</param>
void LoadFromPreset(ConfigFile presetConfig);
}
+21 -10
View File
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using BepInEx.Configuration;
using HarmonyLib;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
@@ -21,6 +23,7 @@ public static class ModdedOptionsManager
private static readonly Dictionary<PropertyInfo, ModdedOptionAttribute> OptionAttributes = [];
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
internal static readonly Dictionary<uint, IModdedOption> ModdedOptions = [];
internal static readonly List<AbstractOptionGroup> Groups = [];
@@ -36,16 +39,18 @@ public static class ModdedOptionsManager
if (TypeToGroup.ContainsKey(type))
{
Logger<MiraApiPlugin>.Error($"Group {type.Name} already exists.");
Error($"Group {type.Name} already exists.");
return false;
}
Groups.Add(group);
TypeToGroup.Add(type, group);
pluginInfo.OptionGroups.Add(group);
pluginInfo.InternalOptionGroups.Add(group);
typeof(OptionGroupSingleton<>).MakeGenericType(type)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, group);
return true;
@@ -55,13 +60,13 @@ public static class ModdedOptionsManager
{
if (!TypeToGroup.TryGetValue(type, out var group))
{
Logger<MiraApiPlugin>.Error($"Failed to get group for {type.Name}");
Error($"Failed to get group for {type.Name}");
return;
}
if (property.GetValue(group) is not IModdedOption option)
{
Logger<MiraApiPlugin>.Error($"Failed to get option for {property.Name}");
Error($"Failed to get option for {property.Name}");
return;
}
@@ -76,13 +81,13 @@ public static class ModdedOptionsManager
{
if (OptionAttributes.ContainsKey(property))
{
Logger<MiraApiPlugin>.Error($"Property {property.Name} already has an attribute registered.");
Error($"Property {property.Name} already has an attribute registered.");
return;
}
if (!TypeToGroup.TryGetValue(type, out var group))
{
Logger<MiraApiPlugin>.Error($"Failed to get group for {type.Name}");
Error($"Failed to get group for {type.Name}");
return;
}
@@ -90,7 +95,7 @@ public static class ModdedOptionsManager
if (option == null)
{
Logger<MiraApiPlugin>.Error($"Failed to get option for {property.Name}");
Error($"Failed to get option for {property.Name}");
return;
}
@@ -119,7 +124,7 @@ public static class ModdedOptionsManager
option.ConfigDefinition = new ConfigDefinition(groupName, propertyName);
option.ParentMod = pluginInfo.MiraPlugin;
pluginInfo.Options.Add(option);
pluginInfo.InternalOptions.Add(option);
ModdedOptions.Add(option.Id, option);
group.Options.Add(option);
}
@@ -140,7 +145,7 @@ public static class ModdedOptionsManager
// we dont know how other plugins handle their configs
// this way, all the options are saved at once, instead of one by one
var oldConfigSetting = new Dictionary<MiraPluginInfo, bool>();
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet);
plugin.PluginConfig.SaveOnConfigSet = false;
@@ -156,7 +161,7 @@ public static class ModdedOptionsManager
option.HandleNetData(netData.Data);
}
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
plugin.PluginConfig.Save();
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
@@ -173,7 +178,10 @@ public static class ModdedOptionsManager
/// </summary>
/// <param name="__originalMethod">The original setter method.</param>
/// <param name="value">The new object value.</param>
#pragma warning disable CA1707
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
#pragma warning restore CA1707
{
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
attribute.SetValue(value);
@@ -185,7 +193,10 @@ public static class ModdedOptionsManager
/// <param name="__originalMethod">The original getter method.</param>
/// <param name="__result">The result of the property getter.</param>
/// <returns>False so the original getter gets skipped.</returns>
#pragma warning disable CA1707
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
#pragma warning restore CA1707
{
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
__result = attribute.GetValue();
@@ -2,7 +2,9 @@
namespace MiraAPI.GameOptions;
#pragma warning disable CA1852
internal class ModifierOptionGroup : AbstractOptionGroup
#pragma warning restore CA1852
{
public override string GroupName { get; }
@@ -18,7 +18,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// <summary>
/// Gets the string values of the enum.
/// </summary>
public string[]? Values { get; }
public string[] Values { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedEnumOption"/> class.
@@ -27,7 +27,8 @@ public class ModdedEnumOption : ModdedOption<int>
/// <param name="defaultValue">The default value as an int.</param>
/// <param name="enumType">The Enum type.</param>
/// <param name="values">An option list of string values to use in place of the enum name.</param>
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null) : base(title, defaultValue)
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
{
Values = values ?? Enum.GetNames(enumType);
Data = ScriptableObject.CreateInstance<StringGameSetting>();
@@ -37,13 +38,13 @@ public class ModdedEnumOption : ModdedOption<int>
data.Type = global::OptionTypes.String;
data.Values = values is null ?
Enum.GetNames(enumType).Select(CustomStringName.CreateAndRegister).ToArray()
: values.Select(CustomStringName.CreateAndRegister).ToArray();
: [.. values.Select(CustomStringName.CreateAndRegister)];
data.Index = Value;
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
@@ -87,7 +88,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// <inheritdoc />
protected override void OnValueChanged(int newValue)
{
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data?.GetValueString(newValue), false);
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false);
if (!OptionBehaviour)
{
return;
@@ -109,7 +110,7 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
/// <summary>
/// Gets the string values of the enum.
/// </summary>
public string[]? Values { get; }
public string[] Values { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedEnumOption{T}"/> class.
@@ -117,7 +118,8 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as an int.</param>
/// <param name="values">An option list of string values to use in place of the enum name.</param>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null) : base(title, defaultValue)
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
{
Values = values ?? Enum.GetNames(typeof(T));
Data = ScriptableObject.CreateInstance<StringGameSetting>();
@@ -127,13 +129,13 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
data.Type = global::OptionTypes.String;
data.Values = values is null ?
Enum.GetNames(typeof(T)).Select(CustomStringName.CreateAndRegister).ToArray()
: values.Select(CustomStringName.CreateAndRegister).ToArray();
: [.. values.Select(CustomStringName.CreateAndRegister)];
data.Index = Convert.ToInt32(Value);
data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo);
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
@@ -48,6 +48,7 @@ public class ModdedNumberOption : ModdedOption<float>
/// <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="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedNumberOption(
string title,
float defaultValue,
@@ -56,7 +57,8 @@ public class ModdedNumberOption : ModdedOption<float>
float increment,
MiraNumberSuffixes suffixType,
string? formatString = null,
bool zeroInfinity = false) : base(title, defaultValue)
bool zeroInfinity = false,
bool includeInPreset = true) : base(title, defaultValue, includeInPreset)
{
Min = min;
Max = max;
@@ -91,6 +93,7 @@ public class ModdedNumberOption : ModdedOption<float>
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
PlayerOption playerOpt,
Transform container)
{
var numberOption = Object.Instantiate(numberOpt, Vector3.zero, Quaternion.identity, container);
+57 -59
View File
@@ -4,6 +4,7 @@ using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using Reactor.Localization.Utilities;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.GameOptions.OptionTypes;
@@ -16,29 +17,19 @@ public abstract class ModdedOption<T> : IModdedOption
{
private IMiraPlugin? _parentMod;
/// <summary>
/// Gets the unique identifier of the option.
/// </summary>
/// <inheritdoc />
public uint Id { get; }
/// <summary>
/// Gets the title of the option.
/// </summary>
/// <inheritdoc />
public string Title { get; }
/// <summary>
/// Gets the StringName object of the option.
/// </summary>
/// <inheritdoc />
public StringNames StringName { get; }
/// <summary>
/// Gets the BaseGameSetting data of the option.
/// </summary>
public BaseGameSetting Data { get; protected init; }
/// <inheritdoc />
public BaseGameSetting Data { get; protected set; } = null!;
/// <summary>
/// Gets or sets the parent mod of the option.
/// </summary>
/// <inheritdoc />
public IMiraPlugin? ParentMod
{
get => _parentMod;
@@ -46,6 +37,7 @@ public abstract class ModdedOption<T> : IModdedOption
{
if (_parentMod != null || value == null) return;
_parentMod = value;
var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue);
Value = entry.Value;
}
@@ -66,37 +58,25 @@ public abstract class ModdedOption<T> : IModdedOption
/// </summary>
public Action<T>? ChangedEvent { get; set; }
/// <summary>
/// Gets or sets the visibility of the option.
/// </summary>
/// <inheritdoc />
public Func<bool> Visible { get; set; }
/// <summary>
/// Gets or sets the option behaviour of the option.
/// </summary>
/// <inheritdoc />
public bool IncludeInPreset { get; set; }
/// <inheritdoc />
public OptionBehaviour? OptionBehaviour { get; protected set; }
/// <summary>
/// Gets or sets the config definition of the option.
/// </summary>
public ConfigDefinition? ConfigDefinition
{
get => _configDefinition;
set
{
if (_configDefinition is not null) return;
_configDefinition = value;
}
}
private ConfigDefinition? _configDefinition;
/// <inheritdoc />
public ConfigDefinition? ConfigDefinition { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOption{T}"/> class.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
protected ModdedOption(string title, T defaultValue)
/// <param name="includeInPreset">Whether to include the option in the preset.</param>
protected ModdedOption(string title, T defaultValue, bool includeInPreset = true)
{
Id = ModdedOptionsManager.NextId;
Title = title;
@@ -104,6 +84,7 @@ public abstract class ModdedOption<T> : IModdedOption
Value = defaultValue;
StringName = CustomStringName.CreateAndRegister(Title);
Visible = () => true;
IncludeInPreset = includeInPreset;
}
internal void ValueChanged(OptionBehaviour optionBehaviour)
@@ -115,7 +96,8 @@ public abstract class ModdedOption<T> : IModdedOption
/// Sets the value of the option.
/// </summary>
/// <param name="newValue">The new value.</param>
public void SetValue(T newValue)
/// <param name="sendRpc">Whether to send the value to other players.</param>
public void SetValue(T newValue, bool sendRpc = true)
{
var oldVal = Value;
Value = newValue;
@@ -125,7 +107,7 @@ public abstract class ModdedOption<T> : IModdedOption
ChangedEvent?.Invoke(Value);
}
if (AmongUsClient.Instance.AmHost)
if (sendRpc && AmongUsClient.Instance.AmHost)
{
if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true)
{
@@ -138,22 +120,44 @@ public abstract class ModdedOption<T> : IModdedOption
OnValueChanged(newValue);
}
/// <summary>
/// Gets the float data of the option.
/// </summary>
/// <returns>A float object representing the option's value.</returns>
/// <inheritdoc />
public void SaveToPreset(ConfigFile presetConfig, bool saveDefault=false)
{
if (ConfigDefinition is null)
{
Error($"Attempted to save {Title} to preset, but ConfigDefinition is null.");
return;
}
Bind(presetConfig);
presetConfig[ConfigDefinition].BoxedValue = saveDefault ? DefaultValue : Value;
}
/// <inheritdoc />
public void Bind(ConfigFile config)
{
config.Bind(ConfigDefinition, DefaultValue);
}
/// <inheritdoc />
public void LoadFromPreset(ConfigFile presetConfig)
{
if (presetConfig.TryGetEntry(ConfigDefinition, out ConfigEntry<T> entry))
{
SetValue(entry.Value, false);
}
else
{
Error($"Attempted to load {Title} from preset, but ConfigDefinition is not found in preset.");
}
}
/// <inheritdoc />
public abstract float GetFloatData();
/// <summary>
/// Gets the net data of the option.
/// </summary>
/// <returns>A NetData object representing this option's data.</returns>
/// <inheritdoc />
public abstract NetData GetNetData();
/// <summary>
/// Handles incoming net data.
/// </summary>
/// <param name="data">The NetData's byte array.</param>
/// <inheritdoc />
public abstract void HandleNetData(byte[] data);
/// <summary>
@@ -169,18 +173,12 @@ public abstract class ModdedOption<T> : IModdedOption
/// <returns>The value.</returns>
public abstract T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour);
/// <summary>
/// Creates the option behaviour.
/// </summary>
/// <param name="toggleOpt">The ToggleOption prefab.</param>
/// <param name="numberOpt">The NumberOption prefab.</param>
/// <param name="stringOpt">The StringOption prefab.</param>
/// <param name="container">The options container.</param>
/// <returns>A new OptionBehaviour for this modded option.</returns>
/// <inheritdoc />
public abstract OptionBehaviour CreateOption(
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
PlayerOption playerOpt,
Transform container);
/// <summary>
@@ -0,0 +1,138 @@
using System;
using System.Linq;
using Il2CppSystem.Collections.Generic;
using MiraAPI.Networking;
using MiraAPI.Utilities;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.GameOptions.OptionTypes;
/// <summary>
/// An option for selecting an ingame player. It returns an index of the values list, NOT A PLAYER ID. To get a player out of this, index the Values list.
/// </summary>
public class ModdedPlayerOption : ModdedOption<int>
{
/// <summary>
/// Gets or sets the included players.
/// </summary>
public List<NetworkedPlayerInfo> Values { get; set; }
/// <summary>
/// Gets or sets a filter for the players included in the option.
/// </summary>
public Func<NetworkedPlayerInfo, bool>? PlayerFilter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the option includes a "None" option. If None is selected, value will return -1.
/// </summary>
public bool AllowNone { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedPlayerOption"/> class.
/// </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>
public ModdedPlayerOption(string title, bool allowNone = true) : base(title, allowNone ? -1 : 0, false)
{
Data = ScriptableObject.CreateInstance<PlayerSelectionGameSetting>();
var data = (PlayerSelectionGameSetting)Data;
data.Title = StringName;
data.Type = global::OptionTypes.Player;
Values = new List<NetworkedPlayerInfo>();
AllowNone = allowNone;
Value = DefaultValue;
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
PlayerOption playerOpt,
Transform container)
{
var playerOption = Object.Instantiate(playerOpt, container);
var filteredList = GetFilteredPlayers();
Values = filteredList.ToIl2CppList();
playerOption.SetUpFromData(Data, 20);
playerOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
playerOption.Title = StringName;
playerOption.TitleText.text = TranslationController.Instance.GetString(StringName);
playerOption.Values = Values;
playerOption.Value = Value;
playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value);
OptionBehaviour = playerOption;
ModdedOptionsManager.CreatedPlayerOptions.TryAdd(playerOption, this);
playerOption.SetValueText();
playerOption.AdjustButtonsActiveState();
return playerOption;
}
/// <summary>
/// Returns a player ran through the filter.
/// </summary>
/// <returns>A list of filtered players.</returns>
public System.Collections.Generic.List<NetworkedPlayerInfo> GetFilteredPlayers()
{
return GameData.Instance.AllPlayers.ToArray()
.Where(x => PlayerFilter?.Invoke(x) ?? true)
.ToList();
}
/// <summary>
/// Returns a player from the value. If AllowNone is enabled and none is enabled this will return null.
/// </summary>
/// <returns>A player or null if chosen none.</returns>
public NetworkedPlayerInfo? GetPlayerValue()
{
return Value < 0 ? null : Values[Value];
}
/// <inheritdoc />
public override float GetFloatData()
{
return Value;
}
/// <inheritdoc />
public override NetData GetNetData()
{
return new NetData(Id, BitConverter.GetBytes(Value));
}
/// <inheritdoc />
public override void HandleNetData(byte[] data)
{
SetValue(BitConverter.ToInt32(data));
}
/// <inheritdoc />
public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return optionBehaviour.GetInt();
}
/// <inheritdoc />
protected override void OnValueChanged(int newValue)
{
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false);
if (!OptionBehaviour)
{
return;
}
if (OptionBehaviour is PlayerOption opt)
{
opt.Value = newValue;
}
}
}
@@ -15,7 +15,8 @@ public class ModdedToggleOption : ModdedOption<bool>
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
public ModdedToggleOption(string title, bool defaultValue) : base(title, defaultValue)
/// <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)
{
Data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
@@ -25,7 +26,7 @@ public class ModdedToggleOption : ModdedOption<bool>
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var toggleOption = Object.Instantiate(toggleOpt, Vector3.zero, Quaternion.identity, container);
+17
View File
@@ -0,0 +1,17 @@
namespace MiraAPI.Hud;
/// <summary>
/// The uses mode for custom buttons.
/// </summary>
public enum ButtonUsesMode
{
/// <summary>
/// Uses per game.
/// </summary>
PerGame,
/// <summary>
/// Uses per round.
/// </summary>
PerRound,
}
+122 -34
View File
@@ -2,8 +2,14 @@
using System.Globalization;
using MiraAPI.Events;
using MiraAPI.Events.Mira;
using MiraAPI.Keybinds;
using MiraAPI.LocalSettings;
using MiraAPI.Patches;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using Rewired;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
@@ -31,13 +37,29 @@ public abstract class CustomActionButton
/// </summary>
public abstract float Cooldown { get; }
/// <summary>
/// Gets a value indicating whether the timer should stop decreasing when the player is venting.
/// Note that both cooldown decrease and effect timer decrease is disabled.
/// </summary>
public virtual bool PauseTimerInVent => false;
/// <summary>
/// Gets the sprite of the button. Use <see cref="LoadableResourceAsset"/> to load a sprite from a resource path. Use <see cref="LoadableBundleAsset{T}"/> to load a sprite from an asset bundle.
/// </summary>
public abstract LoadableAsset<Sprite> Sprite { get; }
/// <summary>
/// Gets the button's effect duration in seconds. If the button has no effect, set to 0.
/// Gets the format string for the cooldown timer.
/// </summary>
public virtual string CooldownTimerFormatString => "0";
/// <summary>
/// Gets a value indicating whether the button has an effect ability. By default true if <see cref="EffectDuration"/> is greater than 0.
/// </summary>
public virtual bool HasEffect => EffectDuration > 0;
/// <summary>
/// Gets the button's effect duration in seconds. If 0, the effect needs to be ended manually.
/// </summary>
public virtual float EffectDuration => 0;
@@ -46,6 +68,16 @@ public abstract class CustomActionButton
/// </summary>
public virtual int MaxUses => 0;
/// <summary>
/// Gets the value indicating uses mode.
/// </summary>
public virtual ButtonUsesMode UsesMode => ButtonUsesMode.PerGame;
/// <summary>
/// Gets the keybind for this button. If null, no keybind will be added.
/// </summary>
public virtual BaseKeybind? Keybind => null;
/// <summary>
/// Gets the button's text outline color.
/// </summary>
@@ -56,11 +88,6 @@ public abstract class CustomActionButton
/// </summary>
public virtual ButtonLocation Location { get; set; } = ButtonLocation.BottomLeft;
/// <summary>
/// Gets a value indicating whether the button has an effect ability.
/// </summary>
public bool HasEffect => EffectDuration > 0;
/// <summary>
/// Gets a value indicating whether the button has limited uses.
/// </summary>
@@ -91,6 +118,16 @@ public abstract class CustomActionButton
/// </summary>
public ActionButton? Button { get; set; }
/// <summary>
/// Gets the gameObject used for the keybind icon.
/// </summary>
public GameObject? KeybindIcon { get; private set; }
/// <summary>
/// Gets or sets the keybind icon text.
/// </summary>
private TextMeshPro? KeybindText { get; set; }
/// <summary>
/// The method used to create the button.
/// </summary>
@@ -162,6 +199,26 @@ public abstract class CustomActionButton
}
}
}));
if (Keybind != null)
{
Keybind.OnActivate(() =>
{
if (Enabled(PlayerControl.LocalPlayer.Data.Role))
{
ClickHandler();
}
});
KeybindIcon =
Helpers.CreateKeybindIcon(
Button.gameObject,
Keybind.CurrentKey,
new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f)
);
KeybindText = KeybindIcon.transform.GetChild(0).GetComponent<TextMeshPro>();
Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.45f, -0.1f);
}
}
/// <summary>
@@ -176,11 +233,14 @@ public abstract class CustomActionButton
return;
}
this.Location = location;
Location = location;
if (!moveButton) return;
if (!moveButton)
{
return;
}
if (HudManagerPatches._bottomLeft == null || HudManagerPatches._bottomRight == null)
if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null)
{
return;
}
@@ -188,17 +248,17 @@ public abstract class CustomActionButton
switch (location)
{
case ButtonLocation.BottomLeft:
var gridArrange = HudManagerPatches._bottomLeft.GetComponent<GridArrange>();
var aspectPosition = HudManagerPatches._bottomLeft.GetComponent<AspectPosition>();
var gridArrange = HudManagerPatches.BottomLeft.GetComponent<GridArrange>();
var aspectPosition = HudManagerPatches.BottomLeft.GetComponent<AspectPosition>();
Button.transform.SetParent(HudManagerPatches._bottomLeft.transform);
Button.transform.SetParent(HudManagerPatches.BottomLeft.transform);
gridArrange.Start();
gridArrange.ArrangeChilds();
aspectPosition.AdjustPosition();
break;
case ButtonLocation.BottomRight:
Button.transform.SetParent(HudManagerPatches._bottomRight);
Button.transform.SetParent(HudManagerPatches.BottomRight);
break;
}
}
@@ -345,6 +405,13 @@ public abstract class CustomActionButton
{
}
/// <summary>
/// Returns a value indicating whether the effect can be ended early by the player.
/// Always false by default.
/// </summary>
/// <returns>Can the effect be canceled.</returns>
public virtual bool IsEffectCancellable() => false;
/// <summary>
/// When the button is usable, this method is called to determine if the button can be clicked.
/// By default, it takes into account the timer, effect, and uses.
@@ -353,7 +420,7 @@ public abstract class CustomActionButton
/// <returns>A value that represents whether the button can be clicked or not.</returns>
public virtual bool CanClick()
{
return Timer <= 0 && !EffectActive && CanUse();
return (EffectActive ? IsEffectCancellable() : Timer <= 0) && CanUse();
}
/// <summary>
@@ -363,7 +430,8 @@ 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 && (!LimitedUses || UsesLeft > 0);
return PlayerControl.LocalPlayer.moveable &&
((EffectActive && IsEffectCancellable()) || (!EffectActive && (!LimitedUses || UsesLeft > 0)));
}
/// <summary>
@@ -385,6 +453,12 @@ public abstract class CustomActionButton
/// </summary>
public virtual void ClickHandler()
{
if (EffectActive && IsEffectCancellable())
{
ResetCooldownAndOrEffect();
return;
}
if (!CanClick())
{
return;
@@ -417,36 +491,50 @@ public abstract class CustomActionButton
/// <param name="playerControl">The local PlayerControl.</param>
public virtual void FixedUpdateHandler(PlayerControl playerControl)
{
if (Timer >= 0 && !TimerPaused)
if (Keybind != null && KeybindText != null)
{
Timer -= Time.deltaTime;
KeybindText.text = Keybind.CurrentKey.ToString();
KeybindIcon?.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value &&
Keybind.CurrentKey != KeyboardKeyCode.None);
}
else if (HasEffect && EffectActive)
if (Timer >= 0)
{
if (!TimerPaused && (!PauseTimerInVent || !playerControl.inVent))
{
Timer -= Time.deltaTime;
}
}
else if (HasEffect && EffectActive && EffectDuration > 0)
{
EffectActive = false;
Timer = Cooldown;
OnEffectEnd();
}
if (CanUse())
if (Button)
{
Button?.SetEnabled();
}
else
{
Button?.SetDisabled();
}
if (CanUse())
{
Button!.SetEnabled();
}
else
{
Button!.SetDisabled();
}
if (EffectActive)
{
Button?.SetFillUp(Timer, EffectDuration);
if (EffectActive && EffectDuration > 0)
{
Button.SetFillUp(Timer, EffectDuration);
Button!.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(NumberFormatInfo.InvariantInfo);
Button!.cooldownTimerText.gameObject.SetActive(true);
}
else
{
Button?.SetCoolDown(Timer, Cooldown);
Button.cooldownTimerText.text = Timer.ToString(CooldownTimerFormatString, NumberFormatInfo.InvariantInfo);
Button.cooldownTimerText.gameObject.SetActive(true);
}
else
{
Button.SetCooldownFormat(Timer, Cooldown, CooldownTimerFormatString);
}
}
FixedUpdate(playerControl);
+4 -1
View File
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Reflection;
using MiraAPI.Events.Mira;
using MiraAPI.PluginLoading;
using UnityEngine;
namespace MiraAPI.Hud;
@@ -29,9 +30,11 @@ public static class CustomButtonManager
}
CustomButtons.Add(button);
pluginInfo.Buttons.Add(button);
pluginInfo.InternalButtons.Add(button);
typeof(CustomButtonSingleton<>).MakeGenericType(buttonType)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, button);
ButtonEventTypes.Add(buttonType, typeof(MiraButtonClickEvent<>).MakeGenericType(buttonType));
+3 -3
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace MiraAPI.Hud;
@@ -13,7 +14,6 @@ public static class CustomButtonSingleton<T> where T : CustomActionButton
/// <summary>
/// Gets the instance of the button.
/// </summary>
#pragma warning disable CA1000
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a custom action button.")]
public static T Instance => _instance ??= CustomButtonManager.CustomButtons.OfType<T>().Single();
#pragma warning restore CA1000
}
+27 -27
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.GameOptions;
using Il2CppInterop.Runtime.Attributes;
@@ -8,6 +9,8 @@ using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using UnityEngine;
using UnityEngine.Events;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
namespace MiraAPI.Hud;
@@ -16,22 +19,18 @@ namespace MiraAPI.Hud;
/// </summary>
/// <param name="il2CppPtr">Used by Il2Cpp. Do not use constructor, this is a MonoBehaviour.</param>
[RegisterInIl2Cpp]
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
public ShapeshifterPanel PanelPrefab;
public float XStart = -0.8f;
public float YStart = 2.15f;
public float XOffset = 1.95f;
public float YOffset = -0.65f;
public UiElement BackButton;
public UiElement DefaultButtonSelected;
public ShapeshifterPanel panelPrefab;
public float xStart = -0.8f;
public float yStart = 2.15f;
public float xOffset = 1.95f;
public float yOffset = -0.65f;
public UiElement backButton;
public UiElement defaultButtonSelected;
public List<ShapeshifterPanel> potentialVictims;
/// <summary>
@@ -42,17 +41,17 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter);
var ogMenu = shapeShifterRole.TryCast<ShapeshifterRole>().ShapeshifterMenu;
var ogMenu = shapeShifterRole.TryCast<ShapeshifterRole>()!.ShapeshifterMenu;
var newMenu = Instantiate(ogMenu);
var customMenu = newMenu.gameObject.AddComponent<CustomPlayerMenu>();
customMenu.PanelPrefab = newMenu.PanelPrefab;
customMenu.XStart = newMenu.XStart;
customMenu.YStart = newMenu.YStart;
customMenu.XOffset = newMenu.XOffset;
customMenu.YOffset = newMenu.YOffset;
customMenu.BackButton = newMenu.BackButton;
var back = customMenu.BackButton.GetComponent<PassiveButton>();
customMenu.panelPrefab = newMenu.PanelPrefab;
customMenu.xStart = newMenu.XStart;
customMenu.yStart = newMenu.YStart;
customMenu.xOffset = newMenu.XOffset;
customMenu.yOffset = newMenu.YOffset;
customMenu.backButton = newMenu.BackButton;
var back = customMenu.backButton.GetComponent<PassiveButton>();
back.OnClick.RemoveAllListeners();
back.OnClick.AddListener((UnityAction)(() =>
{
@@ -65,7 +64,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
newMenu.DestroyImmediate();
customMenu.transform.SetParent(Camera.main.transform, false);
customMenu.transform.SetParent(Camera.main!.transform, false);
customMenu.transform.localPosition = new Vector3(0f, 0f, -50f);
return customMenu;
}
@@ -75,6 +74,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
ControllerManager.Instance.CloseOverlayMenu(name);
}
/// <inheritdoc />
public override void Begin(PlayerTask task)
{
throw new NotImplementedException("Use the other Begin method.");
@@ -90,7 +90,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
MinigameStubs.Begin(this, null);
var back = BackButton.GetComponent<PassiveButton>();
var back = backButton.GetComponent<PassiveButton>();
back.OnClick.AddListener((UnityAction)(() =>
{
onClick(null);
@@ -107,13 +107,13 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
var num = i % 3;
var num2 = i / 3;
var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor;
var shapeshifterPanel = Instantiate(PanelPrefab, transform);
shapeshifterPanel.transform.localPosition = new Vector3(XStart + num * XOffset, YStart + num2 * YOffset, -1f);
var shapeshifterPanel = Instantiate(panelPrefab, transform);
shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); }));
shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white;
potentialVictims.Add(shapeshifterPanel);
list2.Add(shapeshifterPanel.Button);
}
ControllerManager.Instance.OpenOverlayMenu(name, BackButton, DefaultButtonSelected, list2);
ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
}
}
+78
View File
@@ -0,0 +1,78 @@
using System.Linq;
using HarmonyLib;
using Reactor.Utilities.Extensions;
using UnityEngine;
namespace MiraAPI.Hud;
/// <summary>
/// Custom action button that has multiple target objects.
/// </summary>
/// <typeparam name="T">The type of the target objects.</typeparam>
public abstract class MultiTargetButton<T> : CustomActionButton where T : MonoBehaviour
{
/// <summary>
/// Gets or sets the list of targets of the button.
/// </summary>
public T[] Targets { get; protected set; } = [];
/// <inheritdoc cref="CustomActionButton{T}.Distance"/>
public virtual float Distance => PlayerControl.LocalPlayer.Data.Role.GetAbilityDistance();
/// <summary>
/// Gets the maximum amount of targets the button can have.
/// </summary>
public virtual int MaxTargets { get; } = int.MaxValue;
/// <inheritdoc cref="CustomActionButton{T}.IsTargetValid"/>
public virtual bool IsTargetValid(T target)
{
return target != null;
}
/// <summary>
/// The method used to get the target objects.
/// </summary>
/// <returns>The list of target objects.</returns>
public abstract T[] GetTargets();
/// <inheritdoc cref="CustomActionButton{T}.SetOutline"/>
/// <param name="target">The target object to set the oultine.</param>
public abstract void SetOutline(T target, bool active);
/// <inheritdoc cref="CustomActionButton{T}.CanUse"/>
public override bool CanUse()
{
var newTargets = GetTargets();
foreach (var target in Targets)
{
if (!newTargets.Contains(target))
{
SetOutline(target, false);
}
}
Targets = newTargets
.Where(IsTargetValid)
.Take(MaxTargets)
.ToArray();
Targets.Do(t => SetOutline(t, true));
return base.CanUse() && Targets.Length > 0;
}
/// <inheritdoc cref="CustomActionButton{T}.CanClick"/>
public override bool CanClick()
{
return base.CanClick() && Targets.Length > 0;
}
/// <summary>
/// Use this to reset the button's targets after used.
/// </summary>
public virtual void ResetTarget()
{
Targets.Do(t => SetOutline(t, false));
Targets = [];
}
}
+72
View File
@@ -0,0 +1,72 @@
using System;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Base keybind class.
/// </summary>
public class BaseKeybind
{
/// <summary>
/// Gets the Rewired <see cref="InputAction"/> assinged for this keybind.
/// </summary>
public InputAction? RewiredInputAction { get; internal set; }
/// <summary>
/// Gets the unique identifier for this keybind. Used in Rewired.
/// </summary>
public string Id { get; protected set; }
/// <summary>
/// Gets the currently assigned keycode.
/// </summary>
public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByKeybind(this);
/// <summary>
/// Gets or sets the handler of the keybind. Invoked when the keybind is activated.
/// </summary>
protected Action Handler { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BaseKeybind"/> class.
/// </summary>
/// <param name="id">The unique identifier for this keybind.</param>
public BaseKeybind(string id)
{
Id = id;
Handler = () => { };
}
public BaseKeybind()
{
Handler = () => { };
}
/// <summary>
/// Triggers the keybind, invoking its handler.
/// </summary>
public void Invoke()
{
Handler.Invoke();
}
/// <summary>
/// Adds an <see cref="Action"/> that will be invoked when the keybind is used.
/// </summary>
/// <param name="action">The <see cref="Action"/> to add.</param>
public void OnActivate(Action action)
{
Handler += action;
}
/// <summary>
/// Removes an <see cref="Action"/> that would be invoked when the keybind is used.
/// Not recommended to use - add checks in the method added in <see cref="OnActivate"/> itself rather than removing it from the keybind.
/// </summary>
/// <param name="action">The <see cref="Action"/> to remove.</param>
public void RemoveOnActivate(Action action)
{
Handler -= action;
}
}
+98
View File
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Utilities;
using Reactor.Utilities;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Lets you register and manage mod keybinds with conflict checks and rebinding support.
/// </summary>
public static class KeybindManager
{
/// <summary>
/// Gets a list of all registered keybinds.
/// </summary>
public static List<MiraKeybind> Keybinds { get; } = new();
[HideFromIl2Cpp]
internal static Dictionary<Type, VanillaKeybind> VanillaKeybinds { get; set; } = [];
internal static void RewiredInit()
{
try
{
var instance = KeybindUtils.RewiredInputManager!;
foreach (var keybind in Keybinds)
{
if (instance.userData.actions.ToArray().Any(x => x.name == keybind.Id))
{
Warning($"Keybind of id {keybind.Id} already exists. Skipping it");
continue;
}
keybind.RewiredInputAction = instance.userData.RegisterModBind(
keybind.Id,
keybind.Name,
keybind.SourcePluginName,
keybind.DefaultKey,
modifiers: keybind.ModifierKeys);
}
}
catch (Exception e)
{
Error($"Error while registering keybinds in Rewired: {e}");
}
}
/// <summary>
/// Returns all conflicts where keybinds use the same key.
/// </summary>
/// <returns>A list of pairs of keybinds that conflict.</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);
if (all.Length > 1)
{
if (!conflicts.TryGetValue(keybind.CurrentKey, out var group))
{
conflicts.Add(keybind.CurrentKey, new List<MiraKeybind>());
group = conflicts[keybind.CurrentKey];
}
if (!group.Contains(keybind))
{
group.Add(keybind);
}
}
}
return conflicts;
}
/// <summary>
/// Returns all keybinds for a specified keycode.
/// </summary>
/// <param name="keyCode">The key to look for.</param>
/// /// <param name="exclusiveCheck">Should only exlusive keybinds be present.</param>
/// <returns>The list of keybinds.</returns>
public static MiraKeybind[] GetKeybindsForKey(KeyboardKeyCode keyCode, bool exclusiveCheck = false)
{
var all = new List<MiraKeybind>();
foreach (var keybind in Keybinds)
{
if (keybind.CurrentKey == keyCode && (keybind.Exclusive || !exclusiveCheck))
{
all.Add(keybind);
}
}
return all.ToArray();
}
}
+80
View File
@@ -0,0 +1,80 @@
using System;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Collection of keybind-related utility classes.
/// </summary>
public static class KeybindUtils
{
/// <summary>
/// Gets the Rewired <see cref="InputManager_Base"/> instance.
/// </summary>
public static InputManager_Base? RewiredInputManager { get; internal set; }
/// <summary>
/// Returns the currently assigned keycode of a keybind.
/// </summary>
/// <param name="keybind">The keybind to get the keycode from.</param>
/// <returns>The currently assigned keycode.</returns>
public static KeyboardKeyCode GetKeycodeByKeybind(BaseKeybind keybind)
{
if (keybind.RewiredInputAction == null)
{
return KeyboardKeyCode.None;
}
return GetKeycodeByActionId(keybind.RewiredInputAction.id);
}
/// <summary>
/// Gets the keycode for an action with ReInput.
/// </summary>
/// <param name="actionId">The action ID.</param>
/// <returns>The keyboard keycode.</returns>
public static KeyboardKeyCode GetKeycodeByActionId(int actionId)
{
return GetActionElementMap(actionId)?.keyboardKeyCode ?? KeyboardKeyCode.None;
}
/// <summary>
/// Gets a <see cref="ActionElementMap"/> by id with ReInput.
/// </summary>
/// <param name="actionId">The action ID.</param>
/// <returns>The keyboard keycode.</returns>
public static ActionElementMap? GetActionElementMap(int actionId)
{
var player = ReInput.players.GetPlayer(0);
return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false);
}
/// <summary>
/// Gets an <see cref="InputAction"/> by id.
/// </summary>
/// <param name="id">The action identifier name.</param>
/// <returns>The keyboard keycode.</returns>
public static InputAction? GetInputActionById(int id)
{
return RewiredInputManager?.userData?.GetActionById(id);
}
/// <summary>
/// Finds and returns an unused KeyCode that is not equal to the excluded key.
/// </summary>
/// <param name="exclude">The KeyCode to skip during the search.</param>
/// <returns>
/// The first available KeyCode not currently used by any registered keybind,
/// or KeyCode.None if none are available.
/// </returns>
public static KeyboardKeyCode FindAvailableKey(KeyboardKeyCode exclude)
{
foreach (KeyboardKeyCode key in Enum.GetValues<KeyboardKeyCode>())
{
if (key == exclude) continue;
bool used = KeybindManager.Keybinds.Exists(e => e.DefaultKey == key);
if (!used) return key;
}
return KeyboardKeyCode.None;
}
}
+41
View File
@@ -0,0 +1,41 @@
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Class containing all default keybinds.
/// Recommended for cross-mod compatibility.
/// </summary>
[RegisterCustomKeybinds]
public static class MiraGlobalKeybinds
{
/// <summary>
/// Gets the keybind for primary abilities.
/// </summary>
public static MiraKeybind PrimaryAbility { get; } = new("Primary Ability", KeyboardKeyCode.T);
/// <summary>
/// Gets the keybind for secondary abilities.
/// </summary>
public static MiraKeybind SecondaryAbility { get; } = new("Secondary Ability", KeyboardKeyCode.Y);
/// <summary>
/// Gets the keybind for Tetiary abilities.
/// </summary>
public static MiraKeybind TertiaryAbility { get; } = new("Tertiary Ability", KeyboardKeyCode.U);
/// <summary>
/// Gets the keybind for primary modifier abilities.
/// </summary>
public static MiraKeybind ModifierPrimaryAbility { get; } = new("Modifier Primary Ability", KeyboardKeyCode.I);
/// <summary>
/// Gets the keybind for secondary modifier abilities.
/// </summary>
public static MiraKeybind ModifierSecondaryAbility { get; } = new("Modifier Secondary Ability", KeyboardKeyCode.O);
/// <summary>
/// Gets the keybind for Tetiary modifier abilities.
/// </summary>
public static MiraKeybind ModifierTertiaryAbility { get; } = new("Modifier Tertiary Ability", KeyboardKeyCode.P);
}
+58
View File
@@ -0,0 +1,58 @@
using System.Globalization;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Represents a registered keybind.
/// </summary>
public class MiraKeybind : BaseKeybind
{
/// <summary>
/// Gets name of the keybind
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the default keycode of this keybind.
/// </summary>
public KeyboardKeyCode DefaultKey { get; }
/// <summary>
/// Gets the modifier keys assigned to this keybind.
/// Due to Rewired limitations, there can only be 3 modifier keys.
/// </summary>
public ModifierKey[] ModifierKeys { get; }
/// <summary>
/// Gets a value indicating whether this keybind should be checked for conflicts with other exclusive keybinds using the same key.
/// </summary>
public bool Exclusive { get; }
internal string? SourcePluginName { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="MiraKeybind"/> class.
/// </summary>
/// <param name="name">The name of the keybind.</param>
/// <param name="defaultKeycode">The default keycode.</param>
/// <param name="modifierKeys">Up to 3 optional modifier keys.</param>
/// <param name="exclusive">Is exclusive.</param>
public MiraKeybind(
string name,
KeyboardKeyCode? defaultKeycode,
ModifierKey[]? modifierKeys = null,
bool exclusive = true) : base(name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_'))
{
Name = name;
DefaultKey = defaultKeycode ?? KeyboardKeyCode.None;
ModifierKeys = modifierKeys ?? [];
Exclusive = exclusive;
}
/// <inheritdoc/>
public override string ToString()
{
return $"{Name} ({Id})";
}
}
@@ -0,0 +1,9 @@
using System;
namespace MiraAPI.Keybinds;
/// <summary>
/// Used to mark a class for custom keybinds registration.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class RegisterCustomKeybindsAttribute : Attribute;
+24
View File
@@ -0,0 +1,24 @@
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Represents a vanilla keybind.
/// </summary>
public class VanillaKeybind : BaseKeybind
{
/// <summary>
/// Gets the <see cref="ActionButton"/> this keybind is binded to.
/// </summary>
public ActionButton? Button { get; internal set; }
/// <summary>
/// Initializes a new instance of the <see cref="VanillaKeybind"/> class.
/// </summary>
/// <param name="id">The keybind id.</param>
public VanillaKeybind(int id)
{
RewiredInputAction = KeybindUtils.GetInputActionById(id);
Id = RewiredInputAction?.name;
}
}
@@ -0,0 +1,17 @@
namespace MiraAPI.Keybinds;
/// <summary>
/// Singleton for getting a vanilla keybind by <see cref="ActionButton"/>.
/// </summary>
/// <typeparam name="T">ActionButton type.</typeparam>
public static class VanillaKeybinding<T> where T : ActionButton
{
private static VanillaKeybind? _instance;
/// <summary>
/// Gets the instance of the option group.
/// </summary>
#pragma warning disable CA1000
public static VanillaKeybind Instance => _instance ??= KeybindManager.VanillaKeybinds[typeof(T)];
#pragma warning restore CA1000
}
@@ -0,0 +1,27 @@
using System;
using BepInEx.Configuration;
using MiraAPI.LocalSettings.SettingTypes;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Creates an enum setting for the <see cref="ConfigEntry{T}"/>.
/// </summary>
/// <param name="names">Optional custom enum names.</param>
/// <inheritdoc/>
[AttributeUsage(AttributeTargets.Property)]
public class LocalEnumSettingAttribute(
string? name = null,
string? description = null,
string[]? names = null
) : LocalSettingAttribute(name, description)
{
private readonly string? _name = name;
private readonly string? _description = description;
/// <inheritdoc/>
public override LocalEnumSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalEnumSetting(tab, configEntryBase, configEntryBase.SettingType, _name, _description, names);
}
}
@@ -0,0 +1,36 @@
using System;
using BepInEx.Configuration;
using MiraAPI.LocalSettings.SettingTypes;
using MiraAPI.Utilities;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Creates a number setting for the <see cref="ConfigEntry{T}"/>.
/// </summary>
/// <param name="min">Minimum range.</param>
/// <param name="max">Maximum range.</param>
/// <param name="increment">Increment per use.</param>
/// <param name="suffixType">Suffix for the value.</param>
/// <param name="formatString">Format string used when formating.</param>
/// <inheritdoc/>
[AttributeUsage(AttributeTargets.Property)]
public class LocalNumberSettingAttribute(
string? name = null,
string? description = null,
float min = 1,
float max = 5,
float increment = 1,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null
) : LocalSettingAttribute(name, description)
{
private readonly string? _name = name;
private readonly string? _description = description;
/// <inheritdoc/>
public override LocalNumberSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalNumberSetting(tab, configEntryBase, _name, _description, new FloatRange(min, max), increment, suffixType, formatString);
}
}
@@ -0,0 +1,24 @@
using System;
using BepInEx.Configuration;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Base class for all local settings attributes.
/// </summary>
/// <param name="name">The name of the setting. Defaults to entry key.</param>
/// <param name="description">The description of the setting. Defalts to entry description.</param>
[AttributeUsage(AttributeTargets.Property)]
public abstract class LocalSettingAttribute(
string? name = null,
string? description = null
) : Attribute
{
/// <summary>
/// Returns the created setting object.
/// </summary>
/// <param name="tab">Gets the tab where the setting is located.</param>
/// <param name="configEntryBase">Gets the config entry the setting is attached to.</param>
/// <returns>The created class.</returns>
public abstract ILocalSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase);
}
@@ -0,0 +1,12 @@
using System;
using BepInEx.Configuration;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Attribute for local setting buttons.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class LocalSettingsButtonAttribute : Attribute
{
}
@@ -0,0 +1,36 @@
using System;
using BepInEx.Configuration;
using MiraAPI.LocalSettings.SettingTypes;
using MiraAPI.Utilities;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Creates a slider setting for the <see cref="ConfigEntry{T}"/>.
/// </summary>
/// <inheritdoc/>
/// <param name="min">Minimum range.</param>
/// <param name="max">Maximum range.</param>
/// <param name="roundValue">Should the value be rounded.</param>
/// <param name="suffixType">Suffix for the value.</param>
[AttributeUsage(AttributeTargets.Property)]
public class LocalSliderSettingAttribute(
string? name = null,
float min = 0,
float max = 100,
string? description = null,
bool displayValue = false,
string? formatString = null,
bool roundValue = false,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None
) : LocalSettingAttribute(name, description)
{
private readonly string? _name = name;
private readonly string? _description = description;
/// <inheritdoc/>
public override LocalSliderSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalSliderSetting(tab, configEntryBase, _name, _description, new FloatRange(min, max), displayValue, suffixType, formatString, roundValue);
}
}
@@ -0,0 +1,25 @@
using System;
using BepInEx.Configuration;
using MiraAPI.LocalSettings.SettingTypes;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Creates a toggle setting for the <see cref="ConfigEntry{T}"/>.
/// </summary>
/// <inheritdoc/>
[AttributeUsage(AttributeTargets.Property)]
public class LocalToggleSettingAttribute(
string? name = null,
string? description = null
) : LocalSettingAttribute(name, description)
{
private readonly string? _name = name;
private readonly string? _description = description;
/// <inheritdoc/>
public override LocalToggleSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalToggleSetting(tab, configEntryBase, _name, _description);
}
}
+42
View File
@@ -0,0 +1,42 @@
using BepInEx.Configuration;
using UnityEngine;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Interface for all local settings.
/// </summary>
public interface ILocalSetting
{
/// <summary>
/// Gets the preferred name of a setting.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the description of a setting.
/// </summary>
string Description { get; }
/// <summary>
/// Gets the setting's gameobject.
/// </summary>
GameObject? Setting { get; }
/// <summary>
/// Gets the setting's config entry.
/// </summary>
ConfigEntryBase ConfigEntry { get; }
/// <summary>
/// Used to create the setting.
/// </summary>
/// <param name="toggle">Toggle template.</param>
/// <param name="slider">Slider template.</param>
/// <param name="parent">The parent of the setting.</param>
/// <param name="offset">The offset.</param>
/// <param name="order">The order.</param>
/// <param name="last">Whether it's the last on the row.</param>
/// <returns>The created setting.</returns>
GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last);
}
@@ -0,0 +1,78 @@
using System;
using MiraAPI.Utilities;
using Reactor.Localization.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Represents a local settings button.
/// </summary>
public class LocalSettingsButton(string text, Action onClick)
{
/// <summary>
/// Gets or sets the button text.
/// </summary>
public string Text { get; set; } = text;
/// <summary>
/// Gets or sets the button on-click action.
/// </summary>
public Action OnClick { get; set; } = onClick;
/// <summary>
/// Gets the tab instance this button belongs to.
/// </summary>
public LocalSettingsTab? Tab { get; internal set; }
internal GameObject CreateButton(ToggleButtonBehaviour toggle, Transform parent, ref float offset, ref int order, bool last)
{
var button = Object.Instantiate(toggle, parent).GetComponent<PassiveButton>();
var tmp = button.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>();
var rollover = button.GetComponent<ButtonRolloverHandler>();
tmp.GetComponent<TextTranslatorTMP>().Destroy();
button.gameObject.SetActive(true);
var toggleComp = button.GetComponent<ToggleButtonBehaviour>();
var background = toggleComp.Background;
button.transform.FindChild("ButtonHighlight")?.gameObject.DestroyImmediate();
toggleComp.Destroy();
if (last && order == 1)
{
// Button in the middle
button.transform.localPosition = new Vector3(0, 1.85f - offset, -7);
}
else
{
button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7);
}
tmp.text = Text;
button.name = Text;
button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
rollover.OutColor = Tab!.TabAppearance.ButtonColor;
rollover.OverColor = Tab!.TabAppearance.ButtonHoverColor;
rollover.Target = background;
background.color = Tab!.TabAppearance.ButtonColor;
button.OnClick.AddListener((UnityAction)OnClick);
background.transform.localScale = new Vector3(1, 1.25f, 1);
Helpers.DivideSize(button.gameObject, 1.1f);
order++;
if (order > 2 && !last)
{
offset += 0.5f;
order = 1;
}
if (last)
offset += 0.6f;
return button.gameObject;
}
}
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using BepInEx.Unity.IL2CPP;
using MiraAPI.GameOptions;
using Reactor.Utilities;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Manages local settings.
/// </summary>
public static class LocalSettingsManager
{
internal static readonly Dictionary<Type, LocalSettingsTab> TypeToTab = [];
internal static readonly List<LocalSettingsTab> Tabs = [];
internal static bool RegisterTab(Type type, BasePlugin pluginInfo)
{
if (Activator.CreateInstance(type, pluginInfo.Config) is not LocalSettingsTab tab)
{
return false;
}
if (TypeToTab.ContainsKey(type))
{
Error($"Local settings tab {type.Name} already exists.");
return false;
}
Tabs.Add(tab);
TypeToTab.Add(type, tab);
typeof(LocalSettingsTabSingleton<>).MakeGenericType(type)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, tab);
return true;
}
}
+299
View File
@@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using BepInEx.Configuration;
using HarmonyLib;
using MiraAPI.Patches.LocalSettings;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Represents a local settings tab. Gets a button by default.
/// </summary>
public abstract class LocalSettingsTab(ConfigFile config)
{
/// <summary>
/// Gets the name of the tab.
/// </summary>
public abstract string TabName { get; }
/// <summary>
/// Gets the tab appearance.
/// </summary>
public virtual LocalSettingTabAppearance TabAppearance { get; } = new();
/// <summary>
/// Gets a value indicating whether a button should be created for the tab.
/// </summary>
public virtual bool ShouldCreateButton => true;
/// <summary>
/// Gets a value indicating whether the tab should have category labels.
/// </summary>
protected virtual bool ShouldCreateLabels => true;
/// <summary>
/// Gets the mod's config file. Automatically found in the plugin loader.
/// </summary>
public ConfigFile Config { get; } = config ?? throw new ArgumentNullException(nameof(config));
/// <summary>
/// Gets the tab button instance.
/// </summary>
public TabGroup? TabButton { get; internal set; }
/// <summary>
/// Gets the list of settings.
/// </summary>
public List<ILocalSetting> Settings { get; } = [];
/// <summary>
/// Gets the list of buttons.
/// </summary>
public List<LocalSettingsButton> Buttons { get; } = [];
/// <summary>
/// Gets the index of this tab.
/// </summary>
protected int TabIndex => LocalSettingsManager.Tabs.IndexOf(this) + 10;
private Scroller? Scroller { get; set; }
/// <summary>
/// Invoked when an option value is changed.
/// </summary>
/// <param name="configEntry">The config entry which was changed.</param>
public virtual void OnOptionChanged(ConfigEntryBase configEntry)
{
}
/// <summary>
/// Attempts to open the tab.
/// Override for custom behavior.
/// </summary>
public virtual void Open()
{
OptionsMenuPatches.Instance?.OpenTabGroup(TabIndex);
}
/// <summary>
/// Creates the tab <see cref="GameObject"/> and it's content.
/// </summary>
/// <param name="instance">The <see cref="OptionsMenuBehaviour"/> instace.</param>
/// <returns>The created tab <see cref="GameObject"/>.</returns>
public virtual GameObject CreateTab(OptionsMenuBehaviour instance)
{
var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform);
tab.name = $"{TabName}Tab";
tab.transform.DestroyChildren();
tab.gameObject.SetActive(false);
if (Scroller == null)
{
Scroller = Helpers.CreateScroller(tab.transform, OptionsMenuPatches.MaskCollider);
}
if (TabButton != null)
{
TabButton.Content = tab.gameObject;
}
var generalLabel = instance.transform.FindChild("GeneralTab").FindChild("ControlGroup")
.FindChild("ControlText_TMP").gameObject;
var toggle = instance.transform.FindChild("GeneralTab").FindChild("ChatGroup").FindChild("CensorChatButton").GetComponent<ToggleButtonBehaviour>();
var slider = instance.transform.FindChild("GeneralTab").FindChild("SoundGroup").FindChild("SFXSlider").GetComponent<SlideBar>();
Dictionary<string, List<ILocalSetting>> entriesByGroup = new();
foreach (var entry in Settings)
{
var group = entry.ConfigEntry.Definition.Section;
if (!entriesByGroup.ContainsKey(group))
entriesByGroup.Add(group, []);
entriesByGroup[group].Add(entry);
}
float contentOffset = 0;
var contentOrder = 1;
var contentIndex = 1;
foreach (var pair in entriesByGroup)
{
if (ShouldCreateLabels)
{
CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset);
contentOrder = 1;
contentIndex = 1;
}
foreach (var setting in pair.Value)
{
var obj = setting.CreateOption(
toggle,
slider,
Scroller.Inner,
ref contentOffset,
ref contentOrder,
contentIndex == pair.Value.Count);
obj!.GetComponentsInChildren<SpriteRenderer>(true).Do(x =>
{
x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
x.sortingOrder = 150;
x.sortingLayerName = "Default";
});
obj.GetComponentsInChildren<MeshRenderer>(true).Do(x =>
{
x.sortingOrder = 151;
x.sortingLayerName = "Default";
});
obj.GetComponentsInChildren<PassiveButton>().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; });
contentIndex++;
}
contentOrder = 1;
}
contentIndex = 1;
contentOrder = 1;
foreach (var button in Buttons)
{
var obj = button.CreateButton(
toggle,
Scroller.Inner,
ref contentOffset,
ref contentOrder,
contentIndex == Buttons.Count);
obj!.GetComponentsInChildren<SpriteRenderer>(true).Do(x =>
{
x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
x.sortingOrder = 150;
x.sortingLayerName = "Default";
});
obj.GetComponentsInChildren<MeshRenderer>(true).Do(x =>
{
x.sortingOrder = 151;
x.sortingLayerName = "Default";
});
obj.GetComponentsInChildren<PassiveButton>().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; });
contentIndex++;
}
Scroller.SetBounds(new FloatRange(0, (Settings.Count + Buttons.Count) * 0.5f - 5f), new FloatRange(0, 0));
return tab;
}
/// <summary>
/// Creates the tab button.
/// </summary>
/// <param name="instance">The <see cref="OptionsMenuBehaviour"/> instance.</param>
/// <param name="tabIdx">The tab index.</param>
/// <param name="offset">The current button offset.</param>
/// <returns>The created button <see cref="GameObject"/>.</returns>
public virtual GameObject CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset)
{
var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform);
tabButtonObject.name = $"{TabName} Button";
tabButtonObject.transform.localPosition = new Vector3(2.4f, 2.1f - offset, 5.5f);
tabButtonObject.transform.localScale = new Vector3(1.25f, 1.25f, 1);
tabButtonObject.Button.color = TabAppearance.TabButtonColor;
TabButton = tabButtonObject;
var tabButtonText = tabButtonObject.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>();
tabButtonText.GetComponent<TextTranslatorTMP>().Destroy(); // i hate text translators
tabButtonText.transform.localPosition = new Vector3(0.078f, 0, 0);
tabButtonText.transform.localScale = new Vector3(0.9f, 0.9f);
tabButtonText.alignment = TextAlignmentOptions.Right;
tabButtonText.text = $"<b>{GetShortName(TabName)}</b>";
var tabButton = tabButtonObject.GetComponent<PassiveButton>();
var rollover = tabButtonObject.Rollover;
rollover.OverColor = TabAppearance.TabButtonHoverColor;
rollover.OutColor = TabAppearance.TabButtonColor;
SpriteRenderer? tabButtonRend = null;
if (TabAppearance.TabIcon != null)
{
tabButtonRend = new GameObject("sprite").AddComponent<SpriteRenderer>();
tabButtonRend.gameObject.layer = 5; // ui layer
tabButtonRend.transform.SetParent(tabButtonObject.transform);
tabButtonRend.transform.localPosition = new Vector3(0.5f, 0, -2);
tabButtonRend.transform.localScale = new Vector3(0.2f, 0.2f, 1);
tabButtonRend.sprite = TabAppearance.TabIcon.LoadAsset();
tabButtonText.gameObject.SetActive(false);
}
var tabIndex = tabIdx; // Local copies of the tab variables so we can use them in the lambdas
var tabOffset = offset;
tabButton.OnClick.AddListener((UnityAction)(() => { instance.OpenTabGroup(tabIndex + 10); }));
tabButton.OnMouseOver.AddListener((UnityAction)(() =>
{
tabButton.transform.localPosition = new Vector3(3.55f, 2.1f - tabOffset, 5.5f);
tabButtonText.gameObject.SetActive(true);
tabButtonText.transform.localPosition = new Vector3(-0.024f, 0, 0);
tabButtonText.maxVisibleCharacters = int.MaxValue;
tabButtonText.text = TabName;
tabButtonText.alignment = TextAlignmentOptions.Left;
tabButtonRend?.gameObject.SetActive(!TabAppearance.HideIconOnHover);
}));
tabButton.OnMouseOut.AddListener((UnityAction)(() =>
{
if (!tabButtonObject.Content.gameObject.activeSelf)
{
//tabButtonObject.Button.color = TabColor;
//tabButtonObject.Rollover.OutColor = TabColor;
}
tabButton.transform.localPosition = new Vector3(2.4f, 2.1f - tabOffset, 5.5f);
tabButtonText.alignment = TextAlignmentOptions.Right;
if (tabButtonRend != null)
{
tabButtonRend.gameObject.SetActive(true);
tabButtonText.gameObject.SetActive(false);
}
tabButtonText.transform.localPosition = new Vector3(0.1f, 0, 0);
tabButtonText.maxVisibleCharacters = 4;
tabButtonText.text = $"<b>{GetShortName(TabName)}</b>";
}));
return tabButtonObject.gameObject;
}
private static void CreateLabel(GameObject template, Transform parent, string text, ref float offset)
{
var label = Object.Instantiate(template, parent);
label.transform.localPosition = new Vector3(0, 1.85f - offset);
label.GetComponent<TextTranslatorTMP>().Destroy();
label.name = text;
var tmp = label.GetComponent<TextMeshPro>();
tmp.text = $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\"><b>{text}</b></font>";
tmp.alignment = TextAlignmentOptions.Center;
var meshRend = label.GetComponent<MeshRenderer>();
meshRend.sortingLayerName = "Default";
meshRend.sortingOrder = 151;
offset += 0.5f;
}
private static string GetShortName(string name)
{
var shortName = string.Empty;
name.Split(' ').Do(x => shortName += x[0]);
return shortName;
}
}
@@ -0,0 +1,96 @@
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Struct for modifying tab appearance.
/// Defines how the settings look.
/// </summary>
public record struct LocalSettingTabAppearance()
{
/// <summary>
/// Gets or sets main color of the tab.
/// </summary>
public Color TabColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets tab button icon. No icon by default.
/// </summary>
public LoadableAsset<Sprite>? TabIcon { get; set; }
/// <summary>
/// Gets or sets color of the tab button.
/// </summary>
public Color TabButtonColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets a value indicating whether the icon should be hidden when tab button is hovered.
/// </summary>
public bool HideIconOnHover { get; set; } = true;
/// <summary>
/// Gets or sets hover color of the tab button.
/// </summary>
public Color TabButtonHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets active color of the tab button.
/// </summary>
public Color TabButtonActiveColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets hover color of toggles.
/// </summary>
public Color ToggleHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets active color of toggles.
/// </summary>
public Color ToggleActiveColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets inactive color of toggles.
/// </summary>
public Color ToggleInactiveColor { get; set; } = Color.red;
/// <summary>
/// Gets or sets color of number buttons.
/// </summary>
public Color NumberColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets hover color of number buttons.
/// </summary>
public Color NumberHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets color of sliders.
/// </summary>
public Color SliderColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets hover color of sliders.
/// </summary>
public Color SliderHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets color of enum buttons.
/// </summary>
public Color EnumColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets hover color of enum buttons.
/// </summary>
public Color EnumHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets color of buttons.
/// </summary>
public Color ButtonColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets hover color of buttons.
/// </summary>
public Color ButtonHoverColor { get; set; } = Palette.AcceptedGreen;
}
@@ -0,0 +1,19 @@
using System.Linq;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Singleton for local settings tabs.
/// </summary>
/// <typeparam name="T">The settings tab type.</typeparam>
public static class LocalSettingsTabSingleton<T> where T : LocalSettingsTab
{
private static T? _instance;
/// <summary>
/// Gets the instance of the setting tab.
/// </summary>
#pragma warning disable CA1000
public static T Instance => _instance ??= LocalSettingsManager.Tabs.OfType<T>().Single();
#pragma warning restore CA1000
}
@@ -0,0 +1,132 @@
using System;
using System.Linq;
using BepInEx.Configuration;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings.SettingTypes;
/// <summary>
/// Local setting class for enums.
/// </summary>
public class LocalEnumSetting : LocalSettingBase<int>
{
/// <summary>
/// Gets the enum type of the setting.
/// </summary>
public Type EnumType { get; }
/// <summary>
/// Gets the enum values.
/// </summary>
public string[] Values { get; }
/// <summary>
/// Initializes a new instance of the <see cref="LocalEnumSetting"/> class.
/// </summary>
/// <inheritdoc/>
/// <param name="enumType">The enum type.</param>
/// <param name="values">The optional values array to replace the enum names.</param>
public LocalEnumSetting(
Type tab,
ConfigEntryBase configEntry,
Type enumType,
string? name = null,
string? description = null,
string[]? values = null)
: base(tab, configEntry, name, description)
{
EnumType = enumType;
Values = values ?? Enum
.GetValues(configEntry.SettingType)
.Cast<Enum>()
.Select(x => x.ToDisplayString())
.ToArray();
}
/// <inheritdoc />
public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last)
{
var button = Object.Instantiate(toggle, parent).GetComponent<PassiveButton>();
var tmp = button.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>();
var rollover = button.GetComponent<ButtonRolloverHandler>();
tmp.GetComponent<TextTranslatorTMP>().Destroy();
button.gameObject.SetActive(true);
var toggleComp = button.GetComponent<ToggleButtonBehaviour>();
var background = toggleComp.Background;
var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent<SpriteRenderer>();
if (highlight != null)
{
highlight.color = Tab!.TabAppearance.EnumHoverColor;
highlight.gameObject.SetActive(false);
}
toggleComp.Destroy();
if (last && order == 1)
{
// Button in the middle
button.transform.localPosition = new Vector3(0, 1.85f - offset, -7);
}
else
{
button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7);
}
tmp.text = GetValueText();
button.name = Name;
button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
rollover.OutColor = Tab!.TabAppearance.EnumColor;
rollover.OverColor = Tab!.TabAppearance.EnumHoverColor;
background.color = Tab!.TabAppearance.EnumColor;
button.OnClick.AddListener((UnityAction)(() =>
{
int value = GetValue();
value++;
if (value >= Values.Length)
{
value = 0;
}
SetValue(value);
tmp.text = GetValueText();
}));
button.OnMouseOver.AddListener((UnityAction)(() =>
{
if (!Description.IsNullOrWhiteSpace())
{
tmp.text = Description;
}
highlight?.gameObject.SetActive(true);
}));
button.OnMouseOut.AddListener((UnityAction)(() =>
{
tmp.text = GetValueText();
highlight?.gameObject.SetActive(false);
}));
Helpers.DivideSize(button.gameObject, 1.1f);
order++;
if (order > 2 && !last)
{
offset += 0.5f;
order = 1;
}
if (last)
offset += 0.6f;
return button.gameObject;
}
/// <inheritdoc/>
protected override string GetValueText()
{
return $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\">{Name}: <b>{Values[GetValue()]}</font></b>";
}
}
@@ -0,0 +1,145 @@
using System;
using BepInEx.Configuration;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings.SettingTypes;
/// <summary>
/// Local setting class for numbers.
/// </summary>
public class LocalNumberSetting : LocalSettingBase<float>
{
/// <summary>
/// Gets the range of the button.
/// </summary>
public FloatRange NumberRange { get; }
/// <summary>
/// Gets the increment of the value when button is pressed.
/// </summary>
public float Increment { get; }
/// <summary>
/// Gets a format for the text to use to format the number.
/// </summary>
public string FormatString { get; }
/// <summary>
/// Gets the suffix for the number value.
/// </summary>
public MiraNumberSuffixes SuffixType { get; }
/// <summary>
/// Initializes a new instance of the <see cref="LocalNumberSetting"/> class.
/// </summary>
/// <inheritdoc/>
/// <param name="numberRange">The value range.</param>
/// <param name="increment">The increment per click.</param>
/// <param name="suffixType">The suffix used for formating.</param>
/// <param name="formatString">The format string used for formating.</param>
public LocalNumberSetting(
Type tab,
ConfigEntryBase configEntry,
string? name = null,
string? description = null,
FloatRange? numberRange = null,
float? increment = null,
MiraNumberSuffixes? suffixType = null,
string? formatString = null)
: base(tab, configEntry, name, description)
{
SuffixType = suffixType ?? MiraNumberSuffixes.None;
NumberRange = numberRange ?? new FloatRange(1, 5);
Increment = increment ?? 1;
FormatString = formatString ?? "0";
}
/// <inheritdoc />
public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last)
{
var button = Object.Instantiate(toggle, parent).GetComponent<PassiveButton>();
var tmp = button.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>();
var rollover = button.GetComponent<ButtonRolloverHandler>();
tmp.GetComponent<TextTranslatorTMP>().Destroy();
button.gameObject.SetActive(true);
var toggleComp = button.GetComponent<ToggleButtonBehaviour>();
var background = toggleComp.Background;
var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent<SpriteRenderer>();
if (highlight != null)
{
highlight.color = Tab!.TabAppearance.NumberHoverColor;
highlight.gameObject.SetActive(false);
}
toggleComp.Destroy();
if (last && order == 1)
{
// Button in the middle
button.transform.localPosition = new Vector3(0, 1.85f - offset, -7);
}
else
{
button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7);
}
tmp.text = GetValueText();
button.name = Name;
button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
rollover.OutColor = Tab!.TabAppearance.NumberColor;
rollover.OverColor = Tab!.TabAppearance.NumberHoverColor;
background.color = Tab!.TabAppearance.NumberColor;
button.OnClick.AddListener((UnityAction)(() =>
{
float value = GetValue();
value += Increment;
if (value > NumberRange.max)
{
value = NumberRange.min;
}
SetValue(value);
tmp.text = GetValueText();
}));
button.OnMouseOver.AddListener((UnityAction)(() =>
{
if (!Description.IsNullOrWhiteSpace())
{
tmp.text = Description;
}
highlight?.gameObject.SetActive(true);
}));
button.OnMouseOut.AddListener((UnityAction)(() =>
{
tmp.text = GetValueText();
highlight?.gameObject.SetActive(false);
}));
Helpers.DivideSize(button.gameObject, 1.1f);
order++;
if (order > 2 && !last)
{
offset += 0.5f;
order = 1;
}
if (last)
offset += 0.6f;
return button.gameObject;
}
/// <inheritdoc/>
protected override string GetValueText()
{
var value = GetValue();
var formated = Helpers.FormatValue(value, SuffixType, FormatString);
return $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\">{Name}: <b>{formated}</font></b>";
}
}
@@ -0,0 +1,76 @@
using System;
using System.Linq;
using BepInEx.Configuration;
using UnityEngine;
namespace MiraAPI.LocalSettings.SettingTypes;
/// <inheritdoc />
public abstract class LocalSettingBase<T> : ILocalSetting
{
/// <inheritdoc />
public string Name { get; }
/// <inheritdoc />
public string Description { get; }
/// <inheritdoc />
public GameObject? Setting { get; } = null!;
/// <inheritdoc />
public ConfigEntryBase ConfigEntry { get; }
/// <summary>
/// Gets the tab of the local setting.
/// </summary>
public LocalSettingsTab? Tab => LocalSettingsManager.Tabs.FirstOrDefault(x => x.Settings.Contains(this));
/// <summary>
/// Initializes a new instance of the <see cref="LocalSettingBase{T}"/> class.
/// </summary>
/// <param name="tab">The tab to create the setting in.</param>
/// <param name="configEntry">The config entry.</param>
/// <param name="name">The name of the setting.</param>
/// <param name="description">The description of the setting.</param>
protected LocalSettingBase(Type tab, ConfigEntryBase configEntry, string? name = null, string? description = null)
{
ConfigEntry = configEntry;
Name = name ?? ConfigEntry.Definition.Key;
Description = description ?? ConfigEntry.Description.Description;
LocalSettingsManager.TypeToTab[tab].Settings.Add(this);
}
/// <summary>
/// Creates an instance of the setting.
/// </summary>
/// <returns>The created setting.</returns>
public abstract GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last);
/// <summary>
/// Returns the formated string to use in the text of the setting.
/// </summary>
/// <returns>The value text.</returns>
protected virtual string GetValueText()
{
return string.Empty;
}
/// <summary>
/// Gets the value of the config entry, cast to the setting type.
/// </summary>
/// <returns>The value.</returns>
public virtual T GetValue()
{
return (T)ConfigEntry.BoxedValue;
}
/// <summary>
/// Sets the value of the config entry.
/// </summary>
/// <param name="value">The value to set to.</param>
public virtual void SetValue(T value)
{
ConfigEntry.BoxedValue = value;
Tab?.OnOptionChanged(ConfigEntry);
}
}
@@ -0,0 +1,120 @@
using System;
using BepInEx.Configuration;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings.SettingTypes;
/// <summary>
/// Local setting class for sliders.
/// </summary>
public class LocalSliderSetting : LocalSettingBase<float>
{
/// <summary>
/// Gets the range of the slider.
/// </summary>
public FloatRange SliderRange { get; }
/// <summary>
/// Gets a value indicating whether the value should be displayed next to name.
/// </summary>
public bool DisplayValue { get; }
/// <summary>
/// Gets a format for the text to use to format the number.
/// </summary>
public string FormatString { get; }
/// <summary>
/// Gets a value indicating whether the value should be rounded.
/// </summary>
public bool RoundValue { get; }
/// <summary>
/// Gets the suffix for the number value.
/// </summary>
public MiraNumberSuffixes SuffixType { get; }
/// <summary>
/// Initializes a new instance of the <see cref="LocalSliderSetting"/> class.
/// </summary>
/// <inheritdoc/>
/// <param name="sliderRange">The value range.</param>
/// <param name="suffixType">The suffix used for formating.</param>
/// <param name="formatString">The format string used for formating.</param>
/// <param name="roundValue">Should the value be rounded.</param>
public LocalSliderSetting(
Type tab,
ConfigEntryBase configEntry,
string? name = null,
string? description = null,
FloatRange? sliderRange = null,
bool displayValue = false,
MiraNumberSuffixes? suffixType = null,
string? formatString = null,
bool roundValue = false)
: base(tab, configEntry, name, description)
{
SliderRange = sliderRange ?? new FloatRange(0, 100);
DisplayValue = displayValue;
SuffixType = suffixType ?? MiraNumberSuffixes.None;
FormatString = formatString ?? "0.0";
RoundValue = roundValue;
}
/// <inheritdoc />
public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last)
{
var newSlider = Object.Instantiate(slider, parent).GetComponent<SlideBar>();
var rollover = newSlider.GetComponent<ButtonRolloverHandler>();
newSlider.Title = newSlider.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>(); // Why the hell slider has a title property that is not even assigned???
newSlider.Title.GetComponent<TextTranslatorTMP>().Destroy();
newSlider.gameObject.SetActive(true);
newSlider.Bar.color = Tab!.TabAppearance.SliderColor;
if (order == 2)
offset += 0.5f;
newSlider.Bar.transform.localPosition = new Vector3(2.85f, 0, 0);
newSlider.transform.localPosition = new Vector3(-2.12f, 1.85f - offset, -7);
newSlider.name = Name;
newSlider.Range = new FloatRange(-1.5f, 1.5f);
newSlider.SetValue(Mathf.InverseLerp(SliderRange.min, SliderRange.max, GetValue()));
rollover.OutColor = Tab!.TabAppearance.SliderColor;
rollover.OverColor = Tab!.TabAppearance.SliderHoverColor;
newSlider.Title.transform.localPosition = new Vector3(0.5f, 0, -1f);
newSlider.Title.horizontalAlignment = DisplayValue ? HorizontalAlignmentOptions.Left : HorizontalAlignmentOptions.Center;
newSlider.Title.text = GetValueText();
newSlider.OnValueChange.AddListener((UnityAction)(() =>
{
SetValue(RoundValue
? Mathf.Round(Mathf.Lerp(SliderRange.min, SliderRange.max, newSlider.Value))
: Mathf.Lerp(SliderRange.min, SliderRange.max, newSlider.Value));
newSlider.Title.text = GetValueText();
}));
order = 1;
offset += 0.5f;
return newSlider.gameObject;
}
/// <inheritdoc/>
protected override string GetValueText()
{
if (DisplayValue)
{
var value = GetValue();
var formated = Helpers.FormatValue(value, SuffixType, FormatString);
var maxFormated = Helpers.FormatValue(SliderRange.max, SuffixType, FormatString);
return $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\">{Name}: <b>{formated} / {maxFormated}</font></b>";
}
return $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\">{Name}</font></b>";
}
}
@@ -0,0 +1,88 @@
using System;
using BepInEx.Configuration;
using MiraAPI.Utilities;
using Reactor.Localization.Utilities;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings.SettingTypes;
/// <summary>
/// Local setting class for toggles.
/// </summary>
public class LocalToggleSetting : LocalSettingBase<bool>
{
/// <summary>
/// Initializes a new instance of the <see cref="LocalToggleSetting"/> class.
/// </summary>
/// <inheritdoc/>
public LocalToggleSetting(
Type tab,
ConfigEntryBase configEntry,
string? name = null,
string? description = null
) : base(tab, configEntry, name, description)
{
}
/// <inheritdoc />
public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last)
{
var toggleObject = Object.Instantiate(toggle, parent).GetComponent<ToggleButtonBehaviour>();
var tmp = toggleObject.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>();
var passiveButton = toggleObject.GetComponent<PassiveButton>();
var rollover = toggleObject.GetComponent<ButtonRolloverHandler>();
toggleObject.gameObject.SetActive(true);
if (last && order == 1)
{
// Toggle in the middle
toggleObject.transform.localPosition = new Vector3(0, 1.85f - offset, -7);
}
else
{
toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7);
}
toggleObject.BaseText = CustomStringName.CreateAndRegister(Name);
toggleObject.UpdateText(GetValue());
toggleObject.name = Name;
toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor;
passiveButton.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
rollover.OverColor = Tab!.TabAppearance.ToggleHoverColor;
passiveButton.OnClick.AddListener((UnityAction)(() =>
{
SetValue(!GetValue());
toggleObject.UpdateText(GetValue());
toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor;
}));
passiveButton.OnMouseOver.AddListener((UnityAction)(() =>
{
if (!Description.IsNullOrWhiteSpace())
{
tmp.text = Description;
}
}));
passiveButton.OnMouseOut.AddListener((UnityAction)(() =>
{
toggleObject.UpdateText(GetValue());
toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor;
}));
Helpers.DivideSize(toggleObject.gameObject, 1.1f);
order++;
if (order > 2 && !last)
{
offset += 0.5f;
order = 1;
}
if (last)
offset += 0.6f;
return toggleObject.gameObject;
}
}
+11 -8
View File
@@ -1,19 +1,13 @@
using System;
global using static Reactor.Utilities.Logger<MiraAPI.MiraApiPlugin>;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using Il2CppInterop.Runtime;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MonoMod.Utils;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
using UnityEngine.ProBuilder;
using Delegate = Il2CppSystem.Delegate;
namespace MiraAPI;
@@ -23,11 +17,20 @@ namespace MiraAPI;
[BepInAutoPlugin("mira.api", "MiraAPI")]
[BepInProcess("Among Us.exe")]
[BepInDependency(ReactorPlugin.Id)]
[BepInDependency(ModCompatibility.SubmergedId, BepInDependency.DependencyFlags.SoftDependency)]
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class MiraApiPlugin : BasePlugin
{
/// <summary>
/// Gets the branding Mira API color.
/// </summary>
public static Color MiraColor { get; } = new Color32(238, 154, 112, 255);
public static Color DefaultHeaderColor = new Color32(77, 77, 77, 255);
/// <summary>
/// Gets the default color for headers in the options menu.
/// </summary>
public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255);
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
+40
View File
@@ -0,0 +1,40 @@
using BepInEx.Configuration;
using MiraAPI.LocalSettings;
using MiraAPI.LocalSettings.Attributes;
using MiraAPI.Utilities.Assets;
namespace MiraAPI;
/// <summary>
/// Mira API Config File Handler
/// </summary>
public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
{
/// <inheritdoc />
public override string TabName => "Mira API";
/// <inheritdoc />
public override LocalSettingTabAppearance TabAppearance => new()
{
TabButtonHoverColor = MiraApiPlugin.MiraColor,
TabIcon = MiraAssets.SettingsIcon,
HideIconOnHover = false,
};
/// <summary>
/// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false);
/// <summary>
/// Gets whether to show keybinds in the control mapper.
/// </summary>
public ConfigEntry<bool> ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds in Control Mapper", true);
/// <summary>
/// Gets whether to apply cosmetic changes to the TaskAdder.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> PrettyTaskAdder { get; private set; } = config.Bind("Freeplay", "Pretty Task Laptop", true);
}
+28
View File
@@ -0,0 +1,28 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using BepInEx.Unity.IL2CPP;
namespace MiraAPI;
/// <summary>
/// Mod compatibility tools.
/// </summary>
public static class ModCompatibility
{
/// <summary>
/// The ID for the Submerged mod.
/// </summary>
public const string SubmergedId = "Submerged";
/// <summary>
/// Checks if the Submerged mod is loaded.
/// </summary>
/// <param name="submergedAssembly">Submerged mod assembly if loaded, null otherwise.</param>
/// <returns>True if the Submerged mod is loaded, false otherwise.</returns>
public static bool SubmergedLoaded([NotNullWhen(true)] out Assembly? submergedAssembly)
{
var result = IL2CPPChainloader.Instance.Plugins.TryGetValue(SubmergedId, out var plugin);
submergedAssembly = result ? plugin?.Instance.GetType().Assembly : null;
return result;
}
}
+26 -7
View File
@@ -1,6 +1,8 @@
using System;
using MiraAPI.GameOptions;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Modifiers;
@@ -38,8 +40,8 @@ public abstract class BaseModifier : IOptionable
/// Gets the parent mod of the modifier.
/// </summary>
public MiraPluginInfo ParentMod => Array.Find(
MiraPluginManager.Instance.RegisteredPlugins(),
x => x.Modifiers.Exists(y => y.TypeId == TypeId)
MiraPluginManager.Instance.RegisteredPlugins,
x => x.InternalModifiers.Exists(y => y.TypeId == TypeId)
) ?? throw new InvalidOperationException("Modifier is not registered.");
/// <summary>
@@ -48,25 +50,35 @@ public abstract class BaseModifier : IOptionable
public abstract string ModifierName { get; }
/// <summary>
/// Gets a value indicating whether the modifier is hidden on the UI.
/// Gets the modifier icon. Useless if HideOnUi is true.
/// </summary>
public virtual bool HideOnUi => false;
public virtual LoadableAsset<Sprite>? ModifierIcon => 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>
public virtual bool HideOnUi => GetDescription() == string.Empty;
/// <summary>
/// Gets a value indicating whether the modifier is shown in the freeplay menu.
/// </summary>
public virtual bool ShowInFreeplay => false;
/// <summary>
/// Gets a value indicating the color that should be used for the modifier within freeplay.
/// </summary>
public virtual Color FreeplayFileColor => Color.gray;
/// <summary>
/// Gets a value indicating whether the modifier is unique. If true, the player can only have one instance of this modifier.
/// </summary>
public virtual bool Unique => true;
/// <summary>
/// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if <see cref="HideOnUi"/> is true.
/// Gets the HUD description for this modifier. Does nothing if <see cref="HideOnUi"/> is true. Required to be visible on UI.
/// </summary>
/// <returns>The information string for the HUD.</returns>
public virtual string GetHudString() => ModifierName;
/// <returns>The description string for the HUD.</returns>
public virtual string GetDescription() => string.Empty;
/// <summary>
/// Called when the modifier is activated.
@@ -104,6 +116,13 @@ public abstract class BaseModifier : IOptionable
{
}
/// <summary>
/// Called when a meeting starts.
/// </summary>
public virtual void OnMeetingStart()
{
}
/// <summary>
/// Determines whether the player can vent.
/// </summary>
+140 -47
View File
@@ -3,13 +3,13 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.LocalSettings;
using MiraAPI.Modifiers.ModifierDisplay;
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities;
using MiraAPI.Patches.Roles;
using Reactor.Utilities;
using Reactor.Utilities.Attributes;
using TMPro;
using UnityEngine;
namespace MiraAPI.Modifiers;
@@ -26,12 +26,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public ImmutableList<BaseModifier> ActiveModifiers { get; private set; } = ImmutableList<BaseModifier>.Empty;
private ModifierDisplayComponent? ModifierDisplay { get; set; }
[HideFromIl2Cpp]
private List<BaseModifier> Modifiers { get; set; } = [];
private PlayerControl? _player;
private TextMeshPro? _modifierText;
private PlayerControl _player = null!;
private readonly List<BaseModifier> _toRemove = [];
@@ -42,9 +42,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
_toRemove.AddRange(Modifiers);
}
private void Start()
private void Awake()
{
_player = GetComponent<PlayerControl>();
if (_player == null)
{
Error("ModifierComponent could not find PlayerControl on the same GameObject.");
Destroy(this);
return;
}
ModifierExtensions.ModifierComponents.Add(_player, this);
Modifiers = [];
if (!_player.AmOwner)
@@ -52,23 +61,42 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
return;
}
_modifierText = Helpers.CreateTextLabel("ModifierText", HudManager.Instance.transform, AspectPosition.EdgeAlignments.RightTop, new Vector3(10.1f, 3.5f, 0), textAlignment: TextAlignmentOptions.Right);
_modifierText.verticalAlignment = VerticalAlignmentOptions.Top;
ModifierDisplay = ModifierDisplayComponent.CreateDisplay();
ModifierDisplay?.UpdateModifiersList(Modifiers);
}
private void FixedUpdate()
{
var removed = _toRemove.Count > 0;
foreach (var modifier in _toRemove.ToArray())
{
modifier.OnDeactivate();
_toRemove.Remove(modifier);
try
{
modifier.OnDeactivate();
}
catch (Exception e)
{
Error($"Error while deactivating modifier {modifier.ModifierName}: {e.ToString()}");
}
Modifiers.Remove(modifier);
}
var added = _toAdd.Count > 0;
foreach (var modifier in _toAdd.ToArray())
{
_toAdd.Remove(modifier);
Modifiers.Add(modifier);
modifier.Initialized = true;
modifier.OnActivate();
try
{
modifier.OnActivate();
}
catch (Exception e)
{
Error($"Error while activating modifier {modifier.ModifierName}: {e.ToString()}");
}
if (modifier is TimedModifier { AutoStart: true } timer)
{
@@ -76,21 +104,42 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
}
if (_toAdd.Count > 0 || _toRemove.Count > 0)
if (removed || added)
{
if (_player?.AmOwner == true)
if (_player.AmOwner && HudManager.InstanceExists)
{
HudManager.Instance?.SetHudActive(_player, _player.Data.Role, HudManager.Instance.TaskPanel.isActiveAndEnabled);
ModifierDisplay?.UpdateModifiersList(Modifiers);
}
_toAdd.Clear();
_toRemove.Clear();
ActiveModifiers = Modifiers.ToImmutableList();
}
foreach (var modifier in ActiveModifiers)
{
modifier.FixedUpdate();
try
{
modifier.FixedUpdate();
}
catch (Exception e)
{
Error($"Error while (fixed) updating modifier {modifier.ModifierName}: {e.ToString()}");
}
}
if (_player.AmOwner && ModifierDisplay && HudManager.InstanceExists)
{
var taskPanelOpen = HudManager.Instance.TaskPanel.open;
var roleTabOpen = HudManagerPatches.RoleTab != null && HudManagerPatches.RoleTab.open;
var leftSideHud = LocalSettingsTabSingleton<MiraApiSettings>.Instance.ModifiersHudLeftSide.Value;
if (leftSideHud && (taskPanelOpen || roleTabOpen))
{
ModifierDisplay!.gameObject.SetActive(false);
}
else
{
ModifierDisplay!.gameObject.SetActive(MeetingHud.Instance || HudManager.Instance.TaskPanel.isActiveAndEnabled);
}
}
}
@@ -98,34 +147,21 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
foreach (var modifier in ActiveModifiers)
{
modifier.Update();
}
if (!_modifierText || _player?.AmOwner == false)
{
return;
}
if (MeetingHud.Instance)
{
return;
}
var showText = false;
var builder = new StringBuilder("<b><size=130%>Modifiers:</b></size>\n");
foreach (var modifier in ActiveModifiers)
{
if (modifier.HideOnUi)
try
{
continue;
modifier.Update();
}
catch (Exception e)
{
Error($"Error while updating modifier {modifier.ModifierName}: {e.ToString()}");
}
showText = true;
builder.AppendLine(modifier.GetHudString());
}
}
_modifierText!.text = showText ? builder.ToString() : string.Empty;
private void OnDestroy()
{
// Remove this component from the global list when destroyed
ModifierExtensions.ModifierComponents.Remove(_player);
}
/// <summary>
@@ -300,7 +336,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
var modifier = modifiers.FirstOrDefault();
if (modifier is null)
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier {type.Name} because it is not active.");
Error($"Cannot remove modifier {type.Name} because it is not active.");
return;
}
@@ -316,7 +352,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
if (!ActiveModifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player.");
Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player.");
return;
}
@@ -346,7 +382,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
var modifier = GetModifier(uniqueId);
if (modifier == null)
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier with unique id {uniqueId} because it is not active.");
Error($"Cannot remove modifier with unique id {uniqueId} because it is not active.");
return;
}
@@ -376,19 +412,19 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
// TODO: Make a proper synchronization system.
if (LobbyBehaviour.Instance)
{
Logger<MiraApiPlugin>.Warning($"Modifiers added in the lobby won't sync to new players!");
Warning($"Modifiers added in the lobby won't sync to new players!");
}
var id = modifier.TypeId;
if (modifier.Unique && ActiveModifiers.Find(x => x.TypeId == id) != null)
{
Logger<MiraApiPlugin>.Error($"Player already has modifier with id {id}!");
Error($"Player already has modifier with id {id}!");
return null;
}
if (ActiveModifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Player already has this modifier!");
Error($"Player already has this modifier!");
return null;
}
@@ -492,4 +528,61 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
return ActiveModifiers.Exists(x => x.UniqueId == id);
}
/// <summary>
/// Checks if a player has an active modifier by its type.
/// </summary>
/// <param name="checkInactive">Whether to check inactive modifiers (those pending to be added).</param>
/// <param name="predicate">The predicate to check the modifier.</param>
/// <typeparam name="T">The Type of the Modifier.</typeparam>
/// <returns>True if the Modifier is present, false otherwise.</returns>
[HideFromIl2Cpp]
public bool HasModifier<T>(bool checkInactive, Func<T, bool>? predicate=null) where T : BaseModifier
{
return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
bool MatchExpr(BaseModifier bm) => bm is T modifier && (predicate == null || predicate(modifier));
}
/// <summary>
/// Checks if a player has an active modifier by its type.
/// </summary>
/// <param name="type">The modifier type.</param>
/// <param name="checkInactive">Whether to check inactive modifiers (those pending to be added).</param>
/// <param name="predicate">The predicate to check the modifier.</param>
/// <returns>True if the Modifier is present, false otherwise.</returns>
[HideFromIl2Cpp]
public bool HasModifier(Type type, bool checkInactive, Func<BaseModifier, bool>? predicate=null)
{
return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
bool MatchExpr(BaseModifier bm) => bm.GetType() == type && (predicate == null || predicate(bm));
}
/// <summary>
/// Checks if a player has an active modifier by its type ID.
/// </summary>
/// <param name="id">The modifier's type ID.</param>
/// <param name="checkInactive">Whether to check inactive modifiers (those pending to be added).</param>
/// <param name="predicate">The predicate to check the modifier.</param>
/// <returns>True if the modifier is present, false otherwise.</returns>
[HideFromIl2Cpp]
public bool HasModifier(uint id, bool checkInactive, Func<BaseModifier, bool>? predicate=null)
{
var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException(
$"Cannot get modifier with id {id} because it is not registered.");
return HasModifier(type, checkInactive, predicate);
}
/// <summary>
/// Checks if a player has an active modifier by its unique ID.
/// </summary>
/// <param name="id">The modifier's guid.</param>
/// <param name="checkInactive">Whether to check inactive modifiers (those pending to be added).</param>
/// <returns>True if the modifier is present, false otherwise.</returns>
[HideFromIl2Cpp]
public bool HasModifier(Guid id, bool checkInactive)
{
return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
bool MatchExpr(BaseModifier bm) => bm.UniqueId == id;
}
}
@@ -0,0 +1,225 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace MiraAPI.Modifiers.ModifierDisplay;
/// <summary>
/// The code used to display Mira modifiers.
/// </summary>
[RegisterInIl2Cpp]
public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
{
/// <summary>
/// Gets the instance of the Modifier Display.
/// </summary>
public static ModifierDisplayComponent? Instance { get; private set; }
private RectTransform _children = null!;
private GameObject _modTemplate = null!;
private PassiveButton _toggleButton = null!;
private TextMeshPro _toggleBtnText = null!;
private RectTransform _pagination = null!;
private TextMeshPro _pageText = null!;
private PassiveButton _nextButton = null!;
private PassiveButton _backButton = null!;
private int _currentPage;
private const int ItemsPerPage = 3;
private readonly Dictionary<BaseModifier, ModifierUiComponent> _modifiers = [];
/// <summary>
/// Gets a read only dictionary of the created modifier UI components.
/// </summary>
[HideFromIl2Cpp]
public ReadOnlyDictionary<BaseModifier, ModifierUiComponent> Modifiers => new(_modifiers);
/// <summary>
/// Gets a value indicating whether the modifier display hud is currently open.
/// </summary>
public bool IsOpen { get; private set; }
private void Awake()
{
Instance = this;
_children = transform.GetChild(0).GetComponent<RectTransform>();
_modTemplate = transform.GetChild(2).gameObject;
_toggleButton = transform.GetChild(1).GetComponent<PassiveButton>();
_toggleButton.OnClick = new Button.ButtonClickedEvent();
_toggleButton.OnClick.AddListener((UnityAction)(() =>
{
IsOpen = !IsOpen;
_children.gameObject.SetActive(!_children.gameObject.activeSelf);
}));
_toggleButton.ClickSound = HudManager.Instance.GameMenu.StreamerModeButton.GetComponent<PassiveButton>().ClickSound;
_toggleButton.HoverSound = HudManager.Instance.GameMenu.StreamerModeButton.GetComponent<PassiveButton>().HoverSound;
_toggleButton.inactiveSprites = _toggleButton.transform.GetChild(1).gameObject;
_toggleBtnText = _toggleButton.transform.GetChild(0).GetComponent<TextMeshPro>();
_pagination = _children.transform.GetChild(0).GetComponent<RectTransform>();
_pageText = _pagination.transform.GetChild(0).GetComponent<TextMeshPro>();
_nextButton = _pagination.transform.GetChild(1).GetComponent<PassiveButton>();
_backButton = _pagination.transform.GetChild(2).GetComponent<PassiveButton>();
_toggleBtnText.font = _pageText.font = _nextButton.buttonText.font = _backButton.buttonText.font = HudManager.Instance.TaskPanel.taskText.font;
_toggleBtnText.fontMaterial = _pageText.fontMaterial = _nextButton.buttonText.fontMaterial = _backButton.buttonText.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial;
_nextButton.OnClick = new Button.ButtonClickedEvent();
_nextButton.OnClick.AddListener((UnityAction)(() =>
{
_currentPage++;
UpdatePage();
}));
_backButton.OnClick = new Button.ButtonClickedEvent();
_backButton.OnClick.AddListener((UnityAction)(() =>
{
_currentPage--;
UpdatePage();
}));
IsOpen = false;
_children.gameObject.SetActive(false);
if (LocalSettingsTabSingleton<MiraApiSettings>.Instance.ModifiersHudLeftSide.Value)
{
var aspect = GetComponent<AspectPosition>();
_toggleButton.transform.localPosition = new Vector3(-1f, 2.7f, 0f);
_children.transform.localPosition = new Vector3(-0.2f, -0.05f, 0f);
aspect.Alignment = AspectPosition.EdgeAlignments.LeftTop;
aspect.DistanceFromEdge = new Vector3(1.8f, 2.55f, -20f);
aspect.AdjustPosition();
}
}
/// <summary>
/// Use this to refresh the modifiers UI. Useful if you hide modifiers uis based on options.
/// </summary>
public void RefreshModifiers()
{
UpdateModifiersList([.. PlayerControl.LocalPlayer.GetModifierComponent().ActiveModifiers]);
}
/// <summary>
/// Use this to toggle the visibility of the modifiers tab.
/// </summary>
public void ToggleTab()
{
SetOpened(!IsOpen);
}
/// <summary>
/// Use this to set the visibility of the modifiers tab.
/// </summary>
/// <param name="val">Whether it should be visible or not.</param>
public void SetOpened(bool val)
{
IsOpen = val;
_children.gameObject.SetActive(IsOpen);
RefreshModifiers();
}
internal void DestroyComponent(ModifierUiComponent component)
{
_modifiers.Remove(component.Modifier!);
component.gameObject.DestroyImmediate();
RefreshModifiers();
}
[HideFromIl2Cpp]
internal static ModifierDisplayComponent CreateDisplay()
{
var gameObject = Instantiate(MiraAssets.ModifierDisplay.LoadAsset(), HudManager.Instance.transform);
var minigame = gameObject.AddComponent<ModifierDisplayComponent>();
return minigame;
}
[HideFromIl2Cpp]
private ModifierUiComponent CreateForModifier(BaseModifier modifier)
{
var newMod = Instantiate(_modTemplate, _children.transform);
newMod.transform.name = modifier.ModifierName;
var comp = newMod.AddComponent<ModifierUiComponent>();
comp.Modifier = modifier;
return comp;
}
private void UpdatePage()
{
var totalPages = Mathf.CeilToInt((float)_modifiers.Count / ItemsPerPage);
_currentPage = Mathf.Clamp(_currentPage, 0, Mathf.Max(0, totalPages - 1));
if (_modifiers.Count <= ItemsPerPage)
{
_modifiers.Do(x => x.Value.gameObject.SetActive(true));
_pagination.gameObject.SetActive(false);
return;
}
_pagination.gameObject.SetActive(true);
if (_pageText != null)
{
_pageText.text = $"Page {_currentPage + 1}/{Mathf.Max(totalPages, 1)}";
}
_nextButton.gameObject.SetActive(true);
_backButton.gameObject.SetActive(true);
_modifiers.Do(x => x.Value.gameObject.SetActive(false));
var start = _currentPage * ItemsPerPage;
var end = Mathf.Min(start + ItemsPerPage, _modifiers.Count);
var modifiers = _modifiers.ToArray();
for (var i = start; i < end; i++)
{
modifiers[i].Value.gameObject.SetActive(true);
}
_pagination.SetAsLastSibling();
}
[HideFromIl2Cpp]
internal void UpdateModifiersList(List<BaseModifier> modifiers)
{
var filteredModifiers = modifiers.Where(x => !x.HideOnUi && x.GetDescription() != string.Empty).ToList();
var modifiersToAdd = filteredModifiers.ToList();
foreach (var mod in _modifiers.ToArray())
{
if (modifiersToAdd.Contains(mod.Key))
{
modifiersToAdd.Remove(mod.Key);
continue;
}
mod.Value.gameObject.Destroy();
_modifiers.Remove(mod.Key);
}
_toggleBtnText.text = $"Modifiers ({filteredModifiers.Count})";
_toggleButton.gameObject.SetActive(filteredModifiers.Count != 0);
foreach (var mod in modifiersToAdd)
{
_modifiers.Add(mod, CreateForModifier(mod));
}
UpdatePage();
}
}
@@ -0,0 +1,72 @@
using Il2CppInterop.Runtime.Attributes;
using Reactor.Utilities.Attributes;
using TMPro;
using UnityEngine;
namespace MiraAPI.Modifiers.ModifierDisplay;
/// <summary>
/// The code placed on every Modifier HUD object. Used to handle updating.
/// </summary>
[RegisterInIl2Cpp]
public class ModifierUiComponent(nint cppPtr) : MonoBehaviour(cppPtr)
{
/// <summary>
/// Gets the modifier which this component is for.
/// </summary>
[HideFromIl2Cpp]
public BaseModifier? Modifier { get; internal set; }
// ReSharper disable InconsistentNaming
private SpriteRenderer modBg = null!;
private TextMeshPro desc = null!;
private SpriteRenderer icon = null!;
private TextMeshPro nameText = null!;
private RectTransform descRect = null!;
// ReSharper restore InconsistentNaming
private void Awake()
{
if (Modifier == null) return;
modBg = transform.GetChild(0).GetComponent<SpriteRenderer>();
desc = modBg.transform.GetChild(0).GetComponent<TextMeshPro>();
icon = modBg.transform.GetChild(1).GetComponent<SpriteRenderer>();
var nameBg = transform.GetChild(1);
nameText = nameBg.GetChild(0).GetComponent<TextMeshPro>();
descRect = desc.gameObject.GetComponent<RectTransform>();
nameText.font = desc.font = HudManager.Instance.TaskPanel.taskText.font;
nameText.fontMaterial = desc.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial;
}
// Might not be the best solution, but used to update name/description/icon if updated in the modifier.
private void FixedUpdate()
{
if (Modifier == null || ModifierDisplayComponent.Instance == null || !ModifierDisplayComponent.Instance.IsOpen) return;
if (Modifier.HideOnUi || Modifier.GetDescription() == string.Empty)
{
ModifierDisplayComponent.Instance.DestroyComponent(this);
return;
}
nameText.text = Modifier.ModifierName;
desc.text = Modifier.GetDescription();
modBg.gameObject.SetActive(true);
icon.gameObject.SetActive(Modifier.ModifierIcon != null);
if (icon.gameObject && icon.gameObject.activeSelf && Modifier.ModifierIcon != null)
{
icon.sprite = Modifier.ModifierIcon?.LoadAsset();
icon.size = new Vector2(0.7f, 0.7f);
icon.tileMode = SpriteTileMode.Continuous;
descRect.localPosition = new Vector3(0.38f, -0.1f, -5);
descRect.sizeDelta = new Vector2(2.38f, 0.65f);
}
else
{
descRect.localPosition = new Vector3(0, -0.1f, -5);
descRect.sizeDelta = new Vector2(3.1f, 0.65f);
}
}
}
+13 -13
View File
@@ -30,7 +30,7 @@ public static class ModifierExtensions
_ = ModifierManager.GetModifierType(typeId) ?? throw new InvalidOperationException(
$"Modifier with ID {typeId} is not registered.");
Rpc<AddModifierRpc>.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args));
Rpc<AddModifierRpc>.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args), true);
}
/// <summary>
@@ -83,7 +83,7 @@ public static class ModifierExtensions
var modifier = target.GetModifier(typeId, predicate);
if (modifier is null)
{
Logger<MiraApiPlugin>.Error($"Player {target.PlayerId} does not have modifier with type ID {typeId}.");
Error($"Player {target.PlayerId} does not have modifier with type ID {typeId}.");
return;
}
@@ -180,6 +180,17 @@ public static class ModifierExtensions
return player.GetModifierComponent().HasModifier(typeId, predicate);
}
/// <summary>
/// Checks if the player has a specific modifier by its GUID.
/// </summary>
/// <param name="player">The PlayerControl instance.</param>
/// <param name="uniqueId">The unique ID of the modifier.</param>
/// <returns>True if the player has the modifier, false otherwise.</returns>
public static bool HasModifier(this PlayerControl player, Guid uniqueId)
{
return player.GetModifierComponent().HasModifier(uniqueId);
}
/// <summary>
/// Clears all modifiers from a player.
/// </summary>
@@ -204,17 +215,6 @@ public static class ModifierExtensions
}
}
/// <summary>
/// Checks if the player has a specific modifier by its GUID.
/// </summary>
/// <param name="player">The PlayerControl instance.</param>
/// <param name="uniqueId">The unique ID of the modifier.</param>
/// <returns>True if the player has the modifier, false otherwise.</returns>
public static bool HasModifier(this PlayerControl player, Guid uniqueId)
{
return player.GetModifierComponent().HasModifier(uniqueId);
}
/// <summary>
/// Tries to get a modifier by its type.
/// </summary>
+6 -4
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using MiraAPI.Utilities;
@@ -11,7 +12,7 @@ namespace MiraAPI.Modifiers;
/// </summary>
public static class ModifierFactory
{
private static readonly Dictionary<(Type, Type[]), Func<object[], BaseModifier>> _constructorCache = [];
private static readonly Dictionary<(Type, Type[]), Func<object[], BaseModifier>> ConstructorCache = [];
private static Func<object[], BaseModifier> CreateConstructor(Type type, params object[] args)
{
@@ -45,13 +46,13 @@ public static class ModifierFactory
/// <returns>An instance of the modifier.</returns>
public static BaseModifier CreateInstance(Type type, params object[] args)
{
var argTypes = args.Select(arg => arg?.GetType() ?? typeof(object)).ToArray();
var argTypes = args.Select(arg => arg.GetType()).ToArray();
var key = (type, argTypes);
if (!_constructorCache.TryGetValue(key, out var constructor))
if (!ConstructorCache.TryGetValue(key, out var constructor))
{
constructor = CreateConstructor(type, args);
_constructorCache[key] = constructor;
ConstructorCache[key] = constructor;
}
return constructor(args);
@@ -95,6 +96,7 @@ public static class ModifierFactory<T> where T : BaseModifier
/// </summary>
/// <param name="args">Parameters for the constructor.</param>
/// <returns>An instance of the modifier.</returns>
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a factory class.")]
public static T CreateInstance(params object[] args)
{
return Constructor(args);
+60 -53
View File
@@ -17,6 +17,18 @@ namespace MiraAPI.Modifiers;
/// </summary>
public static class ModifierManager
{
/// <summary>
/// Gets or sets a value indicating whether modifiers should be assigned to players.
/// </summary>
public static bool MiraAssignsModifiers { get; set; } = true;
/// <summary>
/// Gets the list of all registered modifiers.
/// </summary>
public static IReadOnlyList<BaseModifier> Modifiers { get; internal set; } = [];
internal static List<BaseModifier> InternalModifiers { get; } = [];
private static readonly Dictionary<uint, Type> IdToTypeModifierMap = [];
private static readonly Dictionary<Type, uint> TypeToIdModifierMap = [];
private static readonly Dictionary<int, List<uint>> PrioritiesToIdsMap = [];
@@ -75,7 +87,8 @@ public static class ModifierManager
modifier = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType);
}
info.Modifiers.Add(modifier);
info.InternalModifiers.Add(modifier);
InternalModifiers.Add(modifier);
if (modifier is not GameModifier gameModifier)
{
@@ -84,7 +97,7 @@ public static class ModifierManager
if (modifierType.GetConstructor(Type.EmptyTypes) == null)
{
Logger<MiraApiPlugin>.Error($"Game Modifier {modifierType.FullName} does not have a parameterless constructor!");
Error($"Game Modifier {modifierType.FullName} does not have a parameterless constructor!");
return false;
}
@@ -99,72 +112,66 @@ public static class ModifierManager
return true;
}
internal static void AssignModifiers(List<PlayerControl> plrs)
/// <summary>
/// Assigns modifiers to players. ONLY CALL THIS METHOD IF YOU KNOW WHAT YOU ARE DOING.
/// </summary>
/// <param name="plrs">The players to assign modifiers to.</param>
public static void AssignModifiers(List<PlayerControl> plrs)
{
var rand = new Random();
foreach (var priority in PrioritiesToIdsMap.Keys.OrderByDescending(x => x))
// Filter and sort modifiers by descending priority.
var modifiers = IdToTypeModifierMap
.Where(x=>x.Value.IsAssignableTo(typeof(GameModifier)))
.Select(x => Activator.CreateInstance(x.Value) as GameModifier)
.OfType<GameModifier>()
.Where(x => x.GetAmountPerGame() > 0 && x.GetAssignmentChance() > 0)
.Shuffle()
.OrderByDescending(x => x.Priority())
.ToArray();
foreach (var modifier in modifiers)
{
var filteredModifiers = new List<uint>();
var assignments = modifier.GetAmountPerGame();
foreach (var id in PrioritiesToIdsMap[priority])
var validPlayers = plrs.Where(x => IsGameModifierValid(x, modifier, modifier.TypeId)).ToList();
if (validPlayers.Count == 0)
{
var mod = Activator.CreateInstance(IdToTypeModifierMap[id]) as GameModifier;
var chance = Math.Clamp(mod!.GetAssignmentChance(), 0, 100);
var count = mod.GetAmountPerGame();
if (chance == 0 || count == 0)
{
continue;
}
var maxCount = plrs.Count(x => IsGameModifierValid(x, mod, id));
if (maxCount == 0)
{
Logger<MiraApiPlugin>.Warning($"No players are valid for {mod.ModifierName}");
continue;
}
var num = Math.Clamp(count, 0, maxCount);
for (var i = 0; i < num; i++)
{
var randomNum = rand.Next(100);
if (randomNum < chance)
{
filteredModifiers.Add(id);
}
}
}
if (filteredModifiers.Count == 0)
{
Logger<MiraApiPlugin>.Warning($"No filtered modifiers for priority {priority}");
Warning($"No valid players for modifier {modifier.ModifierName}");
continue;
}
var shuffledList = filteredModifiers.Randomize();
assignments = Math.Min(assignments, validPlayers.Count);
var availablePlayers = new List<PlayerControl>(validPlayers);
if (shuffledList.Count > plrs.Count)
for (var i = 0; i < assignments; i++)
{
shuffledList = shuffledList.GetRange(0, plrs.Count);
}
foreach (var id in shuffledList)
{
var mod = Activator.CreateInstance(IdToTypeModifierMap[id]) as GameModifier;
var plr = plrs.Where(x => IsGameModifierValid(x, mod!, id)).Random();
if (!plr || plr == null)
var chance = Math.Clamp(modifier.GetAssignmentChance(), 0, 100);
if (rand.Next(100) >= chance)
{
Logger<MiraApiPlugin>.Warning($"Somehow valid players for modifier {mod!.ModifierName} disappeared");
continue;
}
else
var candidates = availablePlayers
.Where(x => IsGameModifierValid(x, modifier, modifier.TypeId))
.ToList();
if (candidates.Count == 0)
{
plr.RpcAddModifier(id);
Warning(
$"No available players for modifier {modifier.ModifierName} at assignment {i + 1}");
break;
}
var plr = candidates.Random();
if (plr == null)
{
Warning($"Valid player for modifier {modifier.ModifierName} disappeared");
continue;
}
plr.RpcAddModifier(modifier.TypeId);
availablePlayers.Remove(plr);
}
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ public abstract class GameModifier : BaseModifier
public virtual bool IsModifierValidOn(RoleBehaviour role) => true;
/// <summary>
/// Determines whether the player won the game with this modifier.
/// Determines whether the player won the game with this modifier. The modifier with the highest priority will override other modifiers.
/// </summary>
/// <param name="reason">The reason why the game ended.</param>
/// <returns>True if the player won, false if they lost. Return null to use the player's default win condition.</returns>
+5 -5
View File
@@ -45,12 +45,12 @@ public abstract class TimedModifier : BaseModifier
public override bool HideOnUi => !TimerActive;
/// <summary>
/// Gets the HUD information for Timed Modifier, including the time remaining.
/// Gets the HUD description for Timed Modifier, including the time remaining.
/// </summary>
/// <returns>A string with the hud information + timer.</returns>
public override string GetHudString()
/// <returns>A string with the time remaining.</returns>
public override string GetDescription()
{
return base.GetHudString() + $" <size=70%>({Math.Round(Duration - TimeRemaining, 0)}s/{Duration}s)</size>";
return $"Remaining Time: {Math.Round(Duration - TimeRemaining, 0)}s/{Duration}s";
}
/// <summary>
@@ -80,7 +80,7 @@ public abstract class TimedModifier : BaseModifier
{
if (TimerActive)
{
Logger<MiraApiPlugin>.Error("Can't start a timer that has already been started.");
Error("Can't start a timer that has already been started.");
return;
}
+2 -2
View File
@@ -50,7 +50,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<
{
if (!gameOver.VerifyCondition(innerNetObject, [.. data.Winners]))
{
Logger<MiraApiPlugin>.Info($"Game over condition not met for {gameOver.GetType().Name}");
Info($"Game over condition not met for {gameOver.GetType().Name}");
return;
}
@@ -64,7 +64,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<
else
{
CustomGameOver.Instance = null;
Logger<MiraApiPlugin>.Error($"Unknown game over reason: {data.Reason}");
Error($"Unknown game over reason: {data.Reason}");
}
}
}
+30 -12
View File
@@ -1,5 +1,7 @@
using System.Collections;
using System;
using System.Collections;
using System.Linq;
using AmongUs.Data;
using AmongUs.GameOptions;
using Assets.CoreScripts;
using BepInEx.Unity.IL2CPP.Utils;
@@ -7,8 +9,10 @@ using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Networking;
@@ -28,7 +32,7 @@ public static class CustomMurderRpc
/// <param name="teleportMurderer">Should the killer be snapped to the dead player.</param>
/// <param name="showKillAnim">Should the kill animation be shown.</param>
/// <param name="playKillSound">Should the kill sound be played.</param>
[MethodRpc((uint)MiraRpc.CustomMurder, LocalHandling = RpcLocalHandling.Before, SendImmediately = true)]
[MethodRpc((uint)MiraRpc.CustomMurder, LocalHandling = RpcLocalHandling.Before)]
public static void RpcCustomMurder(
this PlayerControl source,
PlayerControl target,
@@ -96,7 +100,7 @@ public static class CustomMurderRpc
var flag = PlayerControl.LocalPlayer.Data.Role.Role == RoleTypes.GuardianAngel;
if (flag && PlayerControl.LocalPlayer.Data.PlayerId == target.protectedByGuardianId)
{
StatsManager.Instance.IncrementStat(StringNames.StatsGuardianAngelCrewmatesProtected);
DataManager.Player.Stats.IncrementStat(StatID.Role_GuardianAngel_CrewmatesProtected);
AchievementManager.Instance.OnProtectACrewmate();
}
@@ -127,14 +131,14 @@ public static class CustomMurderRpc
DebugAnalytics.Instance.Analytics.Kill(target.Data, source.Data);
if (source.AmOwner)
{
StatsManager.Instance.IncrementStat(
DataManager.Player.Stats.IncrementStat(
GameManager.Instance.IsHideAndSeek()
? StringNames.StatsImpostorKills_HideAndSeek
: StringNames.StatsImpostorKills);
? StatID.HideAndSeek_ImpostorKills
: StatID.ImpostorKills);
if (source.CurrentOutfitType == PlayerOutfitType.Shapeshifted)
{
StatsManager.Instance.IncrementStat(StringNames.StatsShapeshifterShiftedKills);
DataManager.Player.Stats.IncrementStat(StatID.Role_Shapeshifter_ShiftedKills);
}
if (Constants.ShouldPlaySfx() && playKillSound)
@@ -152,7 +156,7 @@ public static class CustomMurderRpc
target.gameObject.layer = LayerMask.NameToLayer("Ghost");
if (target.AmOwner)
{
StatsManager.Instance.IncrementStat(StringNames.StatsTimesMurdered);
DataManager.Player.Stats.IncrementStat(StatID.TimesMurdered);
if (Minigame.Instance)
{
try
@@ -168,7 +172,14 @@ public static class CustomMurderRpc
if (showKillAnim)
{
HudManager.Instance.KillOverlay.ShowKillAnimation(source.Data, data);
try
{
HudManager.Instance.KillOverlay.ShowKillAnimation(source.Data, data);
}
catch (Exception e)
{
Error($"Error with kill animation: {e.ToString()}");
}
}
target.cosmetics.SetNameMask(false);
@@ -201,7 +212,7 @@ public static class CustomMurderRpc
bool teleportMurderer = true)
{
var cam = Camera.main?.GetComponent<FollowerCamera>();
var isParticipant = PlayerControl.LocalPlayer == source || PlayerControl.LocalPlayer == target;
var isParticipant = source.AmOwner || target.AmOwner;
var sourcePhys = source.MyPhysics;
if (teleportMurderer)
@@ -221,7 +232,7 @@ public static class CustomMurderRpc
if (createDeadBody)
{
deadBody = Object.Instantiate(GameManager.Instance.DeadBodyPrefab);
deadBody = Object.Instantiate(GameManager.Instance.GetDeadBody(source.Data.Role));
deadBody.enabled = false;
deadBody.ParentId = target.PlayerId;
deadBody.bodyRenderers.ToList().ForEach(target.SetPlayerMaterialColors);
@@ -232,6 +243,13 @@ public static class CustomMurderRpc
deadBody.transform.position = vector;
}
source.Data.Role.KillAnimSpecialSetup(deadBody, source, target);
target.Data.Role.KillAnimSpecialSetup(deadBody, source, target);
// no idea if this causes bugs, but innersloth is brain-dead
// I HATE INNERSCUFF I HATE INNERSCUFF I HATE INNERSCUFF I HATE INNERSCUFF I HATE INNERSCUFF I HATE INNERSCUFF
PlayerControl.LocalPlayer.Data.Role.KillAnimSpecialSetup(deadBody, source, target);
if (isParticipant)
{
if (cam != null)
@@ -263,7 +281,7 @@ public static class CustomMurderRpc
deadBody.enabled = true;
}
var afterMurderEvent = new AfterMurderEvent(source, target);
var afterMurderEvent = new AfterMurderEvent(source, target, deadBody);
MiraEventManager.InvokeEvent(afterMurderEvent);
if (!isParticipant)
+5
View File
@@ -44,4 +44,9 @@ public enum MiraRpc : uint
/// Custom RPC to undo a player's vote.
/// </summary>
RemoveVote,
/// <summary>
/// Custom RPC to replace Among Us CastVote.
/// </summary>
CastVote,
}
+5
View File
@@ -0,0 +1,5 @@
[*.{cs,vb}]
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CS1591.severity = none
dotnet_diagnostic.SA1611.severity = none
dotnet_diagnostic.SA1615.severity = none
Loaded 100 of 198 files, more files were not shown because too many files have changed in this diff. Show more