Compare commits

...
109 Commits
Author SHA1 Message Date
ang-xd 34b89c5c46 WIP Anim API 2024-09-04 20:07:56 -04:00
ang-xd fa8fdfc4f6 Create ChameleonRole.cs 2024-09-04 17:30:08 -04:00
XtraCube a442f54b82 Merge pull request #12 from Rubyboat1207/audio-resource
Added Loadable Audio Resource Asset
2024-09-03 21:29:50 -04:00
Rubyboat 24dca63df1 File name more descriptive 2024-09-03 20:17:50 -05:00
XtraCube 08a4dc75f2 fix period oops 2024-09-03 21:05:55 -04:00
XtraCube 4e474dc5b2 formatting 2024-09-03 21:05:32 -04:00
XtraCube 3deffb701f Merge branch 'refs/heads/dev' into fork/Rubyboat1207/audio-resource 2024-09-03 21:02:00 -04:00
XtraCube f83dce99ef add group priorities for ordering groups 2024-09-03 20:59:26 -04:00
XtraCube 9c14db0321 slight fix to options Click To Close text 2024-09-03 18:39:03 -04:00
Rubyboat f92d08b825 Added Loadable Audio Resource Asset 2024-09-02 15:37:53 -05:00
XtraCube ae10917f2a hidefromil2cpp 2024-09-02 15:22:56 -04:00
XtraCube 51d08bb168 hide mod in TaskAdder if it has no roles 2024-09-02 15:22:50 -04:00
XtraCube 0e56a30c5c fix most non-documentation warnings 2024-09-02 15:19:32 -04:00
XtraCube 74055b8a17 various code alterations to patches to fix warnings 2024-09-02 14:11:10 -04:00
XtraCube 812cef5f2b fix loadable asset code docs and rework logic slightly 2024-09-02 14:10:44 -04:00
XtraCube 9665188a18 various adjustments to code style 2024-09-02 13:46:10 -04:00
XtraCube cb943c2173 CanKill -> UseVanillaKillButton
ICustomRole changes to clear up confusion
Add KillButtonOutlineColor to ICustomRole
2024-09-02 13:37:45 -04:00
XtraCube dc483adc1c more comments + nullable stuff 2024-09-02 13:04:20 -04:00
XtraCube 3d9bbc0833 comments 2024-09-02 13:01:34 -04:00
XtraCube b032e91e08 using statements 2024-09-02 12:57:32 -04:00
XtraCube 3cada4cb3d fix problem with TimedModifier using RPC on every client, despite being replicated 2024-09-02 12:57:25 -04:00
XtraCube 97ded2fc32 some updates to example mod 2024-09-02 12:53:08 -04:00
XtraCube f207849e5e fix neutral killing issues and rename some properties in ICustomRole to clear confusion 2024-09-02 00:12:50 -04:00
XtraCube f8bb63483f fixing possible issues with ghost roles 2024-09-01 20:54:35 -04:00
XtraCube a3dd4acf43 code document custom murder RPCs 2024-09-01 16:29:42 -04:00
XtraCube f17089f435 split HudManager patches 2024-09-01 16:14:30 -04:00
XtraCube d1c469746e fix poor documentation in ICustomRole 2024-09-01 16:08:18 -04:00
XtraCube b053684235 formatting 2024-09-01 16:08:01 -04:00
XtraCube 0f26cadd9a clean up OptionGroupSingleton.cs 2024-09-01 16:07:36 -04:00
XtraCube a07c332dce add DeathReason to modifier OnDeath method 2024-09-01 16:07:22 -04:00
XtraCube 74ab8c5581 oops 2024-09-01 16:06:41 -04:00
XtraCube aa72770bfc change vent color for custom roles 2024-09-01 16:06:35 -04:00
XtraCube 2b71438cd0 fix button reset after meeting 2024-09-01 15:53:33 -04:00
XtraCube e22032e1fb fix neutral naming from outcast 2024-09-01 15:53:09 -04:00
XtraCube bdb8852347 FIX NUGET CI PRAYING RN 2024-08-31 21:45:01 -04:00
XtraCube 4775bbfeaa precautions for the nuget stuff 2024-08-31 21:42:20 -04:00
XtraCube 0aa164de99 Merge branch 'refs/heads/dev' 2024-08-31 21:39:53 -04:00
XtraCube 5ec91813b7 bump version 2024-08-31 21:35:36 -04:00
XtraCube e2cabc6782 fix skidded CI 2024-08-31 21:35:12 -04:00
XtraCube 126df24a23 HideFromIl2Cpp and slight modification to TimedModifier to use new HudString method 2024-08-31 21:33:32 -04:00
XtraCube 8178967fd8 fix issue #11 2024-08-31 20:43:45 -04:00
XtraCube 0e40526d26 forgor to implement hud string thingy for modifiers 2024-08-31 17:41:12 -04:00
XtraCube 7bb9733513 update modifiers in fixedupdate instead of in add/removemodifier methods 2024-08-31 17:33:03 -04:00
XtraCube 1c97a4d32a try fixing null ref with roletab 2024-08-31 17:32:40 -04:00
XtraCube 1f357b5e08 add hud string method and code docs for modifiers 2024-08-31 16:25:50 -04:00
XtraCube 264defe8bf target should have protected setter, not private 2024-08-31 15:32:45 -04:00
XtraCube c318c447f7 namespace issue 2024-08-31 15:31:50 -04:00
XtraCube 06c59c5103 showcase mira stuff 2024-08-31 15:31:41 -04:00
XtraCube 8fe58ca6ca reset buttons on meeting start 2024-08-31 15:12:39 -04:00
XtraCube 1d92404b0e possible fix to weird errors?? 2024-08-31 15:12:27 -04:00
XtraCube 0429cd8005 remove ModdedStringOption.cs, improve code in Options API 2024-08-31 10:41:16 -04:00
XtraCube dc5bb4177f fix comments 2024-08-31 06:32:54 -04:00
XtraCube 5a95a6b352 finally fix custom colors, stupid among us and no errors for somr reason supe rweird 2024-08-31 00:56:32 -04:00
XtraCube 7999506359 change when colors are registered 2024-08-31 00:29:47 -04:00
XtraCube 0b36939858 fix pls???? 2024-08-31 00:24:21 -04:00
XtraCube 09ce9f7cfb possible fix for getplayermodifiers? 2024-08-31 00:12:12 -04:00
XtraCube aba4b30027 fix getclosestplayer errors 2024-08-31 00:10:04 -04:00
XtraCube 409113e37a WIP code documentation and various fixes 2024-08-30 23:52:43 -04:00
XtraCube a52f46972e make Role Tab optional 2024-08-30 12:28:02 -04:00
XtraCube 9f975c8263 half finished docs for cusotm action button 2024-08-30 11:59:02 -04:00
XtraCube e083c0c420 fix null ref in getclosestplayers 2024-08-30 11:44:15 -04:00
XtraCube 592e636903 document Extensions.cs 2024-08-30 11:25:15 -04:00
XtraCube b139fbe51d Add docs to ICustomRole, remove GetNetData from ICustomRole to prevent overriding it 2024-08-30 09:14:58 -04:00
XtraCube ce87fc4705 fix possible bug related to neutral role ghosts if devs dont specify exactly 2024-08-29 22:03:29 -04:00
XtraCube f23d21d896 remove useless property from ICustomRole 2024-08-29 21:51:28 -04:00
XtraCube 84651b90ea precautions for role configs 2024-08-29 21:36:14 -04:00
XtraCube 1428c629b8 document gamemodes 2024-08-29 20:34:12 -04:00
XtraCube feb73d6b07 document colors 2024-08-29 20:34:06 -04:00
XtraCube 54717c495f rework config definition setup + some docs 2024-08-29 20:16:52 -04:00
XtraCube febfda7d6f sonar analyzer 2024-08-29 20:08:31 -04:00
XtraCube 913377e4df Merge remote-tracking branch 'origin/dev' into dev 2024-08-29 19:48:55 -04:00
XtraCube 86893a1e9e update gitignore 2024-08-29 18:08:26 -04:00
Ang 56c2d9c0df Merge pull request #9 from All-Of-Us-Mods/dev
0.1.3 Changes
2024-08-29 16:51:36 -04:00
ang-xd ab5f997fb6 Bump ver 2024-08-29 16:51:24 -04:00
Ang 869d4970fd Merge pull request #8 from All-Of-Us-Mods/stylecop-and-new-build
Stylecop
2024-08-29 16:47:13 -04:00
ang-xd 2cbbcca161 oops 2024-08-29 16:46:38 -04:00
ang-xd 2f09baa371 Bug fixes and new teleport button 2024-08-29 16:31:07 -04:00
ang-xd acf1d44b52 Changes 2024-08-29 15:47:54 -04:00
ang-xd 03ef684691 New 2024-08-28 20:50:34 -04:00
XtraCube eafc740aca new CI, build, and stylecop skidded from reactor :p 2024-08-28 20:40:56 -04:00
ang-xd 7af85d9fbb Implement custom murders & extension method 2024-08-28 15:18:36 -04:00
Ang bdce8fa37d Update README.md 2024-08-28 15:17:56 -04:00
ang-xd 9fc5044085 Merge branch 'master' into dev 2024-08-28 14:08:12 -04:00
XtraCube 7cb103f318 save config file once when handling netdata 2024-08-28 13:45:32 -04:00
XtraCube dd8d3df2b0 refactor access and management methods for modifiers 2024-08-28 12:39:05 -04:00
XtraCube 19c7e38690 what the freak 2024-08-27 21:56:33 -04:00
XtraCube f1639a784d Merge branch 'refs/heads/dev' 2024-08-27 21:52:41 -04:00
XtraCube 5f1996b457 precautions 2024-08-27 21:49:53 -04:00
XtraCube 33d389e107 fix modifiers logic 2024-08-27 21:46:45 -04:00
XtraCube 176d472cd7 nullable 2024-08-27 21:46:36 -04:00
XtraCube 7a3406e0ae XD 2024-08-27 21:46:26 -04:00
XtraCube 9f92af8958 only show role setting button if there is settings for it 2024-08-27 21:46:21 -04:00
Ang 8d167596d2 Update README.md 2024-08-27 21:29:29 -04:00
XtraCube a98a37bea7 fix modifier manager logic? 2024-08-27 21:14:53 -04:00
ang-xd 4963c80395 Fixes and new stuff 2024-08-27 20:56:53 -04:00
XtraCube 4bef2f838d can use fix 2024-08-27 20:46:15 -04:00
XtraCube 763017b6e2 make GetTarget abstract instead of virtual 2024-08-27 20:44:37 -04:00
XtraCube 16fd8e02aa fix bug with advanced roles settings 2024-08-27 20:44:25 -04:00
XtraCube f187a15f2e more documentaion stuff 2024-08-27 20:23:53 -04:00
XtraCube e211c4c49c rename ResetCooldown to ResetCooldownAndOrEffect, add warning in doc 2024-08-27 20:21:06 -04:00
XtraCube 7f532cd82f Merge remote-tracking branch 'origin/master' 2024-08-27 20:11:43 -04:00
XtraCube 7a0396fe5c Make lobby view pane sync on options change 2024-08-27 20:11:36 -04:00
ang-xd d4955b5183 Merge branch 'master' into dev 2024-08-27 20:10:37 -04:00
ang-xd 0a5c59d9cd somewhat fix notifications on game start 2024-08-27 20:10:01 -04:00
ang-xd 774e1a7a2b Modifiers bug fixed 2024-08-27 19:54:33 -04:00
ang-xd 8e6f75cfbd WIP 2024-08-27 19:43:38 -04:00
XtraCube b36b434a99 oversight in the code 2024-08-27 19:39:36 -04:00
XtraCube 858e518a57 for sake of versioning ig 2024-08-27 19:34:31 -04:00
XtraCube b9734f5a1c forgot to bump version what an idiot 2024-08-27 19:31:42 -04:00
131 changed files with 4391 additions and 1832 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
[*.{cs,vb}]
dotnet_diagnostic.CA1707.severity = none
+28 -3
View File
@@ -1,19 +1,28 @@
name: CI
on: workflow_dispatch
on: ["workflow_dispatch", "push", "pull_request"]
jobs:
build:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
submodules: true
- uses: actions/cache@v3
with:
path: |
~/.nuget/packages
~/.cache/bepinex
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.x
dotnet-version: 8.0.x
- name: Run the Cake script
uses: cake-build/cake-action@v2
@@ -24,3 +33,19 @@ jobs:
with:
name: MiraAPI.dll
path: MiraAPI/bin/Release/net6.0/MiraAPI.dll
- uses: actions/upload-artifact@v3
with:
name: AllOfUs.MiraAPI.nupkg
path: MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg
- uses: softprops/action-gh-release@v1
if: github.ref_type == 'tag'
with:
draft: true
files: MiraAPI/bin/Release/net6.0/MiraAPI.dll
- name: Push NuGet package
if: github.ref_type == 'tag'
run: |
dotnet nuget push MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
+2 -1
View File
@@ -4,4 +4,5 @@
obj/
bin/
*.user
*.user
*.lock.json
+14
View File
@@ -0,0 +1,14 @@
<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.8.13" 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" />
</ItemGroup>
<Target Name="Copy" AfterTargets="Build" Condition="'$(AmongUs)' != ''">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(AmongUs)/BepInEx/plugins/" UseSymboliclinksIfPossible="true" />
</Target>
</Project>
+45
View File
@@ -0,0 +1,45 @@
<Project>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<DebugType>embedded</DebugType>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.1.4</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>All Of Us, XtraCube, Angxl</Authors>
<Company>All Of Us</Company>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/All-Of-Us-Mods/MiraAPI</RepositoryUrl>
<PackageLicenseExpression>LGPL-2.1-or-later</PackageLicenseExpression>
<!-- SourceLink -->
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>recommended</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
<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">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)\stylecop.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>
-29
View File
@@ -1,29 +0,0 @@
using MiraAPI.Example.Modifiers;
using MiraAPI.Hud;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Example.Buttons;
[RegisterButton]
public class ExampleButton : CustomActionButton
{
public override string Name => "Example Button";
public override float Cooldown => 5f;
public override float EffectDuration => 0f;
public override int MaxUses => 5;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override ButtonLocation Location => ButtonLocation.BottomRight;
protected override void OnClick()
{
Logger<ExamplePlugin>.Info("Example button clicked!");
}
public override bool Enabled(RoleBehaviour role)
{
return PlayerControl.LocalPlayer.HasModifier<GameModifierExample>();
}
}
@@ -1,40 +0,0 @@
using MiraAPI.Hud;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Buttons;
public class ExampleTargetButton : CustomActionButton<PlayerControl>
{
public override string Name => "Shoot";
public override float Cooldown => 0f;
public override float EffectDuration => 0f;
public override int MaxUses => 0;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
protected override void OnClick()
{
PlayerControl.LocalPlayer.CmdCheckMurder(Target);
}
public override PlayerControl GetTarget()
{
return PlayerControl.LocalPlayer.Data.Role.FindClosestTarget();
}
public override void SetOutline(bool active)
{
Target?.cosmetics.SetOutline(active, new Il2CppSystem.Nullable<Color>(Color.red));
}
public override bool IsTargetValid(PlayerControl target)
{
return PlayerControl.LocalPlayer.Data.Role.IsValidTarget(target.Data);
}
public override bool Enabled(RoleBehaviour role)
{
return role.IsImpostor;
}
}
@@ -0,0 +1,45 @@
using MiraAPI.Example.Modifiers.Freezer;
using MiraAPI.Example.Options.Roles;
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Buttons.Freezer;
[RegisterButton]
public class FreezeButton : CustomActionButton<PlayerControl>
{
public override string Name => "Freeze";
public override float Cooldown => OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeDuration;
public override float EffectDuration => 0f;
public override int MaxUses => (int)OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeUses;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
protected override void OnClick()
{
Target?.RpcAddModifier<FreezeModifier>();
}
public override PlayerControl? GetTarget()
{
return PlayerControl.LocalPlayer.GetClosestPlayer(true, Distance);
}
public override void SetOutline(bool active)
{
Target?.cosmetics.SetOutline(active, new Il2CppSystem.Nullable<Color>(Palette.Blue));
}
public override bool IsTargetValid(PlayerControl? target)
{
return true;
}
public override bool Enabled(RoleBehaviour? role)
{
return role is FreezerRole;
}
}
+48
View File
@@ -0,0 +1,48 @@
using MiraAPI.Example.Modifiers;
using MiraAPI.Hud;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Buttons;
[RegisterButton]
public class MeetingButton : CustomActionButton
{
public override string Name => "Call Meeting";
public override float Cooldown => 15;
public override float EffectDuration => 0;
public override int MaxUses => 3;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override bool Enabled(RoleBehaviour? role)
{
return PlayerControl.LocalPlayer.HasModifier<CaptainModifier>();
}
protected override void OnClick()
{
var bt = ShipStatus.Instance.EmergencyButton;
PlayerControl.LocalPlayer.NetTransform.Halt();
var minigame = Object.Instantiate(bt.MinigamePrefab, Camera.main!.transform, false);
var taskAdderGame = minigame as TaskAdderGame;
if (taskAdderGame != null)
{
taskAdderGame.SafePositionWorld = bt.SafePositionLocal + (Vector2)bt.transform.position;
}
minigame.transform.localPosition = new Vector3(0f, 0f, -50f);
minigame.Begin(null);
if (UsesLeft == 0 && PlayerControl.LocalPlayer.HasModifier<CaptainModifier>())
{
PlayerControl.LocalPlayer.RpcRemoveModifier<CaptainModifier>();
}
}
}
@@ -0,0 +1,86 @@
using MiraAPI.Example.Options.Roles;
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using System.Collections;
using UnityEngine;
namespace MiraAPI.Example.Buttons.Teleporter;
[RegisterButton]
public class TeleportButton : CustomActionButton
{
public override string Name => "Teleport";
public override float Cooldown => OptionGroupSingleton<TeleporterOptions>.Instance.TeleportCooldown.Value;
public override float EffectDuration => OptionGroupSingleton<TeleporterOptions>.Instance.TeleportDuration;
public override int MaxUses => 0;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.TeleportButton;
public static bool IsZoom { get; private set; }
public override bool Enabled(RoleBehaviour? role)
{
return role is TeleporterRole;
}
protected override void OnClick()
{
Coroutines.Start(ZoomOutCoroutine());
}
public override void OnEffectEnd()
{
Coroutines.Start(ZoomInCoroutine());
}
protected override void FixedUpdate(PlayerControl playerControl)
{
base.FixedUpdate(playerControl);
if (!EffectActive) return;
if (Input.GetKey(KeyCode.Mouse0))
{
playerControl.NetTransform.RpcSnapTo(Camera.main.ScreenToWorldPoint(Input.mousePosition));
ResetCooldownAndOrEffect();
}
}
private static IEnumerator ZoomOutCoroutine()
{
HudManager.Instance.ShadowQuad.gameObject.SetActive(false);
IsZoom = true;
var zoomDistance = OptionGroupSingleton<TeleporterOptions>.Instance.ZoomDistance;
for (var ft = Camera.main!.orthographicSize; ft < zoomDistance; ft += 0.3f)
{
Camera.main.orthographicSize = MeetingHud.Instance ? 3f : ft;
ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen);
foreach (var cam in Camera.allCameras) cam.orthographicSize = Camera.main.orthographicSize;
yield return null;
}
foreach (var cam in Camera.allCameras) cam.orthographicSize = zoomDistance;
ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen);
}
private static IEnumerator ZoomInCoroutine()
{
for (var ft = Camera.main!.orthographicSize; ft > 3f; ft -= 0.3f)
{
Camera.main.orthographicSize = MeetingHud.Instance ? 3f : ft;
ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen);
foreach (var cam in Camera.allCameras) cam.orthographicSize = Camera.main.orthographicSize;
yield return null;
}
foreach (var cam in Camera.allCameras) cam.orthographicSize = 3f;
HudManager.Instance.ShadowQuad.gameObject.SetActive(true);
IsZoom = false;
ResolutionManager.ResolutionChanged.Invoke((float)Screen.width / Screen.height, Screen.width, Screen.height, Screen.fullScreen);
}
}
+4 -1
View File
@@ -5,5 +5,8 @@ namespace MiraAPI.Example;
public static class ExampleAssets
{
public static LoadableResourceAsset ExampleButton { get; } = new("MiraAPI.Example.Resources.ExampleButton.png");
// Credit to EpicHorrors for the teleport button asset.
public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png");
public static LoadableResourceAsset Banner { get; } = new("MiraAPI.Example.Resources.FortniteBanner.jpeg");
}
}
+3 -3
View File
@@ -6,9 +6,9 @@ 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));
public static CustomColor Rose { get; } = new("Rose", new Color(0.98f, 0.26f, 0.62f));
public static CustomColor Gold { get; } = new("Gold", new Color(1.0f, 0.84f, 0.0f));
}
}
+1 -2
View File
@@ -22,6 +22,5 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin
public override void Load()
{
Harmony.PatchAll();
AddComponent<MiraDebugWindow>();
}
}
}
+4 -21
View File
@@ -1,25 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<DebugType>embedded</DebugType>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Version>1.0.0</Version>
<Description>Example mod for Mira API</Description>
<Authors>Angxl, XtraCube</Authors>
</PropertyGroup>
<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.8.13" PrivateAssets="all" />
<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MiraAPI\MiraAPI.csproj" />
@@ -28,9 +12,8 @@
<ItemGroup>
<EmbeddedResource Include="Resources\ExampleButton.png" />
<EmbeddedResource Include="Resources\FortniteBanner.jpeg" />
<EmbeddedResource Include="Resources\TeleportButton.png" />
</ItemGroup>
<Target Name="Copy" AfterTargets="Build" Condition="'$(AmongUs)' != ''">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(AmongUs)/BepInEx/plugins/" UseSymboliclinksIfPossible="true" />
</Target>
<Import Project="..\AmongUs.props" />
</Project>
+12 -16
View File
@@ -1,31 +1,27 @@
using MiraAPI.Utilities;
using System;
using Il2CppInterop.Runtime.Attributes;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.ImGui;
using System;
using MiraAPI.Example.Modifiers;
using UnityEngine;
namespace MiraAPI.Example;
[RegisterInIl2Cpp]
public class MiraDebugWindow(IntPtr ptr) : MonoBehaviour(ptr)
public class MiraDebugWindow(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
public readonly DragWindow DebuggingWindow = new(new Rect(10, 10, 0, 0), "MIRA API DEBUGGING", () =>
{
if (GUILayout.Button("Test modifier"))
[HideFromIl2Cpp]
public DragWindow DebuggingWindow { get; } = new(
new Rect(10, 10, 0, 0),
"MIRA API DEBUGGING",
() =>
{
PlayerControl.LocalPlayer.AddModifier<ModifierTimerExample>();
}
if (GUILayout.Button("Remove modifier"))
{
PlayerControl.LocalPlayer.RemoveModifier<ModifierTimerExample>();
}
})
})
{
Enabled = true
Enabled = true,
};
public void OnGUI()
{
DebuggingWindow.OnGUI();
}
}
}
@@ -4,14 +4,9 @@ using MiraAPI.Modifiers.Types;
namespace MiraAPI.Example.Modifiers;
[RegisterModifier]
public class GameModifierExample : GameModifier
public class CaptainModifier : GameModifier
{
public override string ModifierName => "Example Game Modifier";
public override bool CanVent()
{
return true;
}
public override string ModifierName => "Captain";
public override int GetAmountPerGame()
{
@@ -22,4 +17,4 @@ public class GameModifierExample : GameModifier
{
return 100;
}
}
}
@@ -0,0 +1,43 @@
using Il2CppSystem;
using MiraAPI.Example.Roles;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
using UnityEngine;
namespace MiraAPI.Example.Modifiers.Freezer;
[RegisterModifier]
public class FreezeModifier : TimedModifier
{
public override string ModifierName => "Freezed";
public override bool HideOnUi => false;
public override float Duration => 15f;
public override bool AutoStart => true;
public override bool RemoveOnComplete => true;
public override void OnActivate()
{
if (Player?.AmOwner == true)
{
Player.moveable = false;
}
}
public override void FixedUpdate()
{
base.FixedUpdate();
if (Player?.AmOwner == true || PlayerControl.LocalPlayer.Data.Role is FreezerRole)
{
Player?.cosmetics.SetOutline(true, new Nullable<Color>(Palette.LightBlue));
}
}
public override void OnTimerComplete()
{
if (Player?.AmOwner == true)
{
Player.moveable = true;
}
}
}
@@ -1,21 +0,0 @@
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
namespace MiraAPI.Example.Modifiers;
[RegisterModifier]
public class ModifierTimerExample : TimedModifier
{
public override string ModifierName => "Color Changer";
public override bool HideOnUi => true;
public override float Duration => 10f;
public override bool AutoStart => true;
private readonly System.Random _rand = new();
public override void OnTimerComplete()
{
Player.SetColor((byte)_rand.Next(0, Palette.PlayerColors.Count));
StartTimer();
}
}
+7 -10
View File
@@ -19,16 +19,13 @@ public class ExampleOptions : AbstractOptionGroup
[ModdedNumberOption("Number Opt", min: 0, max: 10, suffixType: MiraNumberSuffixes.Percent)]
public float NumberOpt { get; set; } = 4f;
[ModdedStringOption("String Opt", ["Hello", "Hey", "Bye"])]
public int StringOpt { get; set; } = 2;
[ModdedEnumOption("Enum Opt", typeof(TestEnum))]
public TestEnum EnumOpt { get; set; } = TestEnum.Mira;
[ModdedEnumOption("Best API", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])]
public BestApi Opt { get; set; } = BestApi.MiraAPI;
}
public enum TestEnum
public enum BestApi
{
Welcome,
To,
Mira
}
MiraAPI,
Mitochondria,
Reactor,
}
+11 -7
View File
@@ -1,5 +1,6 @@
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.OptionTypes;
using Reactor.Utilities;
namespace MiraAPI.Example.Options;
@@ -7,21 +8,24 @@ public class ExampleOptions2 : AbstractOptionGroup
{
public override string GroupName => "Example Options 2";
public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue.
public ModdedToggleOption ToggleOpt1 { get; } = new("Toggle Option 1", false);
public ModdedToggleOption ToggleOpt2 { get; } = new("Toggle Option 2", false)
{
Visible = () => OptionGroupSingleton<ExampleOptions2>.Instance.ToggleOpt1.Value
Visible = () => OptionGroupSingleton<ExampleOptions2>.Instance.ToggleOpt1.Value,
};
public ModdedStringOption StringOpt { get; } = new("String Opt", 0, ["Choice 1", "Choice 2", "Choice 3"]);
public ModdedEnumOption EnumOpt { get; } = new("Enum Opt", 0, typeof(TestingEnum));
public ModdedEnumOption EnumOpt { get; } = new("Enum Opt", 0, typeof(TestingData))
{
ChangedEvent = x => Logger<ExamplePlugin>.Info($"changed Enum Opt to {x}"),
};
}
public enum TestingEnum
public enum TestingData
{
Happy,
Sad,
Neutral
}
Neutral,
}
@@ -1,16 +0,0 @@
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using System;
namespace MiraAPI.Example.Options.Roles;
public class CustomRole2Settings : AbstractOptionGroup
{
public override string GroupName => "Custom Role";
public override Type AdvancedRole => typeof(CustomRole2);
[ModdedToggleOption("Teleportation ability")] public bool Teleport { get; set; } = true;
}
@@ -1,16 +0,0 @@
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using System;
namespace MiraAPI.Example.Options.Roles;
public class CustomRoleSettings : AbstractOptionGroup
{
public override string GroupName => "Custom Role";
public override Type AdvancedRole => typeof(CustomRole);
[ModdedNumberOption("Number Opt", min: 3, max: 9)] public float NumberOpt { get; set; } = 4;
}
@@ -0,0 +1,20 @@
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using System;
using MiraAPI.Utilities;
namespace MiraAPI.Example.Options.Roles;
public class FreezerRoleSettings : AbstractOptionGroup
{
public override string GroupName => "Custom Role";
public override Type AdvancedRole => typeof(FreezerRole);
[ModdedNumberOption("Freeze Duration", 1, 15, 1, MiraNumberSuffixes.Seconds)]
public float FreezeDuration { get; set; } = 5;
[ModdedNumberOption("Freeze Uses", 1, 5)]
public float FreezeUses { get; set; } = 1;
}
@@ -0,0 +1,23 @@
using System;
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Utilities;
namespace MiraAPI.Example.Options.Roles;
public class TeleporterOptions : AbstractOptionGroup
{
public override string GroupName => "Teleporter";
public override Type AdvancedRole => typeof(TeleporterRole);
public ModdedNumberOption TeleportCooldown { get; set; } = new("Teleport Cooldown", 10, 5, 60, 2.5f, MiraNumberSuffixes.Seconds);
[ModdedNumberOption("Teleport Duration", 5, 25, 1, MiraNumberSuffixes.Seconds)]
public float TeleportDuration { get; set; } = 10;
[ModdedNumberOption("Zoom Distance", 4, 15)]
public float ZoomDistance { get; set; } = 6;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+46
View File
@@ -0,0 +1,46 @@
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
namespace MiraAPI.Example.Roles;
[RegisterCustomRole]
public class ChameloenRole : CrewmateRole, ICustomRole
{
public string RoleName => "Chamelon";
public string RoleLongDescription => "Stay invisible while not moving.";
public string RoleDescription => RoleLongDescription;
public Color RoleColor => Palette.AcceptedGreen;
public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
public LoadableAsset<Sprite> OptionsScreenshot => ExampleAssets.Banner;
public int MaxPlayers => 2;
public void PlayerControlFixedUpdate(PlayerControl playerControl)
{
if (playerControl.MyPhysics.Velocity.magnitude > 0)
{
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, 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>())
{
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);
foreach (var cosmetic in playerControl.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
{
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
}
}
}
}
-17
View File
@@ -1,17 +0,0 @@
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Roles;
[RegisterCustomRole]
public class CustomRole : ImpostorRole, ICustomRole
{
public string RoleName => "Custom Role";
public string RoleLongDescription => "Eliminate everyone with your special ability!";
public string RoleDescription => RoleLongDescription;
public Color RoleColor => Palette.Black;
public ModdedRoleTeams Team => ModdedRoleTeams.Impostor;
public LoadableAsset<Sprite> OptionsScreenshot => ExampleAssets.Banner;
}
-17
View File
@@ -1,17 +0,0 @@
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Roles;
[RegisterCustomRole]
public class CustomRole2 : CrewmateRole, ICustomRole
{
public string RoleName => "Custom Role 2";
public string RoleLongDescription => "Save everyone from dying!";
public string RoleDescription => RoleLongDescription;
public Color RoleColor => Palette.AcceptedGreen;
public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
public LoadableAsset<Sprite> OptionsScreenshot => ExampleAssets.Banner;
}
+19
View File
@@ -0,0 +1,19 @@
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Roles;
[RegisterCustomRole]
public class FreezerRole : ImpostorRole, ICustomRole
{
public string RoleName => "Freezer";
public string RoleLongDescription => "Freeze another player for a duration of time.";
public string RoleDescription => RoleLongDescription;
public Color RoleColor => Palette.Blue;
public ModdedRoleTeams Team => ModdedRoleTeams.Impostor;
[HideFromIl2Cpp]
public LoadableAsset<Sprite> OptionsScreenshot => ExampleAssets.Banner;
public int MaxPlayers => 2;
}
@@ -0,0 +1,27 @@
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Example.Roles;
[RegisterCustomRole]
public class NeutralKillerRole : ImpostorRole, ICustomRole
{
public string RoleName => "Neutral Killer";
public string RoleDescription => "Neutral who can kill.";
public string RoleLongDescription => RoleDescription;
public Color RoleColor => Color.magenta;
public ModdedRoleTeams Team => ModdedRoleTeams.Neutral;
public bool UseVanillaKillButton => true;
public bool CanGetKilled => true;
public bool CanUseVent => true;
public override void SpawnTaskHeader(PlayerControl playerControl)
{
// remove existing task header.
}
public override bool DidWin(GameOverReason gameOverReason)
{
return GameManager.Instance.DidHumansWin(gameOverReason);
}
}
+18
View File
@@ -0,0 +1,18 @@
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Roles;
[RegisterCustomRole]
public class TeleporterRole : CrewmateRole, ICustomRole
{
public string RoleName => "Teleporter";
public string RoleLongDescription => "Zoom out and teleport across the map!";
public string RoleDescription => RoleLongDescription;
public Color RoleColor => new Color32(221, 176, 152, 255);
public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
[HideFromIl2Cpp]
public LoadableAsset<Sprite> OptionsScreenshot => ExampleAssets.Banner;
}
@@ -0,0 +1,46 @@
using MiraAPI.Animations;
using System;
using System.Collections.Generic;
namespace MiraAPI.Modifiers;
/// <summary>
/// The manager for handling modifiers.
/// </summary>
public static class AnimationPresetManager
{
private static uint _nextId;
/// <summary>
/// Gets registered Animation Presets.
/// </summary>
public static List<CustomAnimationPreset> Presets { get; internal set; } = new List<CustomAnimationPreset>();
private static uint GetNextId()
{
_nextId++;
return _nextId;
}
/// <summary>
/// Get Animation Preset by ID.
/// </summary>
/// <param name="id">The id of the preset you want to find.</param>
/// <returns>The animation preset.</returns>
public static CustomAnimationPreset? GetPresetById(uint id)
{
return Presets.Find(p => p.Id == id);
}
internal static void RegisterPreset(Type modifierType)
{
if (!typeof(CustomAnimationPreset).IsAssignableFrom(modifierType))
{
return;
}
CustomAnimationPreset preset = (CustomAnimationPreset)Activator.CreateInstance(modifierType);
preset.Id = GetNextId();
Presets.Add(preset);
}
}
@@ -0,0 +1,24 @@
using UnityEngine;
namespace MiraAPI.Animations;
/// <summary>
/// Configure an animation's settings using presets.
/// </summary>
/// <param name="clip">The animation clip the preset will use.</param>
public abstract class CustomAnimationPreset(AnimationClip clip)
{
internal uint Id;
internal void PlayAnimation(PlayerControl source)
{
}
/// <summary>
/// Check whether the local player should be able to see this animation.
/// </summary>
/// <param name="source">The player who the animation is being triggered upon.</param>
/// <returns>Whether the local player can see the animation or not.</returns>
public abstract bool AnimationVisible(PlayerControl source);
}
+38
View File
@@ -0,0 +1,38 @@
using MiraAPI.Animations.Presets;
using MiraAPI.Modifiers;
using MiraAPI.Networking;
using MiraAPI.Utilities;
using Reactor.Networking.Attributes;
using Reactor.Utilities.Attributes;
using System;
using UnityEngine;
namespace MiraAPI.Animations;
[RegisterInIl2Cpp]
public class CustomAnimator(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
private PlayerControl _player;
private void Start()
{
_player = gameObject.GetComponent<PlayerControl>();
_player.GetCustomAnimator()?.TriggerCustomAnimation(new LocalAnimationPreset(new AnimationClip()));
}
public void TriggerCustomAnimation(CustomAnimationPreset preset) => RpcTriggerCustomAnimation(_player, preset.Id);
[MethodRpc((uint)MiraRpc.TriggerAnimation)]
internal static void RpcTriggerCustomAnimation(PlayerControl source, uint presetId)
{
CustomAnimationPreset preset = AnimationPresetManager.GetPresetById(presetId);
if (preset is null) return;
if (preset.AnimationVisible(source))
{
preset.PlayAnimation(source);
}
}
}
@@ -0,0 +1,11 @@
using UnityEngine;
namespace MiraAPI.Animations.Presets;
public class LocalAnimationPreset(AnimationClip clip) : CustomAnimationPreset(clip)
{
/// <inheritdoc/>
public override bool AnimationVisible(PlayerControl source)
{
return source.AmOwner;
}
}
@@ -0,0 +1,6 @@
using System;
namespace MiraAPI.Animations;
[AttributeUsage(AttributeTargets.Class)]
public class RegisterAnimationPresetAttribute : Attribute;
+22 -1
View File
@@ -3,21 +3,42 @@ using Reactor.Localization.Utilities;
using UnityEngine;
namespace MiraAPI.Colors;
/// <summary>
/// Represents a custom color with a main color and a shadow color.
/// </summary>
/// <param name="name">The name of the option.</param>
/// <param name="mainColor">The main color.</param>
/// <param name="shadowColor">The shadow color.</param>
public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
{
/// <summary>
/// Gets the main color.
/// </summary>
public Color32 MainColor { get; } = mainColor;
/// <summary>
/// Gets the shadow color.
/// </summary>
public Color32 ShadowColor { get; } = shadowColor;
/// <summary>
/// Gets the name of the color.
/// </summary>
public StringNames Name { get; } = name;
/// <inheritdoc />
public CustomColor(StringNames name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60))
{
}
/// <inheritdoc />
public CustomColor(string name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60))
{
}
/// <inheritdoc />
public CustomColor(string name, Color32 mainColor, Color32 shadowColor) : this(CustomStringName.CreateAndRegister(name), mainColor, shadowColor)
{
}
}
}
+24 -13
View File
@@ -4,18 +4,29 @@ using HarmonyLib;
namespace MiraAPI.Colors;
internal static class PaletteManager
/// <summary>
/// Used to register and track custom colors.
/// </summary>
public static class PaletteManager
{
public static readonly List<CustomColor> CustomColors = [];
public static void RegisterAllColors()
{
var colors = CustomColors.Select(x => x.MainColor);
var shadowColors = CustomColors.Select(x => x.ShadowColor);
var stringNames = CustomColors.Select(x => x.Name);
/// <summary>
/// Gets all registered custom colors.
/// </summary>
public static CustomColor[] RegisteredColors => [.. CustomColors];
Palette.PlayerColors = Palette.PlayerColors.ToArray().AddRangeToArray(colors.ToArray());
Palette.ShadowColors = Palette.ShadowColors.ToArray().AddRangeToArray(shadowColors.ToArray());
Palette.ColorNames = Palette.ColorNames.ToArray().AddRangeToArray(stringNames.ToArray());
}
}
internal static readonly List<CustomColor> CustomColors = [];
internal static void RegisterAllColors()
{
var colors = CustomColors.Select(x => x.MainColor).ToArray();
var shadowColors = CustomColors.Select(x => x.ShadowColor).ToArray();
var stringNames = CustomColors.Select(x => x.Name).ToArray();
Palette.PlayerColors = Palette.PlayerColors.ToArray().AddRangeToArray(colors);
Palette.ShadowColors = Palette.ShadowColors.ToArray().AddRangeToArray(shadowColors);
Palette.ColorNames = Palette.ColorNames.ToArray().AddRangeToArray(stringNames);
Palette.TextColors = Palette.TextColors.ToArray().AddRangeToArray(colors);
Palette.TextOutlineColors = Palette.TextOutlineColors.ToArray().AddRangeToArray(shadowColors);
}
}
@@ -2,5 +2,8 @@
namespace MiraAPI.Colors;
/// <summary>
/// Used to mark a class for custom colors registration.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class RegisterCustomColorsAttribute : Attribute;
public class RegisterCustomColorsAttribute : Attribute;
+66 -50
View File
@@ -3,74 +3,82 @@
namespace MiraAPI.GameModes;
/// <summary>
/// Base class for custom gamemodes
/// Base class for custom gamemodes.
/// </summary>
public abstract class CustomGameMode
{
/// <summary>
/// Game mode name
/// Gets the game mode name.
/// </summary>
public abstract string Name { get; }
/// <summary>
/// Game mode description
/// Gets the game mode description.
/// </summary>
public abstract string Description { get; }
/// <summary>
/// Game mode ID
/// Gets the game mode ID.
/// </summary>
public abstract int Id { get; }
/// <summary>
/// Called when Intro Cutscene is destroyed
/// Called when Intro Cutscene is destroyed.
/// </summary>
public virtual void Initialize() { }
public virtual void Initialize()
{
}
/// <summary>
/// Called on HudManager.Start()
/// Called on HudManager.Start().
/// </summary>
/// <param name="instance">Instance of HudManager</param>
public virtual void HudStart(HudManager instance) { }
/// <param name="instance">Instance of HudManager.</param>
public virtual void HudStart(HudManager instance)
{
}
/// <summary>
/// Called every frame on HudManager.Update()
/// Called every frame on HudManager.Update().
/// </summary>
/// <param name="instance">Instance of HudManager</param>
public virtual void HudUpdate(HudManager instance) { }
/// <param name="instance">Instance of HudManager.</param>
public virtual void HudUpdate(HudManager instance)
{
}
/// <summary>
/// Called when a player is killed
/// Called when a player is killed.
/// </summary>
/// <param name="player">PlayerControl that was killed</param>
public virtual void OnDeath(PlayerControl player) { }
/// <param name="player">PlayerControl that was killed.</param>
public virtual void OnDeath(PlayerControl player)
{
}
/// <summary>
/// Calculate Game End logic
/// Calculate Game End logic.
/// </summary>
/// <param name="runOriginal">Should original game end logic be used</param>
/// <param name="instance">Instance of LogicGameFlowNormal</param>
/// <param name="runOriginal">Should original game end logic be used.</param>
/// <param name="instance">Instance of LogicGameFlowNormal.</param>
public virtual void CheckGameEnd(out bool runOriginal, LogicGameFlowNormal instance)
{
runOriginal = true;
}
/// <summary>
/// Custom role assignment
/// Custom role assignment.
/// </summary>
/// <param name="runOriginal">Should original role assignment be used</param>
/// <param name="instance">Instance of LogicRoleSelection</param>
/// <param name="runOriginal">Should original role assignment be used.</param>
/// <param name="instance">Instance of LogicRoleSelection.</param>
public virtual void AssignRoles(out bool runOriginal, LogicRoleSelectionNormal instance)
{
runOriginal = true;
}
/// <summary>
/// Custom murder logic
/// Custom murder logic.
/// </summary>
/// <param name="runOriginal">Should original murder logic be used</param>
/// <param name="result">Result of custom murder logic</param>
/// <param name="target">Target player for murder</param>
/// <param name="runOriginal">Should the original murder logic be used.</param>
/// <param name="result">Result of custom murder logic.</param>
/// <param name="target">Target player for murder.</param>
public virtual void CanKill(out bool runOriginal, out bool result, PlayerControl target)
{
result = false;
@@ -78,61 +86,69 @@ public abstract class CustomGameMode
}
/// <summary>
/// Should Roles Settings be available when this gamemode is selected?
/// Should Roles Settings be available when this gamemode is selected.
/// </summary>
public virtual bool CanAccessRolesTab() => true;
/// <returns>True if Role Settings are enabled in this game mode.</returns>
public virtual bool AreRoleSettingsEnabled() => true;
/// <summary>
/// Should Game Settings be available when this gamemode is selected?
/// Should Game Settings be available when this gamemode is selected.
/// </summary>
public virtual bool CanAccessSettingsTab() => true;
/// <returns>True if Game Settings are enabled in this mode.</returns>
public virtual bool AreGameSettingsEnabled() => true;
/// <summary>
/// Custom winner selection
/// Custom winner selection.
/// </summary>
/// <returns>List of winners</returns>
public virtual List<NetworkedPlayerInfo> CalculateWinners() => null;
/// <returns>List of winners or null.</returns>
public virtual List<NetworkedPlayerInfo>? CalculateWinners() => null;
/// <summary>
/// Show gamemode in Intro Cutscene
/// Show gamemode in Intro Cutscene.
/// </summary>
public virtual bool ShowCustomRoleScreen() => false;
/// <returns>True if the game mode should be shown in the intro cutscene.</returns>
public virtual bool ShowGameModeIntroCutscene() => false;
/// <summary>
/// Can Admin be used in this gamemode
/// Can Admin be used in this gamemode.
/// </summary>
/// <param name="console">Admin Console</param>
/// <param name="console">Admin Console.</param>
/// <returns>True if Admin console is enabled.</returns>
public virtual bool CanUseMapConsole(MapConsole console) => true;
/// <summary>
/// Can a body be reported in this gamemode
/// Can a body be reported in this gamemode.
/// </summary>
/// <param name="body">Target body for reporting</param>
/// <param name="body">Target body for reporting.</param>
/// <returns>True if dead bodies can be reported.</returns>
public virtual bool CanReport(DeadBody body) => true;
/// <summary>
/// Can system consoles be used in this gamemode
/// Can system consoles be used in this gamemode.
/// </summary>
/// <param name="console">System Console</param>
/// <param name="console">System Console.</param>
/// <returns>True if system consoles are enabled in this mode.</returns>
public virtual bool CanUseSystemConsole(SystemConsole console) => true;
/// <summary>
/// Can tasks be interacted with in this gamemode
/// Can tasks be interacted with in this gamemode.
/// </summary>
/// <param name="console">Task console</param>
public virtual bool CanUseConsole(Console console) => true;
/// <param name="console">Task console.</param>
/// <returns>True if tasks are enabled in this mode.</returns>
public virtual bool CanUseTasks(Console console) => true;
/// <summary>
/// Should the sabotage map be used when attempting to open sabotage overlay
/// Should the sabotage map be used when attempting to open sabotage overlay.
/// </summary>
/// <param name="map">MapBehaviour object</param>
/// <param name="map">MapBehaviour object.</param>
/// <returns>True if the sabotage map should be shown.</returns>
public virtual bool ShouldShowSabotageMap(MapBehaviour map) => true;
/// <summary>
/// Can a player vent in this gamemode
/// Can a player vent in this gamemode.
/// </summary>
/// <param name="vent">Target vent</param>
/// <param name="playerInfo">Player attempting to vent</param>
/// <param name="vent">Target vent.</param>
/// <param name="playerInfo">Player attempting to vent.</param>
/// <returns>True if venting is enabled in this mode.</returns>
public virtual bool CanVent(Vent vent, NetworkedPlayerInfo playerInfo) => true;
}
}
+10 -11
View File
@@ -1,5 +1,4 @@
#nullable enable
using Reactor.Utilities;
using Reactor.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,12 +6,12 @@ using System.Linq;
namespace MiraAPI.GameModes;
/// <summary>
/// Manages custom gamemodes
/// Manages custom gamemodes.
/// </summary>
public static class CustomGameModeManager
{
/// <summary>
/// List of registered gamemodes
/// List of registered gamemodes.
/// </summary>
internal static readonly Dictionary<int, CustomGameMode> GameModes = [];
@@ -22,14 +21,14 @@ public static class CustomGameModeManager
}
/// <summary>
/// Current gamemode
/// Current gamemode.
/// </summary>
public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode();
/// <summary>
/// Set current gamemode
/// Set current gamemode.
/// </summary>
/// <param name="id">gamemode ID</param>
/// <param name="id">gamemode ID.</param>
public static void SetGameMode(int id)
{
if (GameModes.TryGetValue(id, out var gameMode))
@@ -42,9 +41,9 @@ public static class CustomGameModeManager
}
/// <summary>
/// Register gamemode from type
/// Register gamemode from type.
/// </summary>
/// <param name="gameModeType">Type of gamemode class, should inherit from <see cref="CustomGameMode"/></param>
/// <param name="gameModeType">Type of gamemode class, should inherit from <see cref="CustomGameMode"/>.</param>
internal static void RegisterGameMode(Type gameModeType)
{
if (!typeof(CustomGameMode).IsAssignableFrom(gameModeType))
@@ -60,7 +59,7 @@ public static class CustomGameModeManager
Logger<MiraApiPlugin>.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!");
@@ -69,4 +68,4 @@ public static class CustomGameModeManager
GameModes.Add(gameMode.Id, gameMode);
}
}
}
+1 -1
View File
@@ -6,4 +6,4 @@ public class DefaultMode : CustomGameMode
public override string Name => "Default";
public override string Description => "Default Among Us GameMode";
public override int Id => 0;
}
}
@@ -25,4 +25,4 @@ public class RegisterGameModeAttribute : Attribute
}
}
}
}
}
+17 -8
View File
@@ -4,31 +4,40 @@ using UnityEngine;
namespace MiraAPI.GameOptions;
/// <summary>
/// Base class for option groups. An option group is a collection of options that are displayed together in the options menu.
/// </summary>
public abstract class AbstractOptionGroup
{
public List<IModdedOption> Options { get; } = [];
internal List<IModdedOption> Options { get; } = [];
/// <summary>
/// The name of the group. Visible in options menu.
/// Gets the name of the group. Visible in options menu.
/// </summary>
public abstract string GroupName { get; }
/// <summary>
/// A function that returns whether the group should be visible or not.
/// Gets the function that determines whether the group should be visible or not.
/// </summary>
public virtual Func<bool> GroupVisible => () => true;
/// <summary>
/// The group color. This is used to color the group in the options menu.
/// Gets the group color. This is used to color the group in the options menu.
/// </summary>
public virtual Color GroupColor => Color.clear;
/// <summary>
/// The role the group is associated with. This is used for the advanced role options menu.
/// Gets the group priority. This is used to determine the order in which groups are displayed in the options menu.
/// Zero is the highest priority, and the default value is the max uint value.
/// </summary>
public virtual Type AdvancedRole => null;
public virtual uint GroupPriority => uint.MaxValue;
/// <summary>
/// Gets the role the group is associated with. This is used for the advanced role options menu.
/// </summary>
public virtual Type? AdvancedRole => null;
internal bool AllOptionsHidden { get; set; }
internal CategoryHeaderMasked Header { get; set; }
}
internal CategoryHeaderMasked? Header { get; set; }
}
@@ -4,24 +4,33 @@ using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
public class ModdedEnumOptionAttribute(string title, Type enumType, Type roleType = null)
/// <summary>
/// Attribute for creating an enum option.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null, Type? roleType = null)
: ModdedOptionAttribute(title, roleType)
{
internal override IModdedOption CreateOption(object value, PropertyInfo property)
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
var opt = new ModdedEnumOption(Title, (int)value, enumType, RoleType);
var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values, RoleType);
return opt;
}
/// <inheritdoc />
public override void SetValue(object value)
{
var opt = HolderOption as ModdedEnumOption;
opt.SetValue((int)value);
opt?.SetValue((int)value);
}
/// <inheritdoc />
public override object GetValue()
{
var opt = HolderOption as ModdedEnumOption;
return Enum.ToObject(enumType, opt.Value);
if (HolderOption is ModdedEnumOption opt)
{
return Enum.ToObject(enumType, opt.Value);
}
throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${enumType.FullName} is not a ModdedEnumOption");
}
}
}
@@ -5,6 +5,10 @@ using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// A number option attribute for the modded options system.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedNumberOptionAttribute(
string title,
float min,
@@ -12,24 +16,28 @@ public class ModdedNumberOptionAttribute(
float increment = 1,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
bool zeroInfinity = false,
Type roleType = null)
Type? roleType = null)
: ModdedOptionAttribute(title, roleType)
{
internal override IModdedOption CreateOption(object value, PropertyInfo property)
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
var toggleOpt = new ModdedNumberOption(Title, (float)value, min, max, increment, suffixType, zeroInfinity, RoleType);
return toggleOpt;
return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, zeroInfinity, RoleType);
}
/// <inheritdoc />
public override void SetValue(object value)
{
var toggleOpt = HolderOption as ModdedNumberOption;
toggleOpt.SetValue((float)value);
var opt = HolderOption as ModdedNumberOption;
opt?.SetValue((float)value);
}
/// <inheritdoc />
public override object GetValue()
{
var toggleOpt = HolderOption as ModdedNumberOption;
return toggleOpt.Value;
if (HolderOption is ModdedNumberOption opt)
{
return opt.Value;
}
throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption");
}
}
}
@@ -3,18 +3,37 @@ using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Represents an attribute that is used to define a modded option.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="roleType">Optional parameter to specify a role Type.</param>
[AttributeUsage(AttributeTargets.Property)]
public abstract class ModdedOptionAttribute(string title, Type roleType = null) : Attribute
public abstract class ModdedOptionAttribute(string title, Type? roleType = null) : Attribute
{
public IModdedOption HolderOption { get; internal set; }
public string Title { get; private set; } = title;
protected Type RoleType { get; private set; } = roleType;
internal IModdedOption? HolderOption { get; set; }
public abstract void SetValue(object val);
/// <summary>
/// Gets the title of the option.
/// </summary>
public string Title { get; private set; } = title;
/// <summary>
/// Gets the role type of the option.
/// </summary>
protected Type? RoleType { get; private set; } = roleType;
/// <summary>
/// Sets the value of the option.
/// </summary>
/// <param name="value">The new value as an object.</param>
public abstract void SetValue(object value);
/// <summary>
/// Gets the value of the option.
/// </summary>
/// <returns>The value of the option as an object.</returns>
public abstract object GetValue();
internal abstract IModdedOption CreateOption(object value, PropertyInfo property);
}
internal abstract IModdedOption? CreateOption(object? value, PropertyInfo property);
}
@@ -1,27 +0,0 @@
using MiraAPI.GameOptions.OptionTypes;
using System;
using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
public class ModdedStringOptionAttribute(string title, string[] values, Type roleType = null)
: ModdedOptionAttribute(title, roleType)
{
internal override IModdedOption CreateOption(object value, PropertyInfo property)
{
var opt = new ModdedStringOption(Title, (int)value, values, RoleType);
return opt;
}
public override void SetValue(object value)
{
var opt = HolderOption as ModdedStringOption;
opt.SetValue((int)value);
}
public override object GetValue()
{
var opt = HolderOption as ModdedStringOption;
return opt.Value;
}
}
@@ -4,23 +4,34 @@ using System.Reflection;
namespace MiraAPI.GameOptions.Attributes;
public class ModdedToggleOptionAttribute(string title, Type roleType = null) : ModdedOptionAttribute(title, roleType)
/// <summary>
/// Attribute for a toggle option.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="roleType">An optional role type.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedToggleOptionAttribute(string title, Type? roleType = null) : ModdedOptionAttribute(title, roleType)
{
internal override IModdedOption CreateOption(object value, PropertyInfo property)
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
var toggleOpt = new ModdedToggleOption(Title, (bool)value, RoleType);
var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), RoleType);
return toggleOpt;
}
/// <inheritdoc />
public override void SetValue(object value)
{
var toggleOpt = HolderOption as ModdedToggleOption;
toggleOpt.SetValue((bool)value);
var opt = HolderOption as ModdedToggleOption;
opt?.SetValue((bool)value);
}
/// <inheritdoc />
public override object GetValue()
{
var toggleOpt = HolderOption as ModdedToggleOption;
return toggleOpt.Value;
if (HolderOption is ModdedToggleOption opt)
{
return opt.Value;
}
throw new InvalidOperationException($"Holder option for {Title} is not a ModdedToggleOption.");
}
}
}
+17 -18
View File
@@ -1,25 +1,24 @@
using MiraAPI.PluginLoading;
using System;
using System;
using BepInEx.Configuration;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using UnityEngine;
namespace MiraAPI.GameOptions;
public interface IModdedOption
internal interface IModdedOption
{
public uint Id { get; }
public BaseGameSetting Data { get; }
public IMiraPlugin ParentMod { get; set; }
public Type AdvancedRole { get; set; }
public OptionBehaviour OptionBehaviour { get; protected set; }
public string Title { get; }
public StringNames StringName { get; }
public Func<bool> Visible { get; set; }
public ConfigDefinition ConfigDefinition { get; }
public void ValueChanged(OptionBehaviour optionBehaviour);
public OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container);
public abstract float GetFloatData();
public NetData GetNetData();
public void HandleNetData(byte[] data);
}
uint Id { get; }
string Title { get; }
StringNames StringName { get; }
IMiraPlugin? ParentMod { get; set; }
BaseGameSetting? Data { get; }
Type? AdvancedRole { get; set; }
OptionBehaviour? OptionBehaviour { get; }
Func<bool> Visible { get; set; }
ConfigDefinition? ConfigDefinition { get; set; }
OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container);
float GetFloatData();
NetData GetNetData();
void HandleNetData(byte[] data);
}
+79 -39
View File
@@ -1,28 +1,32 @@
using HarmonyLib;
using MiraAPI.GameOptions.Attributes;
using Reactor.Utilities;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BepInEx.Configuration;
using HarmonyLib;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
namespace MiraAPI.GameOptions;
/// <summary>
/// Handles modded options.
/// </summary>
public static class ModdedOptionsManager
{
private static readonly Dictionary<PropertyInfo, ModdedOptionAttribute> OptionAttributes = new();
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = new();
private static readonly Dictionary<PropertyInfo, ModdedOptionAttribute> OptionAttributes = [];
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
internal static readonly Dictionary<uint, IModdedOption> ModdedOptions = new();
internal static readonly Dictionary<uint, IModdedOption> ModdedOptions = [];
internal static readonly List<AbstractOptionGroup> Groups = [];
internal static uint NextId => _nextId++;
private static uint _nextId = 1;
internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo)
{
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
@@ -40,10 +44,10 @@ public static class ModdedOptionsManager
Groups.Add(group);
TypeToGroup.Add(type, group);
pluginInfo.OptionGroups.Add(group);
return true;
}
internal static void RegisterPropertyOption(Type type, PropertyInfo property, MiraPluginInfo pluginInfo)
{
if (!TypeToGroup.TryGetValue(type, out var group))
@@ -57,11 +61,15 @@ public static class ModdedOptionsManager
Logger<MiraApiPlugin>.Error($"Failed to get option for {property.Name}");
return;
}
RegisterOption(option, group, pluginInfo);
RegisterOption(option, group, property.Name, pluginInfo);
}
internal static void RegisterAttributeOption(Type type, ModdedOptionAttribute attribute, PropertyInfo property, MiraPluginInfo pluginInfo)
internal static void RegisterAttributeOption(
Type type,
ModdedOptionAttribute attribute,
PropertyInfo property,
MiraPluginInfo pluginInfo)
{
if (OptionAttributes.ContainsKey(property))
{
@@ -76,13 +84,13 @@ public static class ModdedOptionsManager
}
var option = attribute.CreateOption(property.GetValue(group), property);
if (option == null)
{
Logger<MiraApiPlugin>.Error($"Failed to get option for {property.Name}");
return;
}
var setterOriginal = property.GetSetMethod();
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
@@ -93,25 +101,33 @@ public static class ModdedOptionsManager
OptionAttributes.Add(property, attribute);
attribute.HolderOption = option;
RegisterOption(option, group, pluginInfo);
RegisterOption(option, group, property.Name, pluginInfo);
}
private static void RegisterOption(IModdedOption option, AbstractOptionGroup group, MiraPluginInfo pluginInfo)
private static void RegisterOption(
IModdedOption option,
AbstractOptionGroup group,
string propertyName,
MiraPluginInfo pluginInfo)
{
var groupName = group.GetType().FullName;
option.ConfigDefinition = new ConfigDefinition(groupName, propertyName);
option.ParentMod = pluginInfo.MiraPlugin;
option.AdvancedRole = group.AdvancedRole;
pluginInfo.Options.Add(option);
ModdedOptions.Add(option.Id, option);
group.Options.Add(option);
}
internal static void SyncAllOptions(int targetId=-1)
internal static void SyncAllOptions(int targetId = -1)
{
var chunks = ModdedOptions.Values.Select(option => option.GetNetData()).ChunkNetData(1000);
while (chunks.Count > 0)
{
Rpc<SyncOptionsRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, chunks.Dequeue());
@@ -120,35 +136,59 @@ public static class ModdedOptionsManager
internal static void HandleSyncOptions(NetData[] data)
{
// necessary to disable then re-enable this setting
// 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())
{
oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet);
plugin.PluginConfig.SaveOnConfigSet = false;
}
foreach (var netData in data)
{
if (!ModdedOptions.TryGetValue(netData.Id, out var option))
{
continue;
}
option.HandleNetData(netData.Data);
}
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
{
plugin.PluginConfig.Save();
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
}
if (LobbyInfoPane.Instance)
{
LobbyInfoPane.Instance.RefreshPane();
}
}
/// <summary>
/// Patches the setter of a property to update the value of the option.
/// </summary>
/// <param name="__originalMethod">The original setter method.</param>
/// <param name="value">The new object value.</param>
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
{
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod().Equals(__originalMethod)).Value;
attribute?.SetValue(value);
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
attribute.SetValue(value);
}
/// <summary>
/// Patches the getter of a property to return the value of the option.
/// </summary>
/// <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>
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
{
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod().Equals(__originalMethod)).Value;
if (attribute == null)
{
return true;
}
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
__result = attribute.GetValue();
return false;
}
}
}
+14 -18
View File
@@ -1,23 +1,19 @@
using System;
using System.Linq;
using System.Linq;
namespace MiraAPI.GameOptions;
public class OptionGroupSingleton<T> where T : AbstractOptionGroup
/// <summary>
/// Singleton for option groups.
/// </summary>
/// <typeparam name="T">The option group type.</typeparam>
public static class OptionGroupSingleton<T> where T : AbstractOptionGroup
{
private static T _instance;
private static T? _instance;
public static T Instance
{
get => _instance ??= ModdedOptionsManager.Groups.OfType<T>().Single();
set
{
if (_instance != null)
{
throw new InvalidOperationException($"Instance for {typeof(T)} is already set");
}
_instance = value;
}
}
}
/// <summary>
/// Gets the instance of the option group.
/// </summary>
#pragma warning disable CA1000
public static T Instance => _instance ??= ModdedOptionsManager.Groups.OfType<T>().Single();
#pragma warning restore CA1000
}
@@ -1,38 +1,57 @@
using Reactor.Localization.Utilities;
using System;
using System;
using System.Linq;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.Networking;
using Reactor.Localization.Utilities;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.GameOptions.OptionTypes;
/// <summary>
/// An option for selecting an enum value.
/// </summary>
public class ModdedEnumOption : ModdedOption<int>
{
public string[] Values { get; }
/// <summary>
/// Gets the string values of the enum.
/// </summary>
public string[]? Values { get; }
public ModdedEnumOption(string title, int defaultValue, Type enumType, Type roleType=null) : base(title, defaultValue, roleType)
/// <summary>
/// Initializes a new instance of the <see cref="ModdedEnumOption"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <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>
/// <param name="roleType">An optional role type to specify for a specific role.</param>
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, Type? roleType = null) : base(title, defaultValue, roleType)
{
Values = Enum.GetNames(enumType);
Values = values ?? Enum.GetNames(enumType);
Data = ScriptableObject.CreateInstance<StringGameSetting>();
var data = (StringGameSetting)Data;
data.Title = StringName;
data.Type = global::OptionTypes.String;
data.Values = Values.Select(CustomStringName.CreateAndRegister).ToArray();
data.Values = values is null ?
Enum.GetNames(enumType).Select(CustomStringName.CreateAndRegister).ToArray()
: values.Select(CustomStringName.CreateAndRegister).ToArray();
data.Index = Value;
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
// SetUpFromData method doesnt work correctly so we must set the values manually
stringOption.Title = StringName;
stringOption.Values = ((StringGameSetting)Data).Values;
stringOption.Values = (Data as StringGameSetting)?.Values ?? new Il2CppStructArray<StringNames>(0);
stringOption.Value = Value;
OptionBehaviour = stringOption;
@@ -40,43 +59,42 @@ public class ModdedEnumOption : ModdedOption<int>
return stringOption;
}
/// <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));
}
public override string GetHudStringText()
{
return Title + ": " + Values[Value];
}
/// <inheritdoc />
public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return optionBehaviour.GetInt();
}
public override void OnValueChanged(int newValue)
/// <inheritdoc />
protected override void OnValueChanged(int newValue)
{
DestroyableSingleton<HudManager>.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false);
DestroyableSingleton<HudManager>.Instance.Notifier.AddSettingsChangeMessage(StringName, Data?.GetValueString(newValue), false);
if (!OptionBehaviour)
{
return;
}
var opt = OptionBehaviour as StringOption;
if (opt)
if (OptionBehaviour is StringOption opt)
{
opt.Value = newValue;
}
}
}
}
@@ -7,19 +7,56 @@ using Object = UnityEngine.Object;
namespace MiraAPI.GameOptions.OptionTypes;
/// <summary>
/// Represents a modded number option.
/// </summary>
public class ModdedNumberOption : ModdedOption<float>
{
/// <summary>
/// Gets the minimum value of the option.
/// </summary>
public float Min { get; }
/// <summary>
/// Gets the maximum value of the option.
/// </summary>
public float Max { get; }
/// <summary>
/// Gets the increment value of the option.
/// </summary>
public float Increment { get; }
/// <summary>
/// Gets the suffix type of the option.
/// </summary>
public MiraNumberSuffixes SuffixType { get; }
/// <summary>
/// Gets a value indicating whether zero is infinity.
/// </summary>
public bool ZeroInfinity { get; }
public ModdedNumberOption(string title, float defaultValue, float min, float max, float increment, MiraNumberSuffixes suffixType, bool zeroInfinity = false, Type roleType = null) : base(title, defaultValue, roleType)
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOption"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as a float.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="roleType">An optional role type.</param>
public ModdedNumberOption(
string title,
float defaultValue,
float min,
float max,
float increment,
MiraNumberSuffixes suffixType,
bool zeroInfinity = false,
Type? roleType = null) : base(title, defaultValue, roleType)
{
Min = min;
Max = max;
@@ -37,13 +74,18 @@ public class ModdedNumberOption : ModdedOption<float>
data.Value = Value;
data.Increment = Increment;
data.ValidRange = new FloatRange(Min, Max);
data.FormatString = Increment % 1 == 0 && Value % 1 == 0 && Min % 1 == 0 && Max % 1 == 0 ? "0" : "0.0";
data.FormatString = Increment % 1 == 0 && Value % 1 == 0 && Min % 1 == 0 && Max % 1 == 0 ? "0" : "0.0"; // not sure if the == checks will work since this is floats but we'll find out
data.ZeroIsInfinity = ZeroInfinity;
data.SuffixType = (NumberSuffixes)SuffixType;
data.OptionName = FloatOptionNames.Invalid;
}
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
/// <inheritdoc />
public override OptionBehaviour CreateOption(
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
Transform container)
{
var numberOption = Object.Instantiate(numberOpt, Vector3.zero, Quaternion.identity, container);
@@ -54,56 +96,52 @@ public class ModdedNumberOption : ModdedOption<float>
numberOption.Value = Value;
numberOption.Increment = Increment;
numberOption.ValidRange = new FloatRange(Min, Max);
numberOption.FormatString = ((FloatGameSetting)Data).FormatString;
numberOption.FormatString = (Data as FloatGameSetting)?.FormatString ?? "0.0";
numberOption.ZeroIsInfinity = ZeroInfinity;
numberOption.SuffixType = (NumberSuffixes)SuffixType;
numberOption.floatOptionName = FloatOptionNames.Invalid;
OptionBehaviour = numberOption;
return numberOption;
}
/// <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.ToSingle(data));
}
public override string GetHudStringText()
{
return Title + ": " + Value + Helpers.GetSuffix(SuffixType);
}
/// <inheritdoc />
public override float GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return Mathf.Clamp(optionBehaviour.GetFloat(), Min, Max);
}
public override void OnValueChanged(float newValue)
/// <inheritdoc />
protected override void OnValueChanged(float newValue)
{
Value = Mathf.Clamp(newValue, Min, Max);
DestroyableSingleton<HudManager>.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(Value), false);
DestroyableSingleton<HudManager>.Instance.Notifier.AddSettingsChangeMessage(
StringName,
Data?.GetValueString(Value),
false);
if (!OptionBehaviour)
if (OptionBehaviour is NumberOption opt)
{
return;
}
var opt = OptionBehaviour as NumberOption;
if (opt)
{
opt.Value = newValue;
opt.Value = Value;
}
}
}
}
+131 -27
View File
@@ -9,33 +9,101 @@ using UnityEngine;
namespace MiraAPI.GameOptions.OptionTypes;
/// <summary>
/// Represents a modded option.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
public abstract class ModdedOption<T> : IModdedOption
{
private IMiraPlugin _parentMod;
private IMiraPlugin? _parentMod;
/// <summary>
/// Gets the unique identifier of the option.
/// </summary>
public uint Id { get; }
public BaseGameSetting Data { get; protected init; }
public IMiraPlugin ParentMod
/// <summary>
/// Gets the title of the option.
/// </summary>
public string Title { get; }
/// <summary>
/// Gets the StringName object of the option.
/// </summary>
public StringNames StringName { get; }
/// <summary>
/// Gets the BaseGameSetting data of the option.
/// </summary>
public BaseGameSetting? Data { get; protected init; }
/// <summary>
/// Gets or sets the parent mod of the option.
/// </summary>
public IMiraPlugin? ParentMod
{
get => _parentMod;
set
{
if (_parentMod != null || value == null) return;
_parentMod = value;
var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue);
Value = entry.Value;
}
}
public T Value { get; protected set; }
public T DefaultValue { get; }
public Action<T> ChangedEvent { get; set; }
public string Title { get; }
public StringNames StringName { get; }
public Func<bool> Visible { get; set; }
public Type AdvancedRole { get; set; }
public OptionBehaviour OptionBehaviour { get; set; }
public ConfigDefinition ConfigDefinition => new("Options", Title);
protected ModdedOption(string title, T defaultValue, Type roleType)
/// <summary>
/// Gets or sets the value of the option.
/// </summary>
public T Value { get; protected set; }
/// <summary>
/// Gets the default value of the option.
/// </summary>
public T DefaultValue { get; }
/// <summary>
/// Gets or sets the event that is invoked when the value of the option changes.
/// </summary>
public Action<T>? ChangedEvent { get; set; }
/// <summary>
/// Gets or sets the visibility of the option.
/// </summary>
public Func<bool> Visible { get; set; }
/// <summary>
/// Gets or sets the advanced role of the option.
/// </summary>
public Type? AdvancedRole { get; set; }
/// <summary>
/// Gets or sets the option behaviour of the option.
/// </summary>
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;
/// <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>
/// <param name="roleType">The Role Type or null if it doesn't belong to a role.</param>
protected ModdedOption(string title, T defaultValue, Type? roleType)
{
Id = ModdedOptionsManager.NextId;
Title = title;
@@ -50,44 +118,80 @@ public abstract class ModdedOption<T> : IModdedOption
}
}
public void ValueChanged(OptionBehaviour optionBehaviour)
internal void ValueChanged(OptionBehaviour optionBehaviour)
{
SetValue(GetValueFromOptionBehaviour(optionBehaviour));
}
/// <summary>
/// Sets the value of the option.
/// </summary>
/// <param name="newValue">The new value.</param>
public void SetValue(T newValue)
{
var oldVal = Value;
Value = newValue;
if (!Value.Equals(oldVal) && ChangedEvent != null)
if (Value?.Equals(oldVal) == false)
{
ChangedEvent.Invoke(Value);
ChangedEvent?.Invoke(Value);
}
if (AmongUsClient.Instance.AmHost)
{
Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true);
}
if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true)
{
entry.Value = Value;
}
if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true)
{
entry.Value = Value;
Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true);
}
OnValueChanged(newValue);
}
/// <summary>
/// Gets the float data of the option.
/// </summary>
/// <returns>A float object representing the option's value.</returns>
public abstract float GetFloatData();
/// <summary>
/// Gets the net data of the option.
/// </summary>
/// <returns>A NetData object representing this option's data.</returns>
public abstract NetData GetNetData();
/// <summary>
/// Handles incoming net data.
/// </summary>
/// <param name="data">The NetData's byte array.</param>
public abstract void HandleNetData(byte[] data);
public abstract void OnValueChanged(T newValue);
/// <summary>
/// Handles the value changed event.
/// </summary>
/// <param name="newValue">The new value.</param>
protected abstract void OnValueChanged(T newValue);
/// <summary>
/// Gets the value from the option behaviour.
/// </summary>
/// <param name="optionBehaviour">The OptionBehaviour.</param>
/// <returns>The value.</returns>
public abstract T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour);
public abstract OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container);
public abstract string GetHudStringText();
}
/// <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>
public abstract OptionBehaviour CreateOption(
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
Transform container);
}
@@ -1,82 +0,0 @@
using Reactor.Localization.Utilities;
using System;
using System.Linq;
using MiraAPI.Networking;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.GameOptions.OptionTypes;
public class ModdedStringOption : ModdedOption<int>
{
public string[] Values { get; }
public ModdedStringOption(string title, int defaultValue, string[] values, Type roleType=null) : base(title, defaultValue, roleType)
{
Values = values;
Data = ScriptableObject.CreateInstance<StringGameSetting>();
var data = (StringGameSetting)Data;
data.Title = StringName;
data.Type = global::OptionTypes.String;
data.Values = Values.Select(CustomStringName.CreateAndRegister).ToArray();
data.Index = Value;
}
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
// SetUpFromData method doesnt work correctly so we must set the values manually
stringOption.Title = StringName;
stringOption.Values = ((StringGameSetting)Data).Values;
stringOption.Value = Value;
OptionBehaviour = stringOption;
return stringOption;
}
public override float GetFloatData()
{
return Value;
}
public override NetData GetNetData()
{
return new NetData(Id, BitConverter.GetBytes(Value));
}
public override void HandleNetData(byte[] data)
{
SetValue(BitConverter.ToInt32(data));
}
public override string GetHudStringText()
{
return Title + ": " + Values[Value];
}
public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return optionBehaviour.GetInt();
}
public override void OnValueChanged(int newValue)
{
DestroyableSingleton<HudManager>.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false);
if (!OptionBehaviour)
{
return;
}
var opt = OptionBehaviour as StringOption;
if (opt)
{
opt.Value = newValue;
}
}
}
@@ -5,9 +5,18 @@ using Object = UnityEngine.Object;
namespace MiraAPI.GameOptions.OptionTypes;
/// <summary>
/// A modded toggle option.
/// </summary>
public class ModdedToggleOption : ModdedOption<bool>
{
public ModdedToggleOption(string title, bool defaultValue, Type roleType = null) : base(title, defaultValue, roleType)
/// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOption"/> class.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="roleType">An optional role type.</param>
public ModdedToggleOption(string title, bool defaultValue, Type? roleType = null) : base(title, defaultValue, roleType)
{
Data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
@@ -16,6 +25,7 @@ public class ModdedToggleOption : ModdedOption<bool>
data.Type = global::OptionTypes.Checkbox;
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
{
var toggleOption = Object.Instantiate(toggleOpt, Vector3.zero, Quaternion.identity, container);
@@ -32,32 +42,32 @@ public class ModdedToggleOption : ModdedOption<bool>
return toggleOption;
}
/// <inheritdoc />
public override float GetFloatData()
{
return Value ? 1 : 0;
}
/// <inheritdoc />
public override NetData GetNetData()
{
return new NetData(Id, BitConverter.GetBytes(Value));
}
/// <inheritdoc />
public override void HandleNetData(byte[] data)
{
SetValue(BitConverter.ToBoolean(data));
}
public override string GetHudStringText()
{
return Title + ": " + (Value ? "On" : "Off");
}
/// <inheritdoc />
public override bool GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return optionBehaviour.GetBool();
}
public override void OnValueChanged(bool newValue)
/// <inheritdoc />
protected override void OnValueChanged(bool newValue)
{
DestroyableSingleton<HudManager>.Instance.Notifier.AddSettingsChangeMessage(StringName, newValue ? "On" : "Off", false);
if (!OptionBehaviour)
@@ -65,10 +75,9 @@ public class ModdedToggleOption : ModdedOption<bool>
return;
}
var toggleOpt = OptionBehaviour as ToggleOption;
if (toggleOpt)
if (OptionBehaviour is ToggleOption toggleOpt)
{
toggleOpt.CheckMark.enabled = newValue;
}
}
}
}
+12 -2
View File
@@ -1,7 +1,17 @@
namespace MiraAPI.Hud;
/// <summary>
/// The location of the custom button on the screen.
/// </summary>
public enum ButtonLocation
{
/// <summary>
/// Placed in the bottom left.
/// </summary>
BottomLeft,
BottomRight
}
/// <summary>
/// Placed in the bottom right.
/// </summary>
BottomRight,
}
+110 -57
View File
@@ -1,6 +1,4 @@
#nullable enable
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using MiraAPI.Utilities.Assets;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
@@ -8,70 +6,70 @@ using UnityEngine.UI;
namespace MiraAPI.Hud;
/// <summary>
/// Class for making custom action buttons. More customizable than the default Action/Ability buttons in the base game
/// Class for making custom action buttons. More customizable than the default Action/Ability buttons in the base game.
/// </summary>
public abstract class CustomActionButton
{
/// <summary>
/// The name and text of the button.
/// Gets the name and text of the button.
/// </summary>
public abstract string Name { get; }
/// <summary>
/// How long the button takes to cooldown.
/// Gets the button's cooldown duration in seconds.
/// </summary>
public abstract float Cooldown { get; }
/// <summary>
/// If the button has an effect ability, how long the effect should last. To disable effects, set to 0.
/// Gets the button's effect duration in seconds. If the button has no effect, set to 0.
/// </summary>
public abstract float EffectDuration { get; }
/// <summary>
/// If the button has limited uses, how many uses it has. To make the button infinite, set to 0.
/// Gets the maximum amount of uses the button has. If the button has infinite uses, set to 0.
/// </summary>
public abstract int MaxUses { get; }
/// <summary>
/// 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.
/// 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>
/// The location of the button on the screen
/// Gets the location of the button on the screen.
/// </summary>
public virtual ButtonLocation Location => ButtonLocation.BottomLeft;
/// <summary>
/// Returns true if the button has an effect ability.
/// Gets a value indicating whether the button has an effect ability.
/// </summary>
public bool HasEffect => EffectDuration > 0;
/// <summary>
/// Returns true if the button has limited uses.
/// Gets a value indicating whether the button has limited uses.
/// </summary>
public bool LimitedUses => MaxUses > 0;
/// <summary>
/// Returns true if the effect is currently active.
/// Gets or sets a value indicating whether the effect is currently active, if there is one.
/// </summary>
protected bool EffectActive;
public bool EffectActive { get; protected set; }
/// <summary>
/// A timer variable to measure cooldowns and effects.
/// Gets or sets the amount of uses left.
/// </summary>
protected float Timer;
public int UsesLeft { get; protected set; }
/// <summary>
/// Returns the amount of uses left.
/// Gets or sets the timer variable to measure cooldowns and effects.
/// </summary>
protected int UsesLeft;
public float Timer { get; protected set; }
/// <summary>
/// The button object in game. This is created by Mira API automatically.
/// Gets the button object in game. This is created by Mira API automatically.
/// </summary>
protected ActionButton? Button { get; private set; }
internal void CreateButton(Transform parent)
{
if (Button)
@@ -85,7 +83,7 @@ public abstract class CustomActionButton
Button = Object.Instantiate(HudManager.Instance.AbilityButton, parent);
Button.name = Name + "Button";
Button.OverrideText(Name.ToUpper());
Button.OverrideText(Name.ToUpperInvariant());
Button.graphic.sprite = Sprite.LoadAsset();
@@ -101,23 +99,23 @@ public abstract class CustomActionButton
}
/// <summary>
/// A utility function to reset the cooldown of the button.
/// A utility function to reset the cooldown and/or effect of the button.
/// </summary>
public void ResetCooldown()
public void ResetCooldownAndOrEffect()
{
Timer = Cooldown;
if (EffectActive)
{
OnEffectEnd();
}
EffectActive = false;
}
/// <summary>
/// A utility function to override the sprite of the button.
/// </summary>
/// <param name="sprite">The new sprite to override with</param>
/// <param name="sprite">The new sprite to override with.</param>
public void OverrideSprite(Sprite sprite)
{
if (Button != null)
@@ -129,20 +127,76 @@ public abstract class CustomActionButton
/// <summary>
/// A utility function to override the name of the button.
/// </summary>
/// <param name="name">The new name to override with</param>
/// <param name="name">The new name to override with.</param>
public void OverrideName(string name)
{
Button?.OverrideText(name);
}
/// <summary>
/// A utility function that runs with the local PlayerControl's FixedUpdate.
/// Set the button's timer.
/// </summary>
/// <param name="playerControl">the local PlayerControl</param>
protected virtual void FixedUpdate(PlayerControl playerControl) { }
/// <param name="time">The time you want to set to.</param>
public void SetTimer(float time)
{
Timer = Mathf.Clamp(time, -1, float.MaxValue);
}
/// <summary>
/// Runs when the button is clicked.
/// Increase the button's timer.
/// </summary>
/// <param name="amount">The amount you want to increase by.</param>
public void IncreaseTimer(float amount)
{
SetTimer(Timer + amount);
}
/// <summary>
/// Decrease the button's timer.
/// </summary>
/// <param name="amount">The amount you want to decrease by.</param>
public void DecreaseTimer(float amount)
{
SetTimer(Timer - amount);
}
/// <summary>
/// Set the amount of uses this button has left.
/// </summary>
/// <param name="amount">The amount you want to set to.</param>
public void SetUses(int amount)
{
UsesLeft = Mathf.Clamp(amount, 0, int.MaxValue);
}
/// <summary>
/// Increase the amount of uses this button has left.
/// </summary>
/// <param name="amount">The amount you want to increase by. Default: 1.</param>
public void IncreaseUses(int amount = 1)
{
SetUses(UsesLeft + amount);
}
/// <summary>
/// Decrease the amount of uses this button has left.
/// </summary>
/// <param name="amount">The amount you want to decrease by. Default: 1.</param>
public void DecreaseUses(int amount = 1)
{
SetUses(UsesLeft - amount);
}
/// <summary>
/// A utility function that runs with the local PlayerControl's FixedUpdate if the button is enabled.
/// </summary>
/// <param name="playerControl">the local PlayerControl.</param>
protected virtual void FixedUpdate(PlayerControl playerControl)
{
}
/// <summary>
/// Callback method for the button click event.
/// </summary>
protected abstract void OnClick();
@@ -151,12 +205,16 @@ public abstract class CustomActionButton
/// True means the button is active, false means the button is disabled.
/// </summary>
/// <param name="role">The role of the local player.</param>
/// <returns>True if the button is enabled, false otherwise.</returns>
public abstract bool Enabled(RoleBehaviour? role);
/// <summary>
/// Given that there is an effect, this method that runs when the effect ends.
/// Given that there is an effect, this method runs when the effect ends.
/// <br /> <br /> THIS IS A CALLBACK METHOD! Use <see cref="ResetCooldownAndOrEffect" /> if you want to end the effect.
/// </summary>
public virtual void OnEffectEnd() { }
public virtual void OnEffectEnd()
{
}
/// <summary>
/// When the button is enabled, this method is called to determine if the button can be used.
@@ -174,7 +232,7 @@ public abstract class CustomActionButton
/// The default behavior is to show the button if the <paramref name="visible"/> parameter and the <see cref="Enabled"/> method return true.
/// It can be overridden for custom behavior.
/// </summary>
/// <param name="visible">Passed in from HudManager.SetHudActive, should hud be active?</param>
/// <param name="visible">Passed in from HudManager.SetHudActive, should hud be active.</param>
/// <param name="role">Passed in from HudManager.SetHudActive, the current role of the player.</param>
public virtual void SetActive(bool visible, RoleBehaviour role)
{
@@ -217,7 +275,7 @@ public abstract class CustomActionButton
/// By default, it handles the cooldown and effect timers, and sets the button to enabled or disabled.
/// It can be overridden for custom behavior.
/// </summary>
/// <param name="playerControl">The local PlayerControl</param>
/// <param name="playerControl">The local PlayerControl.</param>
public virtual void FixedUpdateHandler(PlayerControl playerControl)
{
if (Timer >= 0)
@@ -239,7 +297,7 @@ public abstract class CustomActionButton
{
Button?.SetDisabled();
}
Button?.SetCoolDown(Timer, EffectActive ? EffectDuration : Cooldown);
FixedUpdate(playerControl);
@@ -253,43 +311,38 @@ public abstract class CustomActionButton
public abstract class CustomActionButton<T> : CustomActionButton where T : MonoBehaviour
{
/// <summary>
/// The target object of the button.
/// Gets or sets the target object of the button.
/// </summary>
public T? Target { get; private set; }
public T? Target { get; protected set; }
/// <summary>
/// The distance the player must be from the target object to use the button.
/// Gets the distance the player must be from the target object to use the button.
/// </summary>
public virtual float Distance => PlayerControl.LocalPlayer.Data.Role.GetAbilityDistance();
/// <summary>
/// An optional collider tag to filter the target object by.
/// </summary>
public virtual string? ColliderTag => null;
/// <summary>
/// Determines if the target object is valid.
/// </summary>
public virtual bool IsTargetValid(T target)
/// <param name="target">The target object being checked.</param>
/// <returns>True if the target object is valid, false otherwise.</returns>
public virtual bool IsTargetValid(T? target)
{
return target;
}
/// <summary>
/// The method used to get the target object.
/// </summary>
public virtual T? GetTarget()
{
return PlayerControl.LocalPlayer.GetNearestObjectOfType<T>(Distance, ColliderTag, IsTargetValid);
}
/// <returns>The target object or null if it isn't found.</returns>
public abstract T? GetTarget();
/// <summary>
/// Sets the outline of the target object.
/// </summary>
/// <param name="active">Should the outline be active</param>
/// <param name="active">Should the outline be active.</param>
public abstract void SetOutline(bool active);
/// <inheritdoc />
public override bool CanUse()
{
var newTarget = GetTarget();
@@ -298,9 +351,9 @@ public abstract class CustomActionButton<T> : CustomActionButton where T : MonoB
SetOutline(false);
}
Target = newTarget;
Target = IsTargetValid(newTarget) ? newTarget : null;
SetOutline(true);
return base.CanUse() && Target;
}
}
}
+2 -2
View File
@@ -22,10 +22,10 @@ internal static class CustomButtonManager
Logger<MiraApiPlugin>.Error($"Failed to create button from {buttonType.Name}");
return;
}
CustomButtons.Add(button);
typeof(CustomButtonSingleton<>).MakeGenericType(buttonType)
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
.SetValue(null, button);
}
}
}
+9 -23
View File
@@ -1,6 +1,4 @@
#nullable enable
using System;
using System.Linq;
using System.Linq;
namespace MiraAPI.Hud;
@@ -8,26 +6,14 @@ namespace MiraAPI.Hud;
/// A utility class to get the instance of a custom action button.
/// </summary>
/// <typeparam name="T">The type of the button you are trying to access.</typeparam>
public class CustomButtonSingleton<T> where T : CustomActionButton
public static class CustomButtonSingleton<T> where T : CustomActionButton
{
private static T? _instance;
public static T Instance
{
get => _instance ??= CustomButtonManager.CustomButtons.OfType<T>().Single();
set
{
if (_instance == value)
{
return;
}
if (_instance != null)
{
throw new InvalidOperationException($"Instance for {typeof(T)} is already set");
}
_instance = value;
}
}
}
/// <summary>
/// Gets the instance of the button.
/// </summary>
#pragma warning disable CA1000
public static T Instance => _instance ??= CustomButtonManager.CustomButtons.OfType<T>().Single();
#pragma warning restore CA1000
}
+1 -1
View File
@@ -6,4 +6,4 @@ namespace MiraAPI.Hud;
/// Attribute to register a button in the HUD. Necessary for the button to be recognized by Mira.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class RegisterButtonAttribute : Attribute;
public class RegisterButtonAttribute : Attribute;
+6 -29
View File
@@ -1,15 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<DebugType>embedded</DebugType>
<PackageId>AllOfUs.MiraAPI</PackageId>
<Version>0.1.0</Version>
<Description>Simple and easy to use Among Us modding API.</Description>
<Authors>Angxl, XtraCube</Authors>
<Company>All Of Us</Company>
<PackageLicenseExpression>LGPL-2.1-or-later</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageId>AllOfUs.MiraAPI</PackageId>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
@@ -17,27 +12,9 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Checkmark.png" />
<EmbeddedResource Include="Resources\CheckMarkBox.png" />
<EmbeddedResource Include="Resources\Cog.png" />
<EmbeddedResource Include="Resources\CogActive.png" />
<EmbeddedResource Include="Resources\NextButton.png" />
<EmbeddedResource Include="Resources\NextButtonActive.png" />
<EmbeddedResource Include="Resources\Empty.png" />
</ItemGroup>
<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.8.13" PrivateAssets="all" />
<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime">
<IncludeAssets>compile; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup>
<Target Name="Copy" AfterTargets="Build" Condition="'$(AmongUs)' != ''">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(AmongUs)/BepInEx/plugins/" />
</Target>
<Import Project="../AmongUs.props" />
</Project>
+4 -5
View File
@@ -2,7 +2,6 @@
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
@@ -17,16 +16,16 @@ namespace MiraAPI;
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class MiraApiPlugin : BasePlugin
{
public Harmony Harmony { get; } = new(Id);
private static MiraPluginManager PluginManager { get; set; }
private static MiraPluginManager? PluginManager { get; set; }
public static Color MiraColor = new Color32(238, 154, 112, 255);
public Harmony Harmony { get; } = new(Id);
public static Color MiraColor { get; } = new Color32(238, 154, 112, 255);
public override void Load()
{
Harmony.PatchAll();
ReactorCredits.Register("Mira API", Version.Truncate(11, "") ?? Version, true, ReactorCredits.AlwaysShow);
ReactorCredits.Register("Mira API", Version, true, ReactorCredits.AlwaysShow);
PluginManager = new MiraPluginManager();
PluginManager.Initialize();
+76 -10
View File
@@ -1,14 +1,80 @@
namespace MiraAPI.Modifiers;
/// <summary>
/// Base class for all modifiers.
/// </summary>
public abstract class BaseModifier
{
public abstract string ModifierName { get; }
public virtual bool HideOnUi => false;
public PlayerControl Player { get; internal set; }
/// <summary>
/// Gets the player that the modifier is attached to.
/// </summary>
public PlayerControl? Player { get; internal set; }
/// <summary>
/// Gets the modifier component that the modifier is attached to.
/// </summary>
public ModifierComponent? ModifierComponent { get; internal set; }
/// <summary>
/// Gets the modifier id.
/// </summary>
public uint ModifierId { get; internal set; }
public virtual void OnActivate() { }
public virtual void OnDeactivate() { }
public virtual void Update() { }
public virtual void FixedUpdate() { }
public virtual void OnDeath() { }
public virtual bool CanVent() => Player.Data.Role.CanVent;
}
/// <summary>
/// Gets the modifier name.
/// </summary>
public abstract string ModifierName { get; }
/// <summary>
/// Gets a value indicating whether the modifier is hidden on the UI.
/// </summary>
public virtual bool HideOnUi => false;
/// <summary>
/// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if <see cref="HideOnUi"/> is true.
/// </summary>
/// <returns>The information string for the HUD.</returns>
public virtual string GetHudString() => ModifierName;
/// <summary>
/// Called when the modifier is activated.
/// </summary>
public virtual void OnActivate()
{
}
/// <summary>
/// Called when the modifier is deactivated.
/// </summary>
public virtual void OnDeactivate()
{
}
/// <summary>
/// Called when the modifier is updated. Attached to the ModifierComponent's Update method.
/// </summary>
public virtual void Update()
{
}
/// <summary>
/// Called when the modifier is updated. Attached to the ModifierComponent's FixedUpdate method.
/// </summary>
public virtual void FixedUpdate()
{
}
/// <summary>
/// Called when the player dies.
/// </summary>
/// <param name="reason">The Death Reason.</param>
public virtual void OnDeath(DeathReason reason)
{
}
/// <summary>
/// Determines whether the player can vent.
/// </summary>
/// <returns>True if the player can vent, false otherwise.</returns>
public virtual bool CanVent() => Player?.Data.Role.CanVent == true;
}
+195 -81
View File
@@ -1,33 +1,58 @@
using MiraAPI.Modifiers.Types;
using MiraAPI.Networking;
using MiraAPI.Utilities;
using Reactor.Networking.Attributes;
using Reactor.Utilities;
using Reactor.Utilities.Attributes;
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities;
using Reactor.Utilities;
using Reactor.Utilities.Attributes;
using TMPro;
using UnityEngine;
namespace MiraAPI.Modifiers;
/// <summary>
/// The component for handling modifiers.
/// </summary>
[RegisterInIl2Cpp]
public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr)
public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
public List<BaseModifier> ActiveModifiers { get; private set; }
/// <summary>
/// Gets the active modifiers on the player.
/// </summary>
[HideFromIl2Cpp]
public ImmutableList<BaseModifier> ActiveModifiers => Modifiers.ToImmutableList();
public PlayerControl player;
[HideFromIl2Cpp]
private List<BaseModifier> Modifiers { get; set; } = [];
private TextMeshPro _modifierText;
private PlayerControl? _player;
public void Start()
private TextMeshPro? _modifierText;
private readonly List<BaseModifier> _toRemove = [];
private readonly List<BaseModifier> _toAdd = [];
internal void ClearModifiers()
{
player = GetComponent<PlayerControl>();
ActiveModifiers = [];
foreach (var modifier in Modifiers)
{
modifier.OnDeactivate();
}
if (!player.AmOwner)
_toRemove.AddRange(Modifiers);
}
private void Start()
{
_player = GetComponent<PlayerControl>();
Modifiers = [];
if (!_player.AmOwner)
{
return;
}
@@ -36,55 +61,80 @@ public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr)
_modifierText.verticalAlignment = VerticalAlignmentOptions.Top;
}
public void FixedUpdate()
private void FixedUpdate()
{
foreach (var modifier in ActiveModifiers)
foreach (var modifier in _toRemove)
{
modifier.OnDeactivate();
Modifiers.Remove(modifier);
}
foreach (var modifier in _toAdd)
{
Modifiers.Add(modifier);
modifier.OnActivate();
if (_player?.AmOwner == true && modifier is TimedModifier { AutoStart: true } timer)
{
timer.StartTimer();
}
}
if (_toAdd.Count > 0 || _toRemove.Count > 0)
{
if (_player?.AmOwner == true)
{
HudManager.Instance?.SetHudActive(true);
}
_toAdd.Clear();
_toRemove.Clear();
}
foreach (var modifier in Modifiers)
{
modifier.FixedUpdate();
}
}
public void Update()
private void Update()
{
foreach (var modifier in ActiveModifiers)
foreach (var modifier in Modifiers)
{
modifier.Update();
}
var filteredModifiers = ActiveModifiers.Where(mod => !mod.HideOnUi);
if (!_modifierText || _player?.AmOwner == false)
{
return;
}
var filteredModifiers = Modifiers.Where(mod => !mod.HideOnUi);
var baseModifiers = filteredModifiers as BaseModifier[] ?? filteredModifiers.ToArray();
if (player.AmOwner && baseModifiers.Any())
if (baseModifiers.Length != 0)
{
var stringBuild = new StringBuilder();
foreach (var mod in baseModifiers)
{
stringBuild.Append($"\n{mod.ModifierName}");
if (mod is TimedModifier timer)
{
stringBuild.Append($" <size=70%>({Math.Round(timer.Duration - timer.TimeRemaining, 0)}s/{timer.Duration}s)</size>");
}
stringBuild.Append(CultureInfo.InvariantCulture, $"\n{mod.GetHudString()}");
}
_modifierText.text = $"<b><size=130%>Modifiers:</b></size>{stringBuild}";
_modifierText!.text = $"<b><size=130%>Modifiers:</b></size>{stringBuild}";
}
else if (player.AmOwner && !baseModifiers.Any() && _modifierText.text != string.Empty)
else if (_modifierText!.text != string.Empty)
{
_modifierText.text = string.Empty;
}
}
public static void RemoveModifier(PlayerControl target, uint modifierId)
/// <summary>
/// Removes a modifier from the player.
/// </summary>
/// <param name="type">The modifier type.</param>
[HideFromIl2Cpp]
public void RemoveModifier(Type type)
{
if (!ModifierManager.IdToTypeModifiers.TryGetValue(modifierId, out var type))
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier with id {modifierId} because it is not registered.");
return;
}
var modifierComponent = target.GetModifierComponent();
var modifier = modifierComponent.ActiveModifiers.Find(x => x.GetType() == type);
var modifier = Modifiers.Find(x => x.GetType() == type);
if (modifier is null)
{
@@ -92,58 +142,122 @@ public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr)
return;
}
modifier.OnDeactivate();
modifierComponent.ActiveModifiers.Remove(modifier);
if (target.AmOwner)
{
HudManager.Instance.SetHudActive(true);
}
RemoveModifier(modifier);
}
public static void AddModifier(PlayerControl target, uint modifierId)
/// <summary>
/// Removes a modifier from the player.
/// </summary>
/// <typeparam name="T">The modifier type.</typeparam>
[HideFromIl2Cpp]
public void RemoveModifier<T>() where T : BaseModifier
{
if (!ModifierManager.IdToTypeModifiers.TryGetValue(modifierId, out var type))
{
Logger<MiraApiPlugin>.Error($"Cannot add modifier with id {modifierId} because it is not registered.");
return;
}
RemoveModifier(typeof(T));
}
if (target.HasModifier(modifierId))
{
Logger<MiraApiPlugin>.Error($"Player already has modifier with id {modifierId}!");
return;
}
var modifier = (BaseModifier)Activator.CreateInstance(type);
var modifierComponent = target.GetModifierComponent();
/// <summary>
/// Removes a modifier from the player.
/// </summary>
/// <param name="modifierId">The modifier ID.</param>
[HideFromIl2Cpp]
public void RemoveModifier(uint modifierId)
{
var modifier = Modifiers.Find(x => x.ModifierId == modifierId);
if (modifier is null)
{
Logger<MiraApiPlugin>.Error($"Cannot add modifier {type.Name} because it is null.");
Logger<MiraApiPlugin>.Error($"Cannot remove modifier with id {modifierId} because it is not active.");
return;
}
modifierComponent.ActiveModifiers.Add(modifier);
modifier.Player = modifierComponent.player;
modifier.ModifierId = modifierId;
modifier.OnActivate();
if (target.AmOwner)
{
if (modifier is TimedModifier { AutoStart: true } timer)
{
timer.StartTimer();
}
HudManager.Instance.SetHudActive(true);
}
RemoveModifier(modifier);
}
[MethodRpc((uint)MiraRpc.RemoveModifier)]
public static void RpcRemoveModifier(PlayerControl target, uint modifierId) => RemoveModifier(target, modifierId);
/// <summary>
/// Removes a modifier from the player.
/// </summary>
/// <param name="modifier">The modifier object.</param>
[HideFromIl2Cpp]
public void RemoveModifier(BaseModifier modifier)
{
if (!Modifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player.");
return;
}
[MethodRpc((uint)MiraRpc.AddModifier)]
public static void RpcAddModifier(PlayerControl target, uint modifierId) => AddModifier(target, modifierId);
}
_toRemove.Add(modifier);
}
/// <summary>
/// Adds a modifier to the player.
/// </summary>
/// <param name="modifier">The modifier to add.</param>
/// <returns>The modifier that was added.</returns>
[HideFromIl2Cpp]
public BaseModifier? AddModifier(BaseModifier modifier)
{
if (Modifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Player already has modifier with id {modifier.ModifierId}!");
return null;
}
var modifierId = ModifierManager.GetModifierId(modifier.GetType());
if (modifierId == null)
{
Logger<MiraApiPlugin>.Error($"Cannot add modifier {modifier.GetType().Name} because it has no ID!");
return null;
}
_toAdd.Add(modifier);
modifier.Player = _player;
modifier.ModifierComponent = this;
modifier.ModifierId = modifierId.Value;
return modifier;
}
/// <summary>
/// Adds a modifier to the player.
/// </summary>
/// <param name="type">The modifier type.</param>
/// <returns>The modifier that was added.</returns>
[HideFromIl2Cpp]
public BaseModifier? AddModifier(Type type)
{
var modifierId = ModifierManager.GetModifierId(type);
if (modifierId == null)
{
Logger<MiraApiPlugin>.Error($"Cannot add modifier {type.Name} because it is not registered.");
return null;
}
if (Modifiers.Find(x => x.ModifierId == modifierId) != null)
{
Logger<MiraApiPlugin>.Error($"Player already has modifier with id {modifierId}!");
return null;
}
if (Activator.CreateInstance(type) is not BaseModifier modifier)
{
Logger<MiraApiPlugin>.Error($"Cannot add modifier {type.Name} because it is null.");
return null;
}
AddModifier(modifier);
return modifier;
}
/// <summary>
/// Adds a modifier to the player.
/// </summary>
/// <typeparam name="T">The Type of the modifier.</typeparam>
/// <returns>The new modifier.</returns>
[HideFromIl2Cpp]
public T? AddModifier<T>() where T : BaseModifier
{
return AddModifier(typeof(T)) as T;
}
}
+90 -45
View File
@@ -1,19 +1,24 @@
using MiraAPI.Modifiers.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Modifiers.Types;
using MiraAPI.Networking;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using Random = System.Random;
namespace MiraAPI.Modifiers;
/// <summary>
/// The manager for handling modifiers.
/// </summary>
public static class ModifierManager
{
public static readonly Dictionary<uint, Type> IdToTypeModifiers = [];
public static readonly Dictionary<Type, uint> TypeToIdModifiers = [];
private static readonly Dictionary<uint, Type> IdToTypeModifierMap = [];
private static readonly Dictionary<Type, uint> TypeToIdModifierMap = [];
private static uint _nextId;
@@ -23,6 +28,26 @@ public static class ModifierManager
return _nextId;
}
/// <summary>
/// Gets the modifier type from the id.
/// </summary>
/// <param name="id">The ID.</param>
/// <returns>The Type of the modifier.</returns>
public static Type? GetModifierType(uint id)
{
return IdToTypeModifierMap.GetValueOrDefault(id);
}
/// <summary>
/// Gets the modifier id from the type.
/// </summary>
/// <param name="type">The Type.</param>
/// <returns>The ID of the modifier.</returns>
public static uint? GetModifierId(Type type)
{
return TypeToIdModifierMap.GetValueOrDefault(type);
}
internal static void RegisterModifier(Type modifierType)
{
if (!typeof(BaseModifier).IsAssignableFrom(modifierType))
@@ -30,8 +55,8 @@ public static class ModifierManager
return;
}
IdToTypeModifiers.Add(GetNextId(), modifierType);
TypeToIdModifiers.Add(modifierType, _nextId);
IdToTypeModifierMap.Add(GetNextId(), modifierType);
TypeToIdModifierMap.Add(modifierType, _nextId);
}
internal static void AssignModifiers(List<PlayerControl> plrs)
@@ -40,10 +65,23 @@ public static class ModifierManager
List<uint> filteredModifiers = [];
foreach (var modifier in IdToTypeModifiers)
foreach (var modifier in IdToTypeModifierMap.Where(pair => pair.Value.IsAssignableTo(typeof(GameModifier))))
{
var mod = (GameModifier)Activator.CreateInstance(modifier.Value);
var num = mod.GetAmountPerGame();
if (Activator.CreateInstance(modifier.Value) is not GameModifier mod)
{
Logger<MiraApiPlugin>.Error($"Failed to create instance of {modifier.Value.Name}");
continue;
}
if (!plrs.Exists(x => IsGameModifierValid(x, mod, modifier.Key)))
{
Logger<MiraApiPlugin>.Warning("No players are valid for modifier: " + mod.ModifierName);
continue;
}
var maxCount = plrs.Count(x => IsGameModifierValid(x, mod, modifier.Key));
var num = Math.Clamp(mod.GetAmountPerGame(), 0, maxCount);
var chance = mod.GetAssignmentChance();
for (var i = 0; i < num; i++)
@@ -52,7 +90,7 @@ public static class ModifierManager
if (randomNum < Math.Clamp(chance, 0, 100))
{
filteredModifiers.Add(TypeToIdModifiers[modifier.Value]);
filteredModifiers.Add(TypeToIdModifierMap[modifier.Value]);
}
}
}
@@ -67,47 +105,47 @@ public static class ModifierManager
{
var id = shuffledModifiers[0];
var mod = (GameModifier)Activator.CreateInstance(IdToTypeModifiers[id]);
var plr = plrs.Random();
if (plr.Data.Role is ICustomRole modRole)
if (Activator.CreateInstance(IdToTypeModifierMap[id]) is not GameModifier mod)
{
if (!modRole.IsModifierApplicable(mod))
{
continue;
}
Logger<MiraApiPlugin>.Error($"Failed to create instance of {IdToTypeModifierMap[id].Name}");
continue;
}
if (plr.HasModifier(id) || mod.IsModifierValidOn(plr.Data.Role))
if (!plrs.Exists(x => IsGameModifierValid(x, mod, id)))
{
shuffledModifiers.RemoveAt(0);
continue;
}
var plr = plrs.Random();
if (plr == null || !IsGameModifierValid(plr, mod, id))
{
continue;
}
shuffledModifiers.RemoveAt(0);
ModifierComponent.RpcAddModifier(plr, id);
plr.RpcAddModifier(id);
}
}
private static bool IsGameModifierValid(PlayerControl player, GameModifier modifier, uint modifierId)
{
return (player.Data.Role is not ICustomRole role || role.IsModifierApplicable(modifier)) &&
modifier.IsModifierValidOn(player.Data.Role) &&
!player.HasModifier(modifierId);
}
internal static void SyncAllModifiers(int targetId = -1)
{
var data = new List<NetData>();
if (targetId == -1)
foreach (var player in PlayerControl.AllPlayerControls)
{
foreach (var player in GameData.Instance.AllPlayers)
{
data.Add(GetPlayerModifiers(player.Object));
}
}
else
{
var player = GameData.Instance.GetPlayerById((byte)targetId).Object;
data.Add(GetPlayerModifiers(player));
}
Rpc<SyncModifiersRpc>.Instance.Send(PlayerControl.LocalPlayer, data.ToArray(), true);
Rpc<SyncModifiersRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, [.. data]);
}
internal static void HandleSyncModifiers(NetData[] data)
@@ -125,33 +163,40 @@ public static class ModifierManager
continue;
}
foreach (var modifier in modifierComponent.ActiveModifiers)
{
modifier.OnDeactivate();
}
modifierComponent.ActiveModifiers.Clear();
modifierComponent.ClearModifiers();
foreach (var id in ids)
{
ModifierComponent.AddModifier(plr, id);
if (!IdToTypeModifierMap.TryGetValue(id, out var type))
{
Logger<MiraApiPlugin>.Error($"Cannot add modifier with id {id} because it is not registered.");
continue;
}
modifierComponent.AddModifier(type);
}
}
}
private static NetData GetPlayerModifiers(PlayerControl player)
private static NetData GetPlayerModifiers(PlayerControl? player)
{
var bytes = new List<byte>();
if (player == null || !player)
{
return new NetData(0, []);
}
List<byte> bytes = [];
var modifierComponent = player.GetComponent<ModifierComponent>();
if (!modifierComponent)
if (modifierComponent == null || !modifierComponent)
{
return new NetData(player.PlayerId, []);
}
foreach (var modifier in modifierComponent.ActiveModifiers)
{
bytes.AddRange(BitConverter.GetBytes(TypeToIdModifiers[modifier.GetType()]));
bytes.AddRange(BitConverter.GetBytes(TypeToIdModifierMap[modifier.GetType()]));
}
return new NetData(player.PlayerId, bytes.ToArray());
return new NetData(player.PlayerId, [.. bytes]);
}
}
}
@@ -2,5 +2,8 @@
namespace MiraAPI.Modifiers;
/// <summary>
/// Marks a class as a modifier that can be registered with the <see cref="ModifierManager"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class RegisterModifierAttribute : Attribute;
public class RegisterModifierAttribute : Attribute;
+20 -1
View File
@@ -1,7 +1,26 @@
namespace MiraAPI.Modifiers.Types;
/// <summary>
/// The base class for a game modifier. Game modifiers are applied at the start of the game on top of the player's role.
/// </summary>
public abstract class GameModifier : BaseModifier
{
/// <summary>
/// Gets the chance of the modifier being assigned to a player.
/// </summary>
/// <returns>An int value between 0 and 100 representing percent.</returns>
public abstract int GetAssignmentChance();
/// <summary>
/// Gets the amount of players that can have this modifier in a game.
/// </summary>
/// <returns>An int value greater than or equal to zero.</returns>
public abstract int GetAmountPerGame();
/// <summary>
/// Determines whether the modifier is valid on a role.
/// </summary>
/// <param name="role">The role to be checked.</param>
/// <returns>True if the modifier is valid on the role, otherwise false.</returns>
public virtual bool IsModifierValidOn(RoleBehaviour role) => true;
}
}
+50 -3
View File
@@ -1,20 +1,59 @@
using Reactor.Utilities;
using System;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Modifiers.Types;
/// <summary>
/// The base class for all timed modifiers. Timed modifiers have a duration and can be started and stopped.
/// </summary>
public abstract class TimedModifier : BaseModifier
{
/// <summary>
/// Gets the duration of the modifier.
/// </summary>
public abstract float Duration { get; }
/// <summary>
/// Gets a value indicating whether the timer should start automatically when added.
/// </summary>
public virtual bool AutoStart => false;
/// <summary>
/// Gets a value indicating whether the modifier should be removed when the timer completes.
/// </summary>
public virtual bool RemoveOnComplete => true;
/// <summary>
/// Called when the timer completes.
/// </summary>
public abstract void OnTimerComplete();
/// <summary>
/// Gets or sets a value indicating whether the timer is active.
/// </summary>
public bool TimerActive { get; protected set; }
/// <summary>
/// Gets or sets the time remaining on the timer.
/// </summary>
public float TimeRemaining { get; protected set; }
/// <summary>
/// Gets the HUD information for Timed Modifier, including the time remaining.
/// </summary>
/// <returns>A string with the hud information + timer.</returns>
public override string GetHudString()
{
return base.GetHudString() + $" <size=70%>({Math.Round(Duration - TimeRemaining, 0)}s/{Duration}s)</size>";
}
/// <summary>
/// The FixedUpdate method for timed modifiers. Automatically handles the timer logic.
/// </summary>
public override void FixedUpdate()
{
if (!Player.AmOwner)
if (Player?.AmOwner == false)
{
return;
}
@@ -28,9 +67,17 @@ public abstract class TimedModifier : BaseModifier
TimerActive = false;
TimeRemaining = Duration;
OnTimerComplete();
if (RemoveOnComplete)
{
ModifierComponent?.RemoveModifier(ModifierId);
}
}
}
/// <summary>
/// Starts the timer.
/// </summary>
public void StartTimer()
{
if (TimerActive)
@@ -42,4 +89,4 @@ public abstract class TimedModifier : BaseModifier
TimerActive = true;
TimeRemaining = Duration;
}
}
}
+271
View File
@@ -0,0 +1,271 @@
using System.Collections;
using System.Linq;
using AmongUs.GameOptions;
using Assets.CoreScripts;
using BepInEx.Unity.IL2CPP.Utils;
using Reactor.Networking.Attributes;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using UnityEngine;
namespace MiraAPI.Networking;
/// <summary>
/// Custom murder RPCs to fix issues with default ones.
/// </summary>
public static class CustomMurderRpc
{
/// <summary>
/// Networked Custom Murder method.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
/// <param name="didSucceed">Whether the murder was successful or not.</param>
/// <param name="resetKillTimer">Should the kill timer be reset.</param>
/// <param name="createDeadBody">Should a dead body be created.</param>
/// <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)]
public static void RpcCustomMurder(
this PlayerControl source,
PlayerControl target,
bool didSucceed = true,
bool resetKillTimer = true,
bool createDeadBody = true,
bool teleportMurderer = true,
bool showKillAnim = true,
bool playKillSound = true)
{
var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError;
var murderResultFlags2 = MurderResultFlags.DecisionByHost | murderResultFlags;
source.CustomMurder(
target,
murderResultFlags2,
resetKillTimer,
createDeadBody,
teleportMurderer,
showKillAnim,
playKillSound);
}
/// <summary>
/// Custom Murder method without networking. If you need a networked version, use <see cref="RpcCustomMurder"/>.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
/// <param name="resultFlags">Murder result flags.</param>
/// <param name="resetKillTimer">Should the kill timer be reset.</param>
/// <param name="createDeadBody">Should a dead body be created.</param>
/// <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>
public static void CustomMurder(
this PlayerControl source,
PlayerControl target,
MurderResultFlags resultFlags,
bool resetKillTimer = true,
bool createDeadBody = true,
bool teleportMurderer = true,
bool showKillAnim = true,
bool playKillSound = true)
{
source.isKilling = false;
Logger<MiraApiPlugin>.Error($"{source.PlayerId} trying to murder {target.PlayerId}");
var data = target.Data;
if (resultFlags.HasFlag(MurderResultFlags.FailedError))
{
return;
}
if (resultFlags.HasFlag(MurderResultFlags.FailedProtected) ||
(resultFlags.HasFlag(MurderResultFlags.DecisionByHost) && target.protectedByGuardianId > -1))
{
target.protectedByGuardianThisRound = true;
var flag = PlayerControl.LocalPlayer.Data.Role.Role == RoleTypes.GuardianAngel;
if (flag && PlayerControl.LocalPlayer.Data.PlayerId == target.protectedByGuardianId)
{
StatsManager.Instance.IncrementStat(StringNames.StatsGuardianAngelCrewmatesProtected);
DestroyableSingleton<AchievementManager>.Instance.OnProtectACrewmate();
}
if (source.AmOwner || flag)
{
target.ShowFailedMurder();
if (resetKillTimer)
{
source.SetKillTimer(
GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown) / 2f);
}
}
else
{
target.RemoveProtection();
}
Logger<MiraApiPlugin>.Error($"{source.PlayerId} failed to murder {target.PlayerId} due to guardian angel protection");
return;
}
if (!resultFlags.HasFlag(MurderResultFlags.Succeeded) &&
!resultFlags.HasFlag(MurderResultFlags.DecisionByHost))
{
return;
}
DestroyableSingleton<DebugAnalytics>.Instance.Analytics.Kill(target.Data, source.Data);
if (source.AmOwner)
{
StatsManager.Instance.IncrementStat(
GameManager.Instance.IsHideAndSeek()
? StringNames.StatsImpostorKills_HideAndSeek
: StringNames.StatsImpostorKills);
if (source.CurrentOutfitType == PlayerOutfitType.Shapeshifted)
{
StatsManager.Instance.IncrementStat(StringNames.StatsShapeshifterShiftedKills);
}
if (Constants.ShouldPlaySfx() && playKillSound)
{
SoundManager.Instance.PlaySound(source.KillSfx, false, 0.8f);
}
if (resetKillTimer)
{
source.SetKillTimer(GameOptionsManager.Instance.CurrentGameOptions.GetFloat(FloatOptionNames.KillCooldown));
}
}
DestroyableSingleton<UnityTelemetry>.Instance.WriteMurder();
target.gameObject.layer = LayerMask.NameToLayer("Ghost");
if (target.AmOwner)
{
StatsManager.Instance.IncrementStat(StringNames.StatsTimesMurdered);
if (Minigame.Instance)
{
try
{
Minigame.Instance.Close();
Minigame.Instance.Close();
}
catch
{
// ignored
}
}
if (showKillAnim)
{
DestroyableSingleton<HudManager>.Instance.KillOverlay.ShowKillAnimation(source.Data, data);
}
target.cosmetics.SetNameMask(false);
target.RpcSetScanner(false);
}
DestroyableSingleton<AchievementManager>.Instance.OnMurder(
source.AmOwner,
target.AmOwner,
source.CurrentOutfitType == PlayerOutfitType.Shapeshifted,
source.shapeshiftTargetPlayerId,
target.PlayerId);
source.MyPhysics.StartCoroutine(source.KillAnimations.Random()?.CoPerformCustomKill(source, target, createDeadBody, teleportMurderer));
Logger<MiraApiPlugin>.Error($"{source.PlayerId} succeeded in murdering {target.PlayerId}");
}
/// <summary>
/// Perform a custom kill animation.
/// </summary>
/// <param name="anim">The kill animation.</param>
/// <param name="source">The murderer.</param>
/// <param name="target">The murdered player.</param>
/// <param name="createDeadBody">Should a dead body be created.</param>
/// <param name="teleportMurderer">Should the murder be teleported.</param>
/// <returns>Coroutine.</returns>
public static IEnumerator CoPerformCustomKill(
this KillAnimation anim,
PlayerControl source,
PlayerControl target,
bool createDeadBody = true,
bool teleportMurderer = true)
{
var cam = Camera.main?.GetComponent<FollowerCamera>();
var isParticipant = PlayerControl.LocalPlayer == source || PlayerControl.LocalPlayer == target;
var sourcePhys = source.MyPhysics;
if (teleportMurderer)
{
KillAnimation.SetMovement(source, false);
}
KillAnimation.SetMovement(target, false);
if (isParticipant)
{
PlayerControl.LocalPlayer.isKilling = true;
source.isKilling = true;
}
DeadBody? deadBody = null;
if (createDeadBody)
{
deadBody = Object.Instantiate(GameManager.Instance.DeadBodyPrefab);
deadBody.enabled = false;
deadBody.ParentId = target.PlayerId;
deadBody.bodyRenderers.ToList().ForEach(target.SetPlayerMaterialColors);
target.SetPlayerMaterialColors(deadBody.bloodSplatter);
var vector = target.transform.position + anim.BodyOffset;
vector.z = vector.y / 1000f;
deadBody.transform.position = vector;
}
if (isParticipant)
{
if (cam != null)
{
cam.Locked = true;
}
ConsoleJoystick.SetMode_Task();
if (PlayerControl.LocalPlayer.AmOwner)
{
PlayerControl.LocalPlayer.MyPhysics.inputHandler.enabled = true;
}
}
target.Die(DeathReason.Kill, true);
yield return source.MyPhysics.Animations.CoPlayCustomAnimation(anim.BlurAnim);
sourcePhys.Animations.PlayIdleAnimation();
if (teleportMurderer)
{
source.NetTransform.SnapTo(target.transform.position);
KillAnimation.SetMovement(source, true);
}
KillAnimation.SetMovement(target, true);
if (deadBody != null)
{
deadBody.enabled = true;
}
if (!isParticipant)
{
yield break;
}
if (cam != null)
{
cam.Locked = false;
}
PlayerControl.LocalPlayer.isKilling = false;
source.isKilling = false;
}
}
+34 -2
View File
@@ -1,10 +1,42 @@
namespace MiraAPI.Networking;
/// <summary>
/// Mira RPCs.
/// </summary>
public enum MiraRpc : uint
{
/// <summary>
/// Syncs the game options.
/// </summary>
SyncGameOptions,
/// <summary>
/// Syncs the role options.
/// </summary>
SyncRoleOptions,
/// <summary>
/// Adds a modifier to a player.
/// </summary>
AddModifier,
/// <summary>
/// Removes a modifier from a player.
/// </summary>
RemoveModifier,
SyncModifiers
}
/// <summary>
/// Syncs all modifiers at once.
/// </summary>
SyncModifiers,
/// <summary>
/// Custom Murder RPC.
/// </summary>
CustomMurder,
/// <summary>
/// Trigger Animation using CustomAnimator.
/// </summary>
TriggerAnimation,
}
+18 -1
View File
@@ -1,8 +1,25 @@
namespace MiraAPI.Networking;
/// <summary>
/// Used to network data and mark it with an ID.
/// </summary>
/// <param name="id">The ID of the data.</param>
/// <param name="data">The byte[] data.</param>
public readonly struct NetData(uint id, byte[] data)
{
/// <summary>
/// Gets the ID of the data.
/// </summary>
public uint Id { get; } = id;
/// <summary>
/// Gets the byte[] data.
/// </summary>
public byte[] Data { get; } = data;
/// <summary>
/// Gets the length of the data in bytes.
/// </summary>
/// <returns>An int representing the number of bytes this NetData takes up.</returns>
public int GetLength() => 4 + Data.Length;
}
}
+13 -7
View File
@@ -6,12 +6,18 @@ using Reactor.Networking.Rpc;
namespace MiraAPI.Networking;
[RegisterCustomRpc((uint)MiraRpc.SyncModifiers)]
internal class SyncModifiersRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, NetData[]>(plugin, id)
internal sealed class SyncModifiersRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, NetData[]>(plugin, id)
{
public override RpcLocalHandling LocalHandling => RpcLocalHandling.None;
public override void Write(MessageWriter writer, NetData[] data)
public override void Write(MessageWriter writer, NetData[]? data)
{
if (data == null)
{
writer.WritePacked(0U);
return;
}
writer.WritePacked((uint)data.Length);
foreach (var netData in data)
{
@@ -26,21 +32,21 @@ internal class SyncModifiersRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc
var data = new NetData[length];
for (var i = 0; i < length; i++)
{
var id = reader.ReadPackedUInt32();
var dataId = reader.ReadPackedUInt32();
var bytes = reader.ReadBytesAndSize();
data[i] = new NetData(id, bytes);
data[i] = new NetData(dataId, bytes);
}
return data;
}
public override void Handle(PlayerControl playerControl, NetData[] data)
public override void Handle(PlayerControl playerControl, NetData[]? data)
{
if (AmongUsClient.Instance.HostId != playerControl.OwnerId)
{
return;
}
ModifierManager.HandleSyncModifiers(data);
ModifierManager.HandleSyncModifiers(data ?? []);
}
}
}
+14 -8
View File
@@ -7,12 +7,18 @@ namespace MiraAPI.Networking;
// METHOD RPC DOESNT WORK WITH THE ARRAYS AND STUFF SO THIS IS HOW WE WILL DO IT FOR NOW
[RegisterCustomRpc((uint)MiraRpc.SyncGameOptions)]
internal class SyncOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, NetData[]>(plugin, id)
internal sealed class SyncOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, NetData[]>(plugin, id)
{
public override RpcLocalHandling LocalHandling => RpcLocalHandling.None;
public override void Write(MessageWriter writer, NetData[] data)
public override void Write(MessageWriter writer, NetData[]? data)
{
if (data == null)
{
writer.WritePacked(0U);
return;
}
writer.WritePacked((uint)data.Length);
foreach (var netData in data)
{
@@ -27,21 +33,21 @@ internal class SyncOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<M
var data = new NetData[length];
for (var i = 0; i < length; i++)
{
var id = reader.ReadPackedUInt32();
var dataId = reader.ReadPackedUInt32();
var bytes = reader.ReadBytesAndSize();
data[i] = new NetData(id, bytes);
data[i] = new NetData(dataId, bytes);
}
return data;
}
public override void Handle(PlayerControl playerControl, NetData[] data)
public override void Handle(PlayerControl playerControl, NetData[]? data)
{
if (AmongUsClient.Instance.HostId != playerControl.OwnerId)
{
return;
}
ModdedOptionsManager.HandleSyncOptions(data);
ModdedOptionsManager.HandleSyncOptions(data ?? []);
}
}
}
+13 -8
View File
@@ -6,13 +6,18 @@ using Reactor.Networking.Rpc;
namespace MiraAPI.Networking;
[RegisterCustomRpc((uint)MiraRpc.SyncRoleOptions)]
internal class SyncRoleOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, NetData[]>(plugin, id)
internal sealed class SyncRoleOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, NetData[]>(plugin, id)
{
public override RpcLocalHandling LocalHandling => RpcLocalHandling.None;
public override void Write(MessageWriter writer, NetData[] data)
public override void Write(MessageWriter writer, NetData[]? data)
{
if (data == null)
{
writer.WritePacked(0U);
return;
}
writer.WritePacked((uint)data.Length);
foreach (var netData in data)
{
@@ -27,21 +32,21 @@ internal class SyncRoleOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomR
var data = new NetData[length];
for (var i = 0; i < length; i++)
{
var id = reader.ReadPackedUInt32();
var dataId = reader.ReadPackedUInt32();
var bytes = reader.ReadBytesAndSize();
data[i] = new NetData(id, bytes);
data[i] = new NetData(dataId, bytes);
}
return data;
}
public override void Handle(PlayerControl playerControl, NetData[] data)
public override void Handle(PlayerControl playerControl, NetData[]? data)
{
if (AmongUsClient.Instance.HostId != playerControl.OwnerId)
{
return;
}
CustomRoleManager.HandleSyncRoleOptions(data);
CustomRoleManager.HandleSyncRoleOptions(data ?? []);
}
}
}
+4 -1
View File
@@ -6,6 +6,9 @@ using MiraAPI.Roles;
namespace MiraAPI.Patches;
/// <summary>
/// Sync all options, role settings, and modifiers to the player when they join the game.
/// </summary>
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.CreatePlayer))]
public static class AmongUsClientSyncPatch
{
@@ -25,4 +28,4 @@ public static class AmongUsClientSyncPatch
CustomRoleManager.SyncAllRoleSettings(clientData.Id);
ModifierManager.SyncAllModifiers(clientData.Id);
}
}
}
@@ -7,8 +7,8 @@ namespace MiraAPI.Patches.Colors;
[HarmonyPatch(typeof(PlayerTab))]
public static class ScrollingColorsPatch
{
/// Collider
private static BoxCollider2D _collider;
// Collider
private static BoxCollider2D? _collider;
/// <summary>
/// Add scrolling to the colors tab
@@ -20,7 +20,7 @@ public static class ScrollingColorsPatch
{
return;
}
var tab = PlayerCustomizationMenu.Instance.Tabs[1].Tab;
if (__instance.scroller == null)
@@ -31,7 +31,7 @@ public static class ScrollingColorsPatch
var maskObj = new GameObject
{
layer = 5,
name = "SpriteMask"
name = "SpriteMask",
};
maskObj.transform.SetParent(__instance.transform);
maskObj.transform.localPosition = new Vector3(0, 0, 0);
@@ -55,4 +55,4 @@ public static class ScrollingColorsPatch
__instance.SetScrollerBounds();
}
}
}
+2 -3
View File
@@ -1,5 +1,4 @@

/*
/*
[HarmonyPatch]
public static class ConsolePatches
{
@@ -69,4 +68,4 @@ public static class ConsolePatches
canUse = couldUse = true;
return true;
}
}*/
}*/
+2 -4
View File
@@ -1,6 +1,4 @@

/*
/*
[HarmonyPatch(typeof(DeadBody))]
public static class DeadBodyPatch
{
@@ -10,4 +8,4 @@ public static class DeadBodyPatch
{
return CustomGameModeManager.ActiveMode == null || CustomGameModeManager.ActiveMode.CanReport(__instance);
}
}*/
}*/
@@ -1,6 +1,4 @@

/*
/*
[HarmonyPatch]
public static class GameLogicPatches
{
@@ -19,4 +17,4 @@ public static class GameLogicPatches
CustomGameModeManager.ActiveMode?.AssignRoles(out runOriginal, __instance);
return runOriginal;
}
}*/
}*/
@@ -1,6 +1,4 @@

/*
/*
/// <summary>
/// Allow Impostors to kill each other if can kill is enabled in gamemode or friendly fire is toggled on
/// </summary>
@@ -16,11 +14,11 @@ public static class ImpostorTargetPatch
{
return true;
}
__result = target is { Disconnected: false, IsDead: false } &&
target.PlayerId != __instance.Player.PlayerId && !(target.Role == null) &&
!(target.Object == null) && !target.Object.inVent && !target.Object.inMovingPlat;
return false;
}
}*/
}*/
@@ -1,6 +1,4 @@

/*
/*
[HarmonyPatch(typeof(MapBehaviour))]
public static class MapBehaviourPatch
{
@@ -17,4 +15,4 @@ public static class MapBehaviourPatch
return true;
}
}*/
}*/
+2 -4
View File
@@ -1,6 +1,4 @@

/*
/*
[HarmonyPatch(typeof(KillAnimation))]
public static class OnDeathPatch
{
@@ -10,4 +8,4 @@ public static class OnDeathPatch
{
CustomGameModeManager.ActiveMode?.OnDeath(target);
}
}*/
}*/
+2 -6
View File
@@ -1,10 +1,7 @@

/*
/*
[HarmonyPatch(typeof(Vent))]
public static class VentPatches
{
[HarmonyPrefix]
[HarmonyPatch(nameof(Vent.CanUse))]
public static bool CanUseVentPatch(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse)
@@ -47,5 +44,4 @@ public static class VentPatches
return false;
}
}*/
}*/
+37
View File
@@ -0,0 +1,37 @@
using HarmonyLib;
using MiraAPI.Hud;
namespace MiraAPI.Patches.Hud;
/// <summary>
/// Reset button patches
/// </summary>
[HarmonyPatch]
public static class ButtonResetPatches
{
/// <summary>
/// Resets the cooldown and effect of all custom buttons when the meeting starts.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
public static void MeetingHudStartPostfix()
{
foreach (var customActionButton in CustomButtonManager.CustomButtons)
{
customActionButton.ResetCooldownAndOrEffect();
}
}
/// <summary>
/// Resets the cooldown and effect of all custom buttons after the exile screen is closed.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))]
public static void ExileControllerReEnableGameplayPostfix()
{
foreach (var customActionButton in CustomButtonManager.CustomButtons)
{
customActionButton.ResetCooldownAndOrEffect();
}
}
}
+21 -73
View File
@@ -1,66 +1,19 @@
using System;
using HarmonyLib;
using InnerNet;
using HarmonyLib;
using MiraAPI.Hud;
using MiraAPI.Roles;
using Reactor.Utilities.Extensions;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches;
/// <summary>
/// General patches for the HudManager class.
/// </summary>
[HarmonyPatch(typeof(HudManager))]
public static class HudManagerPatches
{
/// Custom buttons parent
private static GameObject _bottomLeft;
/// Custom role tab
private static TaskPanelBehaviour _roleTab;
[HarmonyPostfix]
[HarmonyPatch(nameof(HudManager.Update))]
public static void UpdatePostfix(HudManager __instance)
{
var local = PlayerControl.LocalPlayer;
if (AmongUsClient.Instance.GameState != InnerNetClient.GameStates.Started && !ShipStatus.Instance)
{
return;
}
//CustomGameModeManager.ActiveMode?.HudUpdate(__instance);
if (local?.Data?.Role is null)
{
return;
}
if (local.Data.Role is ICustomRole customRole)
{
customRole.HudUpdate(__instance);
if (customRole.SetTabText() != null)
{
if (!_roleTab)
{
_roleTab = CustomRoleManager.CreateRoleTab(customRole);
}
else
{
CustomRoleManager.UpdateRoleTab(_roleTab, customRole);
}
}
else if (customRole.SetTabText() == null && _roleTab)
{
_roleTab.gameObject.Destroy();
}
}
else if (_roleTab)
{
_roleTab.gameObject.Destroy();
}
}
// Custom buttons parent.
private static GameObject? _bottomLeft;
/*
/// <summary>
@@ -74,16 +27,17 @@ public static class HudManagerPatches
}*/
/// <summary>
/// Create custom buttons parent
/// Create custom buttons and arrange them on the hud.
/// </summary>
/// <param name="__instance">The HudManager instance.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(HudManager.Start))]
public static void StartPostfix(HudManager __instance)
{
var buttons = __instance.transform.Find("Buttons");
var bottomRight = buttons.Find("BottomRight");
if (!_bottomLeft)
if (_bottomLeft == null)
{
_bottomLeft = Object.Instantiate(bottomRight.gameObject, buttons);
}
@@ -106,14 +60,14 @@ public static class HudManagerPatches
{
ButtonLocation.BottomLeft => _bottomLeft.transform,
ButtonLocation.BottomRight => bottomRight,
_ => null
_ => null,
};
if (location is null)
{
continue;
}
button.CreateButton(location);
}
@@ -124,30 +78,24 @@ public static class HudManagerPatches
}
/// <summary>
/// Make sure all launchpad hud elements are inactive/active when appropriate
/// Set the custom buttons active when the hud is active.
/// </summary>
/// <param name="__instance">HudManager instance.</param>
/// <param name="localPlayer">The local PlayerControl.</param>
/// <param name="role">The player's RoleBehaviour.</param>
/// <param name="isActive">Whether the Hud should be set active or not.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(HudManager.SetHudActive), typeof(PlayerControl), typeof(RoleBehaviour), typeof(bool))]
public static void SetHudActivePostfix(HudManager __instance, [HarmonyArgument(0)] PlayerControl player, [HarmonyArgument(1)] RoleBehaviour roleBehaviour, [HarmonyArgument(2)] bool isActive)
public static void SetHudActivePostfix(HudManager __instance, PlayerControl localPlayer, RoleBehaviour role, bool isActive)
{
if (!player.Data)
if (!localPlayer.Data)
{
return;
}
if (_roleTab)
{
_roleTab.gameObject.SetActive(isActive);
}
foreach (var button in CustomButtonManager.CustomButtons)
{
button.SetActive(isActive, roleBehaviour);
}
if (roleBehaviour is ICustomRole role)
{
__instance.ImpostorVentButton.gameObject.SetActive(isActive && role.CanUseVent);
button.SetActive(isActive, role);
}
}
}
}
+27 -22
View File
@@ -19,33 +19,38 @@ public static class IntroCutscenePatches
__instance.TeamTitle.text = $"<size=70%>{mode.Name}</size>\n<size=20%>{mode.Description}</size>";
}
}*/
[HarmonyPrefix]
[HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))]
public static bool BeginCrewmatePatch(IntroCutscene __instance)
{
if (PlayerControl.LocalPlayer.Data.Role is ICustomRole customRole)
if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole)
{
if (!customRole.IsNeutral)
{
return true;
}
var barTransform = __instance.BackgroundBar.transform;
var position = barTransform.position;
position.y -= 0.25f;
barTransform.position = position;
__instance.BackgroundBar.material.SetColor(ShaderID.Color, Color.gray);
__instance.TeamTitle.text = "OUTCAST";
__instance.impostorScale = 1f;
__instance.ImpostorText.text = "You are an Outcast. You do not have a team.";
__instance.TeamTitle.color = Color.gray;
__instance.ourCrewmate = __instance.CreatePlayer(0, Mathf.CeilToInt(7.5f), PlayerControl.LocalPlayer.Data, false);
return false;
return true;
}
return true;
if (customRole.Team is not ModdedRoleTeams.Neutral)
{
return true;
}
var barTransform = __instance.BackgroundBar.transform;
var position = barTransform.position;
position.y -= 0.25f;
barTransform.position = position;
__instance.BackgroundBar.material.SetColor(ShaderID.Color, Color.gray);
__instance.TeamTitle.text = "NEUTRAL";
__instance.impostorScale = 1f;
__instance.ImpostorText.text = "You are Neutral. You do not have a team.";
__instance.TeamTitle.color = Color.gray;
__instance.ourCrewmate = __instance.CreatePlayer(
0,
Mathf.CeilToInt(7.5f),
PlayerControl.LocalPlayer.Data,
false);
return false;
}
/*
@@ -55,4 +60,4 @@ public static class IntroCutscenePatches
{
CustomGameModeManager.ActiveMode?.Initialize();
}*/
}
}
-18
View File
@@ -1,18 +0,0 @@
using HarmonyLib;
using MiraAPI.Hud;
namespace MiraAPI.Patches;
[HarmonyPatch(typeof(MeetingHud))]
public static class MeetingHudPatches
{
[HarmonyPostfix]
[HarmonyPatch(nameof(MeetingHud.VotingComplete))]
public static void VotingCompletePostfix()
{
foreach (var customActionButton in CustomButtonManager.CustomButtons)
{
customActionButton.ResetCooldown();
}
}
}
+3 -4
View File
@@ -1,6 +1,5 @@
using HarmonyLib;
using MiraAPI.Utilities;
using System.Linq;
using UnityEngine;
namespace MiraAPI.Patches.Modifiers;
@@ -23,10 +22,10 @@ public static class VentPatches
switch (role.CanVent)
{
case true when modifiers.Any(x => !x.CanVent()):
case true when modifiers.Exists(x => !x.CanVent()):
couldUse = canUse = false;
return;
case false when modifiers.Any(x => x.CanVent()):
case false when modifiers.Exists(x => x.CanVent()):
couldUse = true;
break;
}
@@ -43,4 +42,4 @@ public static class VentPatches
}
__result = num;
}
}
}
+49 -21
View File
@@ -10,26 +10,34 @@ using UnityEngine.Events;
namespace MiraAPI.Patches.Options;
/// <summary>
/// Patches the GameOptionsMenu to add custom options.
/// </summary>
[HarmonyPatch(typeof(GameOptionsMenu))]
public static class GameOptionsMenuPatch
{
/// <summary>
/// Update patch for the GameOptionsMenu.
/// </summary>
/// <param name="__instance">The GameOptionsMenu instance.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(GameOptionsMenu.Update))]
public static void UpdatePatch(GameOptionsMenu __instance)
{
if (GameSettingMenuPatches.CurrentSelectedMod == 0)
if (GameSettingMenuPatches.SelectedModIdx == 0)
{
return;
}
var num = 2.1f;
var filteredGroups = GameSettingMenuPatches.SelectedMod.OptionGroups.Where(x => x.GroupVisible.Invoke() && x.AdvancedRole is null);
var filteredGroups =
GameSettingMenuPatches.SelectedMod?.OptionGroups.Where(
x => x.GroupVisible.Invoke() && x.AdvancedRole is null) ?? [];
foreach (var group in filteredGroups)
{
var filteredOpts = group.Options.Where(x => x.Visible.Invoke()).ToList();
if (filteredOpts.Count == 0)
if (filteredOpts.Count == 0 || group.Header is null)
{
continue;
}
@@ -44,7 +52,12 @@ public static class GameOptionsMenuPatch
{
var newOpt = opt.OptionBehaviour;
if (opt.Visible.Invoke() == false)
if (newOpt is null)
{
continue;
}
if (!opt.Visible.Invoke())
{
newOpt.gameObject.SetActive(false);
continue;
@@ -64,25 +77,28 @@ public static class GameOptionsMenuPatch
}
__instance.scrollBar.SetYBoundsMax(-num - 1.65f);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))]
public static bool SettingsPatch(GameOptionsMenu __instance)
{
if (GameSettingMenuPatches.CurrentSelectedMod == 0)
if (GameSettingMenuPatches.SelectedModIdx == 0)
{
return true;
}
__instance.MapPicker.gameObject.SetActive(false);
var filteredGroups = GameSettingMenuPatches.SelectedMod.OptionGroups.Where(x => x.AdvancedRole is null);
var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups.Where(x => x.AdvancedRole is null) ?? [];
foreach (var group in filteredGroups)
{
var categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer);
var categoryHeaderMasked = Object.Instantiate(
__instance.categoryHeaderOrigin,
Vector3.zero,
Quaternion.identity,
__instance.settingsContainer);
categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20);
if (group.GroupColor != Color.clear)
{
@@ -90,17 +106,24 @@ public static class GameOptionsMenuPatch
categoryHeaderMasked.Divider.color = group.GroupColor;
categoryHeaderMasked.Title.color = group.GroupColor.GetAlternateColor();
}
categoryHeaderMasked.Background.size = new Vector2(categoryHeaderMasked.Background.size.x + 1.5f, categoryHeaderMasked.Background.size.y);
categoryHeaderMasked.Background.size = new Vector2(
categoryHeaderMasked.Background.size.x + 1.5f,
categoryHeaderMasked.Background.size.y);
categoryHeaderMasked.gameObject.SetActive(false);
group.Header = categoryHeaderMasked;
var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform);
newText.text = $"<size=70%>(Click to open)</size>";
newText.text = "<size=70%>(Click to close)</size>";
newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f);
newText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
var options = group.Options.Select(opt => opt.CreateOption(__instance.checkboxOrigin,
__instance.numberOptionOrigin, __instance.stringOptionOrigin, __instance.settingsContainer));
var options = group.Options.Select(
opt => opt.CreateOption(
__instance.checkboxOrigin,
__instance.numberOptionOrigin,
__instance.stringOptionOrigin,
__instance.settingsContainer));
foreach (var newOpt in options)
{
@@ -136,8 +159,10 @@ public static class GameOptionsMenuPatch
if (newOpt is ToggleOption toggle)
{
toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset();
toggle.CheckMark.color = group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal;
var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite").GetComponent<SpriteRenderer>();
toggle.CheckMark.color =
group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal;
var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite")
.GetComponent<SpriteRenderer>();
rend.sprite = MiraAssets.CheckmarkBox.LoadAsset();
rend.color = group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal;
}
@@ -156,11 +181,14 @@ public static class GameOptionsMenuPatch
headerBtn.ClickSound = __instance.BackButton.GetComponent<PassiveButton>().ClickSound;
headerBtn.OnMouseOver = new UnityEvent();
headerBtn.OnMouseOut = new UnityEvent();
headerBtn.OnClick.AddListener((UnityAction)(() =>
{
group.AllOptionsHidden = !group.AllOptionsHidden;
newText.text = group.AllOptionsHidden ? "<size=70%>(Click to open)</size>" : "<size=70%>(Click to close)</size>";
}));
headerBtn.OnClick.AddListener(
(UnityAction)(() =>
{
group.AllOptionsHidden = !group.AllOptionsHidden;
newText.text = group.AllOptionsHidden
? "<size=70%>(Click to open)</size>"
: "<size=70%>(Click to close)</size>";
}));
headerBtn.SetButtonEnableState(true);
}
@@ -201,4 +229,4 @@ public static class GameOptionsMenuPatch
return false;
}
}
}
@@ -12,17 +12,30 @@ using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
/// <summary>
/// Patches for the <see cref="GameSettingMenu"/> to add support for custom options.
/// </summary>
[HarmonyPatch(typeof(GameSettingMenu))]
public static class GameSettingMenuPatches
{
public static int CurrentSelectedMod { get; private set; }
/// <summary>
/// Gets the currently selected mod index.
/// </summary>
public static int SelectedModIdx { get; private set; }
public static MiraPluginInfo SelectedMod { get; private set; }
/// <summary>
/// Gets the currently selected mod.
/// </summary>
public static MiraPluginInfo? SelectedMod { get; private set; }
private static TextMeshPro _text;
private static TextMeshPro? _text;
private static Vector3 _roleBtnOgPos;
/// <summary>
/// Prefix for the <see cref="GameSettingMenu.Start"/> method. Sets up the custom options.
/// </summary>
/// <param name="__instance">The GameSettingMenu instance.</param>
[HarmonyPrefix]
[HarmonyPatch(nameof(GameSettingMenu.Start))]
public static void StartPrefix(GameSettingMenu __instance)
@@ -56,11 +69,12 @@ public static class GameSettingMenuPatches
passiveButton.OnClick = new ButtonClickedEvent();
passiveButton.OnClick.AddListener((UnityAction)(() =>
{
if (CurrentSelectedMod != MiraPluginManager.Instance.RegisteredPlugins.Count)
SelectedModIdx += 1;
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length)
{
CurrentSelectedMod += 1;
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
SelectedModIdx = 0;
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}));
var backButton = Object.Instantiate(nextButton, __instance.BackButton.transform.parent).gameObject;
@@ -70,40 +84,36 @@ public static class GameSettingMenuPatches
backButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().flipX = backButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().flipX = true;
backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener((UnityAction)(() =>
{
if (CurrentSelectedMod != 0)
SelectedModIdx -= 1;
if (SelectedModIdx < 0)
{
CurrentSelectedMod -= 1;
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length;
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}));
}
private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles)
{
if (CurrentSelectedMod == 0)
if (_text is not null && SelectedModIdx == 0)
{
_text.text = "Default";
_text.fontSizeMax = 3.2f;
}
else
else if (_text is not null)
{
_text.fontSizeMax = 2.3f;
SelectedMod = MiraPluginManager.Instance.RegisteredPlugins.ElementAt(CurrentSelectedMod - 1).Value;
if (SelectedMod == null)
{
CurrentSelectedMod = 0;
UpdateText(menu, settings, roles);
}
SelectedMod = MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1];
var name = SelectedMod?.MiraPlugin.OptionsTitleText;
_text.text = name?[..Math.Min(name.Length, 25)];
var name = SelectedMod.MiraPlugin.OptionsTitleText;
_text.text = name[..Math.Min(name.Length, 25)];
}
menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos;
menu.GameSettingsButton.gameObject.SetActive(true);
menu.RoleSettingsButton.gameObject.SetActive(true);
if (CurrentSelectedMod != 0)
if (SelectedModIdx != 0)
{
if (SelectedMod?.Options.Where(x=>x.AdvancedRole==null).ToList().Count == 0)
{
@@ -121,7 +131,7 @@ public static class GameSettingMenuPatches
}
}
if (roles?.roleChances != null && SelectedMod != null && SelectedMod.CustomRoles.Count != 0)
if (roles.roleChances != null && SelectedMod != null && SelectedMod.CustomRoles.Count != 0)
{
if (roles.advancedSettingChildren is not null)
{
@@ -144,7 +154,6 @@ public static class GameSettingMenuPatches
}
roles.roleChances.Clear();
roles.roleChances = null;
roles.AdvancedRolesSettings.gameObject.SetActive(false);
roles.RoleChancesSettings.gameObject.SetActive(true);
roles.SetQuotaTab();
@@ -153,7 +162,7 @@ public static class GameSettingMenuPatches
roles.scrollBar.ScrollToTop();
}
if (settings?.Children != null && SelectedMod?.OptionGroups.Count != 0)
if (settings.Children != null && SelectedMod?.OptionGroups.Count != 0)
{
foreach (var child in settings.Children)
{
+278 -239
View File
@@ -1,5 +1,4 @@
#nullable enable
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
@@ -17,11 +16,11 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(LobbyViewSettingsPane))]
public static class LobbyViewPanePatches
{
public static int CurrentSelectedMod { get; private set; }
public static MiraPluginInfo? SelectedMod => CurrentSelectedMod == 0 ? null : MiraPluginManager.Instance.RegisteredPlugins.ElementAt(CurrentSelectedMod-1).Value;
private static int CurrentSelectedMod { get; set; }
private static MiraPluginInfo? SelectedMod => CurrentSelectedMod == 0
? null
: MiraPluginManager.Instance.RegisteredPlugins()[CurrentSelectedMod - 1];
[HarmonyPostfix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.Awake))]
@@ -35,92 +34,96 @@ public static class LobbyViewPanePatches
nextButton.transform.localPosition = new Vector3(-5.4f, 2.4f, -2f);
nextButton.transform.localScale = new Vector3(3, 3, 2);
nextButton.name = "RightArrowButton";
var normal = nextButton.transform.FindChild("Normal").GetComponentInChildren<SpriteRenderer>();
normal.transform.localPosition = new Vector3(0, 0f, 0.3f);
normal.sprite = MiraAssets.NextButton.LoadAsset();
var hover = nextButton.transform.FindChild("Hover").GetComponentInChildren<SpriteRenderer>();
hover.transform.localPosition = new Vector3(0, 0f, 0.3f);
hover.sprite = MiraAssets.NextButtonActive.LoadAsset();
var passiveButton = nextButton.gameObject.GetComponent<PassiveButton>();
passiveButton.OnClick = new Button.ButtonClickedEvent();
passiveButton.OnClick.AddListener((UnityAction)(() =>
{
CurrentSelectedMod += 1;
if (CurrentSelectedMod > MiraPluginManager.Instance.RegisteredPlugins.Count)
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
CurrentSelectedMod = 0;
}
__instance.RefreshTab();
__instance.scrollBar.ScrollToTop();
}));
CurrentSelectedMod += 1;
if (CurrentSelectedMod > MiraPluginManager.Instance.RegisteredPlugins().Length)
{
CurrentSelectedMod = 0;
}
__instance.RefreshTab();
__instance.scrollBar.ScrollToTop();
}));
// Create the back button
var backButton = Object.Instantiate(nextButton, __instance.BackButton.transform.parent).gameObject;
backButton.transform.localPosition = new Vector3(-6.3f, 2.4f, -2f);
backButton.name = "LeftArrowButton";
backButton.transform.FindChild("Normal").gameObject.GetComponentInChildren<SpriteRenderer>().flipX
= backButton.transform.FindChild("Hover").gameObject.GetComponentInChildren<SpriteRenderer>().flipX
= backButton.transform.FindChild("Hover").gameObject.GetComponentInChildren<SpriteRenderer>().flipX
= true;
var passiveButton2 = backButton.gameObject.GetComponent<PassiveButton>();
passiveButton2.OnClick = new Button.ButtonClickedEvent();
passiveButton2.OnClick.AddListener((UnityAction)(() =>
{
CurrentSelectedMod -= 1;
if (CurrentSelectedMod < 0)
passiveButton2.OnClick.AddListener(
(UnityAction)(() =>
{
CurrentSelectedMod = MiraPluginManager.Instance.RegisteredPlugins.Count;
}
__instance.RefreshTab();
__instance.scrollBar.ScrollToTop();
}));
CurrentSelectedMod -= 1;
if (CurrentSelectedMod < 0)
{
CurrentSelectedMod = MiraPluginManager.Instance.RegisteredPlugins().Length;
}
__instance.RefreshTab();
__instance.scrollBar.ScrollToTop();
}));
}
[HarmonyPostfix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.Update))]
public static void UpdatePatch(LobbyViewSettingsPane __instance)
{
__instance.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Default";
}
[HarmonyPrefix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.DrawNormalTab))]
public static bool DrawNormalTabPatch(LobbyViewSettingsPane __instance)
{
if (CurrentSelectedMod == 0)
{
return true;
}
DrawOptionsTab(__instance);
return false;
if (CurrentSelectedMod == 0)
{
return true;
}
DrawOptionsTab(__instance);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.DrawRolesTab))]
public static bool DrawRolesTabPatch(LobbyViewSettingsPane __instance)
{
if (CurrentSelectedMod == 0)
{
return true;
}
DrawRolesTab(__instance);
return false;
if (CurrentSelectedMod == 0)
{
return true;
}
DrawRolesTab(__instance);
return false;
}
private static void DrawOptionsTab(LobbyViewSettingsPane instance)
{
if (SelectedMod == null)
{
return;
}
var num = 1.44f;
var filteredGroups = SelectedMod.OptionGroups.Where(x => x.GroupVisible.Invoke() && x.AdvancedRole is null);
foreach (var group in filteredGroups)
@@ -129,8 +132,11 @@ public static class LobbyViewPanePatches
{
continue;
}
var categoryHeaderMasked = Object.Instantiate(instance.categoryHeaderOrigin, instance.settingsContainer, true);
var categoryHeaderMasked = Object.Instantiate(
instance.categoryHeaderOrigin,
instance.settingsContainer,
true);
categoryHeaderMasked.SetHeader(StringNames.Name, 61);
categoryHeaderMasked.Title.text = group.GroupName;
categoryHeaderMasked.transform.localScale = Vector3.one;
@@ -146,8 +152,11 @@ public static class LobbyViewPanePatches
{
continue;
}
var viewSettingsInfoPanel = Object.Instantiate(instance.infoPanelOrigin, instance.settingsContainer, true);
var viewSettingsInfoPanel = Object.Instantiate(
instance.infoPanelOrigin,
instance.settingsContainer,
true);
viewSettingsInfoPanel.transform.localScale = Vector3.one;
float num2;
if (i % 2 == 0)
@@ -162,225 +171,255 @@ public static class LobbyViewPanePatches
{
num2 = -3f;
}
viewSettingsInfoPanel.transform.localPosition = new Vector3(num2, num, -2f);
var data = option.Data;
if (data == null)
{
continue;
}
if (data.Type == OptionTypes.Checkbox)
{
viewSettingsInfoPanel.SetInfoCheckbox(data.Title, 61, Mathf.Approximately(option.GetFloatData(), 1));
viewSettingsInfoPanel.SetInfoCheckbox(
data.Title,
61,
Mathf.Approximately(option.GetFloatData(), 1));
}
else
{
viewSettingsInfoPanel.SetInfo(data.Title, data.GetValueString(option.GetFloatData()), 61);
}
instance.settingsInfo.Add(viewSettingsInfoPanel.gameObject);
i++;
}
num -= 0.59f;
}
instance.scrollBar.CalculateAndSetYBounds(instance.settingsInfo.Count + 10, 2f, 6f, 0.59f);
}
private static void DrawRolesTab(LobbyViewSettingsPane instance)
{
if (SelectedMod == null)
{
return;
}
var num = 0.95f;
var num2 = -6.53f;
var categoryHeaderMasked =
Object.Instantiate(instance.categoryHeaderOrigin, instance.settingsContainer, true);
categoryHeaderMasked.SetHeader(StringNames.RoleQuotaLabel, 61);
categoryHeaderMasked.transform.localScale = Vector3.one;
categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, 1.26f, -2f);
instance.settingsInfo.Add(categoryHeaderMasked.gameObject);
var list = new List<Type>();
if (SelectedMod == null)
{
return;
}
foreach (var team in Enum.GetValues<ModdedRoleTeams>())
{
var filteredRoles = SelectedMod.CustomRoles.Values
.OfType<ICustomRole>()
.Where(x => !x.HideSettings && x.Team == team).ToList();
if (filteredRoles.Count == 0)
{
continue;
}
var categoryHeaderRoleVariant =
Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true);
var num = 0.95f;
var num2 = -6.53f;
var categoryHeaderMasked =
Object.Instantiate(instance.categoryHeaderOrigin, instance.settingsContainer, true);
categoryHeaderMasked.SetHeader(StringNames.RoleQuotaLabel, 61);
categoryHeaderMasked.transform.localScale = Vector3.one;
categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, 1.26f, -2f);
instance.settingsInfo.Add(categoryHeaderMasked.gameObject);
switch (team)
{
case ModdedRoleTeams.Crewmate:
categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61);
break;
case ModdedRoleTeams.Impostor:
categoryHeaderRoleVariant.SetHeader(StringNames.ImpostorRolesHeader, 61);
break;
case ModdedRoleTeams.Neutral:
default:
categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61);
categoryHeaderRoleVariant.Title.text = team + " Roles";
categoryHeaderRoleVariant.Background.color = Color.gray;
categoryHeaderRoleVariant.Title.color = Color.white;
break;
}
categoryHeaderRoleVariant.transform.localScale = Vector3.one;
categoryHeaderRoleVariant.transform.localPosition = new Vector3(0.09f, num, -2f);
instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject);
num -= 0.696f;
var list = new List<Type>();
foreach (var customRole in filteredRoles)
{
var roleBehaviour = customRole as RoleBehaviour;
if (roleBehaviour == null)
{
continue;
}
var chancePerGame =
GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleBehaviour.Role);
var numPerGame =
GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role);
var flag = numPerGame == 0;
var viewSettingsInfoPanelRoleVariant =
Object.Instantiate(
instance.infoPanelRoleOrigin, instance.settingsContainer, true);
viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one;
viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f);
var advancedRoleOptions = SelectedMod.Options
.Where(x => x.AdvancedRole == customRole.GetType())
.ToList();
if (!flag && advancedRoleOptions.Count > 0)
{
list.Add(customRole.GetType());
}
foreach (var team in Enum.GetValues<ModdedRoleTeams>())
{
var filteredRoles = SelectedMod.CustomRoles.Values
.OfType<ICustomRole>()
.Where(x => !x.HideSettings && x.Team == team).ToList();
viewSettingsInfoPanelRoleVariant.SetInfo(roleBehaviour.NiceName, numPerGame, chancePerGame,
61, customRole.RoleColor, customRole.Icon.LoadAsset(), true);
viewSettingsInfoPanelRoleVariant.titleText.color =
viewSettingsInfoPanelRoleVariant.chanceTitle.color =
viewSettingsInfoPanelRoleVariant.chanceBackground.color =
viewSettingsInfoPanelRoleVariant.background.color =
customRole.RoleColor.GetAlternateColor();
instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject);
num -= 0.664f;
}
if (filteredRoles.Count == 0)
{
continue;
}
var categoryHeaderRoleVariant =
Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true);
}
switch (team)
{
case ModdedRoleTeams.Crewmate:
categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61);
break;
case ModdedRoleTeams.Impostor:
categoryHeaderRoleVariant.SetHeader(StringNames.ImpostorRolesHeader, 61);
break;
default:
categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61);
categoryHeaderRoleVariant.Title.text = team + " Roles";
categoryHeaderRoleVariant.Background.color = Color.gray;
categoryHeaderRoleVariant.Title.color = Color.white;
break;
}
if (list.Count > 0)
{
var categoryHeaderMasked2 =
Object.Instantiate(instance.categoryHeaderOrigin, instance.settingsContainer, true);
categoryHeaderMasked2.SetHeader(StringNames.RoleSettingsLabel, 61);
categoryHeaderMasked2.transform.localScale = Vector3.one;
categoryHeaderMasked2.transform.localPosition = new Vector3(-9.77f, num, -2f);
instance.settingsInfo.Add(categoryHeaderMasked2.gameObject);
num -= 1.7f;
var num3 = 0f;
for (var k = 0; k < list.Count; k++)
{
float num4;
if (k % 2 == 0)
{
num4 = -5.8f;
if (k > 0)
{
num -= num3 + 0.59f;
num3 = 0f;
}
}
else
{
num4 = 0.14999962f;
}
categoryHeaderRoleVariant.transform.localScale = Vector3.one;
categoryHeaderRoleVariant.transform.localPosition = new Vector3(0.09f, num, -2f);
instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject);
num -= 0.696f;
var advancedRoleViewPanel =
Object.Instantiate(instance.advancedRolePanelOrigin, instance.settingsContainer, true);
advancedRoleViewPanel.transform.localScale = Vector3.one;
advancedRoleViewPanel.transform.localPosition = new Vector3(num4, num, -2f);
var num5 = SetUpAdvancedRoleViewPanel(advancedRoleViewPanel, list[k], 0.59f, 61);
if (num5 > num3)
{
num3 = num5;
}
foreach (var customRole in filteredRoles)
{
var roleBehaviour = customRole as RoleBehaviour;
if (roleBehaviour == null)
{
continue;
}
instance.settingsInfo.Add(advancedRoleViewPanel.gameObject);
}
}
var chancePerGame =
GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(roleBehaviour.Role);
var numPerGame =
GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role);
instance.scrollBar.SetYBoundsMax(-num);
var flag = numPerGame == 0;
var viewSettingsInfoPanelRoleVariant =
Object.Instantiate(
instance.infoPanelRoleOrigin,
instance.settingsContainer,
true);
viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one;
viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f);
var advancedRoleOptions = SelectedMod.Options
.Where(x => x.AdvancedRole == customRole.GetType())
.ToList();
if (!flag && advancedRoleOptions.Count > 0)
{
list.Add(customRole.GetType());
}
viewSettingsInfoPanelRoleVariant.SetInfo(
roleBehaviour.NiceName,
numPerGame,
chancePerGame,
61,
customRole.RoleColor,
customRole.Icon.LoadAsset(),
true);
viewSettingsInfoPanelRoleVariant.titleText.color =
viewSettingsInfoPanelRoleVariant.chanceTitle.color =
viewSettingsInfoPanelRoleVariant.chanceBackground.color =
viewSettingsInfoPanelRoleVariant.background.color =
customRole.RoleColor.GetAlternateColor();
instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject);
num -= 0.664f;
}
}
if (list.Count > 0)
{
var categoryHeaderMasked2 =
Object.Instantiate(instance.categoryHeaderOrigin, instance.settingsContainer, true);
categoryHeaderMasked2.SetHeader(StringNames.RoleSettingsLabel, 61);
categoryHeaderMasked2.transform.localScale = Vector3.one;
categoryHeaderMasked2.transform.localPosition = new Vector3(-9.77f, num, -2f);
instance.settingsInfo.Add(categoryHeaderMasked2.gameObject);
num -= 1.7f;
var num3 = 0f;
for (var k = 0; k < list.Count; k++)
{
float num4;
if (k % 2 == 0)
{
num4 = -5.8f;
if (k > 0)
{
num -= num3 + 0.59f;
num3 = 0f;
}
}
else
{
num4 = 0.14999962f;
}
var advancedRoleViewPanel =
Object.Instantiate(instance.advancedRolePanelOrigin, instance.settingsContainer, true);
advancedRoleViewPanel.transform.localScale = Vector3.one;
advancedRoleViewPanel.transform.localPosition = new Vector3(num4, num, -2f);
var num5 = SetUpAdvancedRoleViewPanel(advancedRoleViewPanel, list[k], 0.59f, 61);
if (num5 > num3)
{
num3 = num5;
}
instance.settingsInfo.Add(advancedRoleViewPanel.gameObject);
}
}
instance.scrollBar.SetYBoundsMax(-num);
}
private static float SetUpAdvancedRoleViewPanel(AdvancedRoleViewPanel viewPanel, Type roleType, float spacingY, int maskLayer)
private static float SetUpAdvancedRoleViewPanel(
AdvancedRoleViewPanel viewPanel,
Type roleType,
float spacingY,
int maskLayer)
{
if (SelectedMod == null)
{
return 0;
}
var role = SelectedMod.CustomRoles.Values.FirstOrDefault(x => x.GetType() == roleType);
if (SelectedMod == null)
{
return 0;
}
if (role == null)
{
return 0;
}
var role = SelectedMod.CustomRoles.Values.FirstOrDefault(x => x.GetType() == roleType);
if (role is not ICustomRole customRole)
{
return 0;
}
viewPanel.header.SetHeader(role.StringName, maskLayer, role.TeamType == RoleTeamTypes.Crewmate, customRole.Icon.LoadAsset());
viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
var num = viewPanel.yPosStart;
var num2 = 1.08f;
var filteredOptions = SelectedMod.Options
.Where(x => x.AdvancedRole == roleType)
.ToList();
for (var i = 0; i < filteredOptions.Count; i++)
{
var option = filteredOptions[i];
var baseGameSetting = option.Data;
var viewSettingsInfoPanel = Object.Instantiate(viewPanel.infoPanelOrigin, viewPanel.transform, true);
viewSettingsInfoPanel.transform.localScale = Vector3.one;
viewSettingsInfoPanel.transform.localPosition = new Vector3(viewPanel.xPosStart, num, -2f);
var value = option.GetFloatData();
if (baseGameSetting.Type == OptionTypes.Checkbox)
{
viewSettingsInfoPanel.SetInfoCheckbox(baseGameSetting.Title, maskLayer, value > 0f);
}
else
{
viewSettingsInfoPanel.SetInfo(baseGameSetting.Title, baseGameSetting.GetValueString(value), maskLayer);
}
num -= spacingY;
if (i > 0)
{
num2 += 0.8f;
}
}
return num2;
if (role == null)
{
return 0;
}
if (role is not ICustomRole customRole)
{
return 0;
}
viewPanel.header.SetHeader(
role.StringName,
maskLayer,
role.TeamType == RoleTeamTypes.Crewmate,
customRole.Icon.LoadAsset());
viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
var num = viewPanel.yPosStart;
var num2 = 1.08f;
var filteredOptions = SelectedMod.Options
.Where(x => x.AdvancedRole == roleType)
.ToList();
for (var i = 0; i < filteredOptions.Count; i++)
{
var option = filteredOptions[i];
var baseGameSetting = option.Data;
var viewSettingsInfoPanel = Object.Instantiate(viewPanel.infoPanelOrigin, viewPanel.transform, true);
viewSettingsInfoPanel.transform.localScale = Vector3.one;
viewSettingsInfoPanel.transform.localPosition = new Vector3(viewPanel.xPosStart, num, -2f);
var value = option.GetFloatData();
if (baseGameSetting == null)
{
continue;
}
if (baseGameSetting.Type == OptionTypes.Checkbox)
{
viewSettingsInfoPanel.SetInfoCheckbox(baseGameSetting.Title, maskLayer, value > 0f);
}
else
{
viewSettingsInfoPanel.SetInfo(baseGameSetting.Title, baseGameSetting.GetValueString(value), maskLayer);
}
num -= spacingY;
if (i > 0)
{
num2 += 0.8f;
}
}
return num2;
}
}
}
@@ -0,0 +1,43 @@
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.Roles;
using System.Linq;
namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(NotificationPopper))]
public static class NotificationPopperPatch
{
[HarmonyPrefix]
[HarmonyPatch(nameof(NotificationPopper.AddRoleSettingsChangeMessage))]
public static bool RoleChangeMsgPatch(
NotificationPopper __instance,
[HarmonyArgument(0)] StringNames key,
[HarmonyArgument(1)] int roleCount,
[HarmonyArgument(2)] int roleChance,
[HarmonyArgument(3)] RoleTeamTypes teamType,
[HarmonyArgument(4)] bool playSound)
{
if (CustomRoleManager.CustomRoles.Values.All(role => role.StringName != key))
{
return true;
}
var text = teamType == RoleTeamTypes.Crewmate
? Palette.CrewmateSettingChangeText.ToTextColor()
: Palette.ImpostorRed.ToTextColor();
var item = DestroyableSingleton<TranslationController>.Instance.GetString(
StringNames.LobbyChangeSettingNotificationRole,
"<font=\"Barlow-Black SDF\" material=\"Barlow-Black Outline\">",
text,
DestroyableSingleton<TranslationController>.Instance.GetString(key, Array.Empty<Object>()),
"</color></font>",
"<font=\"Barlow-Black SDF\" material=\"Barlow-Black Outline\">" + roleCount + "</font>",
"<font=\"Barlow-Black SDF\" material=\"Barlow-Black Outline\">" + roleChance + "%"
);
__instance.SettingsChangeMessageLogic(key, item, playSound);
return false;
}
}
+10 -5
View File
@@ -16,7 +16,8 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text = DestroyableSingleton<TranslationController>.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.TitleText.text =
DestroyableSingleton<TranslationController>.Instance.GetString(__instance.Title, Array.Empty<Object>());
return false;
}
@@ -37,7 +38,8 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text = DestroyableSingleton<TranslationController>.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.TitleText.text =
DestroyableSingleton<TranslationController>.Instance.GetString(__instance.Title, Array.Empty<Object>());
return false;
}
@@ -58,8 +60,11 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text = DestroyableSingleton<TranslationController>.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.ValueText.text = DestroyableSingleton<TranslationController>.Instance.GetString(__instance.Values[__instance.Value], Array.Empty<Object>());
__instance.TitleText.text =
DestroyableSingleton<TranslationController>.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.ValueText.text = DestroyableSingleton<TranslationController>.Instance.GetString(
__instance.Values[__instance.Value],
Array.Empty<Object>());
return false;
}
@@ -70,4 +75,4 @@ public static class OptionsPatches
{
return !__instance.IsCustom();
}
}
}
+118 -56
View File
@@ -1,4 +1,5 @@
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.Networking;
using MiraAPI.Roles;
using MiraAPI.Utilities;
@@ -8,7 +9,6 @@ using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Linq;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
@@ -30,7 +30,7 @@ public static class RoleSettingMenuPatches
var maskBg = __instance.scrollBar.transform.FindChild("MaskBg");
var hitbox = __instance.scrollBar.transform.FindChild("Hitbox");
if (GameSettingMenuPatches.CurrentSelectedMod == 0)
if (GameSettingMenuPatches.SelectedModIdx == 0)
{
__instance.AllButton.transform.parent.gameObject.SetActive(true);
__instance.AllButton.gameObject.SetActive(true);
@@ -54,58 +54,72 @@ public static class RoleSettingMenuPatches
var num3 = 0;
var crewRoles = GameSettingMenuPatches.SelectedMod.CustomRoles.Values
var crewRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values
.OfType<ICustomRole>()
.Where(role => role.Team == ModdedRoleTeams.Crewmate && !role.HideSettings)
.ToList();
.Where(role => role is { Team: ModdedRoleTeams.Crewmate, HideSettings: false })
.ToList() ?? [];
if (crewRoles.Count > 0)
if (crewRoles is { Count: > 0 })
{
var categoryHeaderEditRole = Object.Instantiate(__instance.categoryHeaderEditRoleOrigin, Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform);
var categoryHeaderEditRole = Object.Instantiate(
__instance.categoryHeaderEditRoleOrigin,
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
categoryHeaderEditRole.SetHeader(StringNames.CrewmateRolesHeader, 20);
categoryHeaderEditRole.transform.localPosition = new Vector3(4.986f, num, -2f);
num -= 0.522f;
foreach (var role in crewRoles)
{
CreateQuotaOption(__instance, role as RoleBehaviour, ref num, num3);
if (role is RoleBehaviour roleBehaviour)
{
CreateQuotaOption(__instance, roleBehaviour, ref num, num3);
}
num3++;
}
}
var impRoles = GameSettingMenuPatches.SelectedMod.CustomRoles.Values
var impRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values
.OfType<ICustomRole>()
.Where(role => role.Team == ModdedRoleTeams.Impostor && !role.HideSettings)
.ToList();
.Where(role => role is { Team: ModdedRoleTeams.Impostor, HideSettings: false })
.ToList() ?? [];
if (impRoles.Count > 0)
if (impRoles is { Count: > 0 })
{
num -= 0.4f;
var categoryHeaderEditRole2 = Object.Instantiate(__instance.categoryHeaderEditRoleOrigin,
Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform);
var categoryHeaderEditRole2 = Object.Instantiate(
__instance.categoryHeaderEditRoleOrigin,
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
categoryHeaderEditRole2.SetHeader(StringNames.ImpostorRolesHeader, 20);
categoryHeaderEditRole2.transform.localPosition = new Vector3(4.986f, num, -2f);
num -= 0.522f;
foreach (var role in impRoles)
{
CreateQuotaOption(__instance, role as RoleBehaviour, ref num, num3);
if (role is RoleBehaviour roleBehaviour)
{
CreateQuotaOption(__instance, roleBehaviour, ref num, num3);
}
num3++;
}
}
var neutRoles = GameSettingMenuPatches.SelectedMod.CustomRoles.Values
var neutRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values
.OfType<ICustomRole>()
.Where(role => role.Team == ModdedRoleTeams.Neutral && !role.HideSettings)
.ToList();
.Where(role => role is { Team: ModdedRoleTeams.Neutral, HideSettings: false })
.ToList() ?? [];
if (neutRoles.Count > 0)
if (neutRoles is { Count: > 0 })
{
num -= 0.4f;
var categoryHeaderEditRole3 = Object.Instantiate(__instance.categoryHeaderEditRoleOrigin,
Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform);
var categoryHeaderEditRole3 = Object.Instantiate(
__instance.categoryHeaderEditRoleOrigin,
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
categoryHeaderEditRole3.SetHeader(StringNames.None, 20);
categoryHeaderEditRole3.Title.text = "Neutral Roles";
categoryHeaderEditRole3.Background.color = Color.gray;
@@ -115,7 +129,10 @@ public static class RoleSettingMenuPatches
foreach (var role in neutRoles)
{
CreateQuotaOption(__instance, role as RoleBehaviour, ref num, num3);
if (role is RoleBehaviour roleBehaviour)
{
CreateQuotaOption(__instance, roleBehaviour, ref num, num3);
}
num3++;
}
}
@@ -127,7 +144,7 @@ public static class RoleSettingMenuPatches
[HarmonyPatch(nameof(RolesSettingsMenu.Update))]
public static bool UpdatePatch()
{
return GameSettingMenuPatches.CurrentSelectedMod == 0;
return GameSettingMenuPatches.SelectedModIdx == 0;
}
[HarmonyPostfix]
@@ -161,12 +178,18 @@ public static class RoleSettingMenuPatches
}
roleSetting.UpdateValuesAndText(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions);
DestroyableSingleton<HudManager>.Instance.Notifier.AddRoleSettingsChangeMessage(roleSetting.Role.StringName, roleSetting.RoleMaxCount, roleSetting.RoleChance, roleSetting.Role.TeamType, false);
DestroyableSingleton<HudManager>.Instance.Notifier.AddRoleSettingsChangeMessage(
roleSetting.Role.StringName,
roleSetting.RoleMaxCount,
roleSetting.RoleChance,
roleSetting.Role.TeamType,
false);
if (AmongUsClient.Instance.AmHost)
{
Rpc<SyncRoleOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [role.GetNetData()], true);
}
GameOptionsManager.Instance.GameHostOptions = GameOptionsManager.Instance.CurrentGameOptions;
}
@@ -181,7 +204,7 @@ public static class RoleSettingMenuPatches
var num = -0.872f;
var filteredOptions = GameSettingMenuPatches.SelectedMod.Options.Where(x => x.AdvancedRole == role.GetType());
var filteredOptions = GameSettingMenuPatches.SelectedMod?.Options.Where(x => x.AdvancedRole == role.GetType()) ?? [];
foreach (var option in filteredOptions)
{
@@ -190,7 +213,12 @@ public static class RoleSettingMenuPatches
continue;
}
var newOpt = option.CreateOption(__instance.checkboxOrigin, __instance.numberOptionOrigin, __instance.stringOptionOrigin, __instance.AdvancedRolesSettings.transform);
var newOpt = option.CreateOption(
__instance.checkboxOrigin,
__instance.numberOptionOrigin,
__instance.stringOptionOrigin,
__instance.AdvancedRolesSettings.transform);
newOpt.transform.localPosition = new Vector3(2.17f, num, -2f);
newOpt.SetClickMask(__instance.ButtonClickMask);
@@ -199,10 +227,11 @@ public static class RoleSettingMenuPatches
{
renderer.material.SetInt(PlayerMaterial.MaskLayer, 20);
}
foreach (var textMeshPro in newOpt.GetComponentsInChildren<TextMeshPro>(true))
foreach (var fontMat in newOpt.GetComponentsInChildren<TextMeshPro>(true).Select(x=>x.fontMaterial))
{
textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f);
textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20);
fontMat.SetFloat(ShaderID.StencilComp, 3f);
fontMat.SetFloat(ShaderID.Stencil, 20);
}
newOpt.LabelBackground.enabled = false;
@@ -215,35 +244,66 @@ public static class RoleSettingMenuPatches
__instance.scrollBar.CalculateAndSetYBounds(__instance.advancedSettingChildren.Count + 3, 1f, 6f, 0.45f);
__instance.scrollBar.ScrollToTop();
}
private static void ChangeTab(RoleBehaviour role, RolesSettingsMenu __instance)
{
var customRole = role as ICustomRole;
if (role is not ICustomRole customRole)
{
Logger<MiraApiPlugin>.Error($"Role {role.NiceName} is not a custom role.");
return;
}
__instance.roleDescriptionText.text = customRole.RoleLongDescription;
__instance.roleTitleText.text = DestroyableSingleton<TranslationController>.Instance.GetString(role.StringName, new Il2CppReferenceArray<Il2CppSystem.Object>(0));
__instance.roleScreenshot.sprite = Sprite.Create(customRole.OptionsScreenshot.LoadAsset().texture, new Rect(0, 0, 370, 230), Vector2.one / 2, 100);
__instance.roleTitleText.text = DestroyableSingleton<TranslationController>.Instance.GetString(
role.StringName,
new Il2CppReferenceArray<Il2CppSystem.Object>(0));
__instance.roleScreenshot.sprite = Sprite.Create(
customRole.OptionsScreenshot.LoadAsset().texture,
new Rect(0, 0, 370, 230),
Vector2.one / 2,
100);
__instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced;
__instance.roleHeaderSprite.color = customRole.RoleColor;
__instance.roleHeaderText.color = customRole.RoleColor.GetAlternateColor();
var bg = __instance.AdvancedRolesSettings.transform.Find("Background");
bg.localPosition = new Vector3(1.4041f, -7.08f, 0);
bg.GetComponent<SpriteRenderer>().size = new Vector2(89.4628f, 100);
CreateAdvancedSettings(__instance, role);
foreach (var optionBehaviour in __instance.advancedSettingChildren)
{
if (optionBehaviour.IsCustom())
{
continue;
}
optionBehaviour.OnValueChanged = new Action<OptionBehaviour>(__instance.ValueChanged);
if (AmongUsClient.Instance && !AmongUsClient.Instance.AmHost)
{
optionBehaviour.SetAsPlayer();
}
}
__instance.RoleChancesSettings.SetActive(false);
__instance.AdvancedRolesSettings.SetActive(true);
__instance.RefreshChildren();
}
public static void CreateQuotaOption(RolesSettingsMenu __instance, RoleBehaviour role, ref float yPos, int index)
private static void CreateQuotaOption(RolesSettingsMenu __instance, RoleBehaviour role, ref float yPos, int index)
{
var customRole = role as ICustomRole;
var roleOptionSetting = Object.Instantiate(__instance.roleOptionSettingOrigin, Vector3.zero, Quaternion.identity, __instance.RoleChancesSettings.transform);
if (role is not ICustomRole customRole)
{
Logger<MiraApiPlugin>.Error($"Role {role.NiceName} is not a custom role.");
return;
}
var roleOptionSetting = Object.Instantiate(
__instance.roleOptionSettingOrigin,
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
roleOptionSetting.transform.localPosition = new Vector3(-0.15f, yPos, -2f);
roleOptionSetting.SetRole(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions, role, 20);
roleOptionSetting.labelSprite.color = customRole.RoleColor;
@@ -255,28 +315,30 @@ public static class RoleSettingMenuPatches
roleOptionSetting.titleText.color = customRole.RoleColor.GetAlternateColor();
roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left;
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
newButton.name = "ConfigButton";
newButton.transform.localPosition = new Vector3(0.4473f, -0.3f, -2f);
newButton.transform.FindChild("Text_TMP").gameObject.DestroyImmediate();
newButton.activeSprites.Destroy();
var btnRend = newButton.transform.FindChild("ButtonSprite").GetComponent<SpriteRenderer>();
btnRend.sprite = MiraAssets.Cog.LoadAsset();
var passiveButton = newButton.GetComponent<GameOptionButton>();
passiveButton.OnClick = new ButtonClickedEvent();
passiveButton.interactableColor = btnRend.color = customRole.RoleColor.GetAlternateColor();
passiveButton.interactableHoveredColor = Color.white;
passiveButton.OnClick.AddListener((UnityAction)(() =>
if (GameSettingMenuPatches.SelectedMod is null ||
GameSettingMenuPatches.SelectedMod.Options.Exists(
x => x.AdvancedRole != null && x.AdvancedRole.IsInstanceOfType(role)))
{
ChangeTab(role, __instance);
}));
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
newButton.name = "ConfigButton";
newButton.transform.localPosition = new Vector3(0.4473f, -0.3f, -2f);
newButton.transform.FindChild("Text_TMP").gameObject.DestroyImmediate();
newButton.activeSprites.Destroy();
if (index < GameSettingMenuPatches.SelectedMod.CustomRoles.Count - 1)
var btnRend = newButton.transform.FindChild("ButtonSprite").GetComponent<SpriteRenderer>();
btnRend.sprite = MiraAssets.Cog.LoadAsset();
var passiveButton = newButton.GetComponent<GameOptionButton>();
passiveButton.OnClick = new ButtonClickedEvent();
passiveButton.interactableColor = btnRend.color = customRole.RoleColor.GetAlternateColor();
passiveButton.interactableHoveredColor = Color.white;
passiveButton.OnClick.AddListener((UnityAction)(() => { ChangeTab(role, __instance); }));
}
if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1)
{
yPos += -0.43f;
}
}
}
}
+31 -22
View File
@@ -1,42 +1,51 @@
using HarmonyLib;
using System.Globalization;
using HarmonyLib;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Patches.Options;
/// <summary>
/// Patches for the various game settings.
/// </summary>
[HarmonyPatch]
public class SettingPatches
public static class SettingPatches
{
/// <summary>
/// Prefix for the <see cref="FloatGameSetting.GetValueString"/> method. Adds support for custom number suffixes.
/// </summary>
/// <param name="__instance">The FloatGameSetting instance.</param>
/// <param name="__result">The result of the GetValueString method.</param>
/// <param name="value">The float value.</param>
/// <returns>False to skip original method.</returns>
[HarmonyPrefix]
[HarmonyPatch(typeof(FloatGameSetting), nameof(FloatGameSetting.GetValueString))]
public static bool ValueStringPatch(FloatGameSetting __instance, ref string __result, [HarmonyArgument(0)] float value)
public static bool ValueStringPatch(
FloatGameSetting __instance,
ref string __result,
[HarmonyArgument(0)] float value)
{
string result = string.Empty;
MiraNumberSuffixes suffix = (MiraNumberSuffixes)__instance.SuffixType;
string result;
var suffix = (MiraNumberSuffixes)__instance.SuffixType;
if (__instance.ZeroIsInfinity && Mathf.Abs(value) < 0.0001f)
{
result = "<b>∞</b>";
}
else if (suffix == MiraNumberSuffixes.None)
{
result = value.ToString(__instance.FormatString);
}
else if (suffix == MiraNumberSuffixes.Multiplier)
{
result = value.ToString(__instance.FormatString) + "x";
}
else if (suffix == MiraNumberSuffixes.Percent)
{
result = value.ToString(__instance.FormatString) + "%";
}
else
{
result = DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.GameSecondsAbbrev, (Il2CppSystem.Object[])(
[
value.ToString(__instance.FormatString)
]));
result = suffix switch
{
MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo),
MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x",
MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%",
_ => DestroyableSingleton<TranslationController>.Instance.GetString(
StringNames.GameSecondsAbbrev,
(Il2CppSystem.Object[]) [value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
};
}
__result = result;
return false;
}
}
}
+41 -7
View File
@@ -1,14 +1,22 @@
using HarmonyLib;
using MiraAPI.Animations;
using MiraAPI.Hud;
using MiraAPI.Roles;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using Reactor.Utilities.Extensions;
namespace MiraAPI.Patches;
/// <summary>
/// General patches for the PlayerControl class.
/// </summary>
[HarmonyPatch(typeof(PlayerControl))]
public static class PlayerControlPatches
{
/// <summary>
/// Adds the modifier and animator component to the player on start.
/// </summary>
/// <param name="__instance">The player which will be given the components.</param>
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.Start))]
public static void PlayerControlStartPostfix(PlayerControl __instance)
@@ -18,9 +26,36 @@ public static class PlayerControlPatches
comp.DestroyImmediate();
}
if (__instance.gameObject.TryGetComponent<CustomAnimator>(out var anim))
{
anim.DestroyImmediate();
}
__instance.gameObject.AddComponent<CustomAnimator>();
__instance.gameObject.AddComponent<ModifierComponent>();
}
/// <summary>
/// Calls the OnDeath method for all active modifiers.
/// </summary>
/// <param name="__instance">The player who died.</param>
/// <param name="reason">The reason of the death.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.Die))]
public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason)
{
var modifiersComponent = __instance.GetComponent<ModifierComponent>();
if (modifiersComponent)
{
modifiersComponent.ActiveModifiers.ForEach(x => x.OnDeath(reason));
}
}
/// <summary>
/// FixedUpdate handler for custom roles and custom buttons.
/// </summary>
/// <param name="__instance">The player who's fixed update is being called.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.FixedUpdate))]
public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance)
@@ -29,26 +64,25 @@ public static class PlayerControlPatches
{
customRole.PlayerControlFixedUpdate(__instance);
}
if (!__instance.AmOwner)
{
return;
}
foreach (var button in CustomButtonManager.CustomButtons)
{
if (__instance.Data?.Role == null)
{
continue;
}
if (!button.Enabled(__instance.Data?.Role))
{
continue;
}
button.FixedUpdateHandler(__instance);
}
}
}
}
+7 -4
View File
@@ -3,15 +3,18 @@ using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
/// <summary>
/// Patches for custom ejection messages.
/// </summary>
[HarmonyPatch(typeof(ExileController))]
public static class EjectionPatches
{
[HarmonyPostfix]
[HarmonyPatch(nameof(ExileController.Begin))]
public static void Begin(ExileController __instance)
public static void BeginPostfix(ExileController __instance)
{
if (!__instance.initData.networkedPlayer || !__instance.initData.networkedPlayer.Role || __instance.initData.networkedPlayer.Role is not ICustomRole role)
if (!__instance.initData.networkedPlayer || !__instance.initData.networkedPlayer.Role ||
__instance.initData.networkedPlayer.Role is not ICustomRole role)
{
return;
}
@@ -23,4 +26,4 @@ public static class EjectionPatches
__instance.completeString = role.GetCustomEjectionMessage(__instance.initData.networkedPlayer);
}
}
}
+1 -3
View File
@@ -1,5 +1,4 @@
using HarmonyLib;
using MiraAPI.Colors;
using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
@@ -23,6 +22,5 @@ public static class GameStartupPatch
_runOnce = true;
CustomRoleManager.RegisterInRoleManager();
PaletteManager.RegisterAllColors();
}
}
}
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff. Show more