Compare commits

...
82 Commits
Author SHA1 Message Date
XtraCube 4fcfb2a9ee fix LobbyViewPanePatches.cs AGAIN 2024-09-07 10:38:37 -04:00
XtraCube 41bedd3e16 bump version 2024-09-07 10:24:18 -04:00
XtraCube d27bf67f65 fix lobby view pane issues 2024-09-07 10:23:59 -04:00
XtraCube 76ddfa4126 Merge branch 'dev' 2024-09-05 18:10:04 -04:00
XtraCube 400ad535a4 bump version 2024-09-05 18:09:50 -04:00
XtraCube eaf5890f67 Merge pull request #13 from CallOfCreator/dev 2024-09-05 08:08:52 -04:00
XtraCube b2651adcab adjustments 2024-09-05 07:18:20 -04:00
XtraCube c8c6ab1f61 Update README.md 2024-09-04 18:49:24 -04:00
XtraCube 7f65f2d5bb remove warning from example build 2024-09-04 18:34:45 -04:00
XtraCube 17da594207 fix notif popper issue 2024-09-04 18:34:36 -04:00
ang-xd fa8fdfc4f6 Create ChameleonRole.cs 2024-09-04 17:30:08 -04:00
CallOfCreator 75dd5e8a35 Add CanUseSabotage support in ICustomRole 2024-09-04 16:27:57 +00: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
116 changed files with 3305 additions and 1812 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
[*.{cs,vb}]
dotnet_diagnostic.CA1707.severity = none
+2 -2
View File
@@ -46,6 +46,6 @@ jobs:
files: MiraAPI/bin/Release/net6.0/MiraAPI.dll
- name: Push NuGet package
if: github.repository == 'All-Of-Us-Mods/MiraAPI' && github.ref == 'refs/heads/master' && github.ref_type == 'tag'
if: github.ref_type == 'tag'
run: |
dotnet nuget push {MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg} --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
dotnet nuget push MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
+1 -1
View File
@@ -2,7 +2,7 @@
<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="AmongUs.GameLibs.Steam" Version="2024.9.4" 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" />
+6 -1
View File
@@ -8,7 +8,7 @@
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.1.3</VersionPrefix>
<VersionPrefix>0.1.7</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>All Of Us, XtraCube, Angxl</Authors>
@@ -33,6 +33,11 @@
<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>
@@ -1,5 +1,7 @@
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;
@@ -11,9 +13,9 @@ namespace MiraAPI.Example.Buttons.Freezer;
public class FreezeButton : CustomActionButton<PlayerControl>
{
public override string Name => "Freeze";
public override float Cooldown => 5f;
public override float Cooldown => OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeDuration;
public override float EffectDuration => 0f;
public override int MaxUses => 0;
public override int MaxUses => (int)OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeUses;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
protected override void OnClick()
@@ -21,9 +23,9 @@ public class FreezeButton : CustomActionButton<PlayerControl>
Target?.RpcAddModifier<FreezeModifier>();
}
public override PlayerControl GetTarget()
public override PlayerControl? GetTarget()
{
return PlayerControl.LocalPlayer.Data.Role.FindClosestTarget();
return PlayerControl.LocalPlayer.GetClosestPlayer(true, Distance);
}
public override void SetOutline(bool active)
@@ -31,14 +33,13 @@ public class FreezeButton : CustomActionButton<PlayerControl>
Target?.cosmetics.SetOutline(active, new Il2CppSystem.Nullable<Color>(Palette.Blue));
}
public override bool IsTargetValid(PlayerControl target)
public override bool IsTargetValid(PlayerControl? target)
{
return true;
}
public override bool Enabled(RoleBehaviour role)
public override bool Enabled(RoleBehaviour? role)
{
return role is FreezerRole;
}
}
}
+4 -3
View File
@@ -1,10 +1,11 @@
using MiraAPI.Example;
using MiraAPI.Example.Modifiers;
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
{
@@ -18,7 +19,7 @@ public class MeetingButton : CustomActionButton
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override bool Enabled(RoleBehaviour role)
public override bool Enabled(RoleBehaviour? role)
{
return PlayerControl.LocalPlayer.HasModifier<CaptainModifier>();
}
@@ -13,7 +13,7 @@ public class TeleportButton : CustomActionButton
{
public override string Name => "Teleport";
public override float Cooldown => OptionGroupSingleton<TeleporterOptions>.Instance.TeleportCooldown;
public override float Cooldown => OptionGroupSingleton<TeleporterOptions>.Instance.TeleportCooldown.Value;
public override float EffectDuration => OptionGroupSingleton<TeleporterOptions>.Instance.TeleportDuration;
@@ -22,7 +22,7 @@ public class TeleportButton : CustomActionButton
public override LoadableAsset<Sprite> Sprite => ExampleAssets.TeleportButton;
public static bool IsZoom { get; private set; }
public override bool Enabled(RoleBehaviour role)
public override bool Enabled(RoleBehaviour? role)
{
return role is TeleporterRole;
}
+2 -2
View File
@@ -6,7 +6,7 @@ public static class ExampleAssets
{
public static LoadableResourceAsset ExampleButton { get; } = new("MiraAPI.Example.Resources.ExampleButton.png");
// Credit to EpicHorrors for the teleport button asset.
// 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>();
}
}
}
+12 -16
View File
@@ -1,31 +1,27 @@
using MiraAPI.Example.Modifiers;
using MiraAPI.Utilities;
using System;
using Il2CppInterop.Runtime.Attributes;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.ImGui;
using System;
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();
}
}
}
+2 -1
View File
@@ -7,6 +7,7 @@ namespace MiraAPI.Example.Modifiers;
public class CaptainModifier : GameModifier
{
public override string ModifierName => "Captain";
public override int GetAmountPerGame()
{
return 1;
@@ -16,4 +17,4 @@ public class CaptainModifier : GameModifier
{
return 100;
}
}
}
@@ -1,5 +1,8 @@
using MiraAPI.Modifiers;
using Il2CppSystem;
using MiraAPI.Example.Roles;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
using UnityEngine;
namespace MiraAPI.Example.Modifiers.Freezer;
@@ -14,15 +17,25 @@ public class FreezeModifier : TimedModifier
public override void OnActivate()
{
if (Player.AmOwner)
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)
if (Player?.AmOwner == true)
{
Player.moveable = true;
}
+5 -8
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("Best API", typeof(BestApiEnum), ["Mira API", "Mitochondria", "Reactor"])]
public BestApiEnum EnumOpt { get; set; } = BestApiEnum.MiraAPI;
[ModdedEnumOption("Best API", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])]
public BestApi Opt { get; set; } = BestApi.MiraAPI;
}
public enum BestApiEnum
public enum BestApi
{
MiraAPI,
Mitochondria,
Reactor
}
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(FreezerRole);
[ModdedToggleOption("Teleportation ability")] public bool Teleport { get; set; } = true;
}
@@ -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;
}
@@ -1,8 +1,9 @@
using MiraAPI.Example.Roles;
using System;
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Utilities;
using System;
namespace MiraAPI.Example.Options.Roles;
@@ -12,12 +13,11 @@ public class TeleporterOptions : AbstractOptionGroup
public override Type AdvancedRole => typeof(TeleporterRole);
[ModdedNumberOption("Teleport Cooldown", 5, 60, 2.5f, MiraNumberSuffixes.Seconds)]
public float TeleportCooldown { get; set; } = 5;
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;
}
}
+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);
}
}
}
}
+4 -2
View File
@@ -1,4 +1,5 @@
using MiraAPI.Roles;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using UnityEngine;
@@ -12,6 +13,7 @@ public class FreezerRole : ImpostorRole, ICustomRole
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);
}
}
+4 -3
View File
@@ -1,4 +1,5 @@
using MiraAPI.Roles;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using UnityEngine;
@@ -12,6 +13,6 @@ public class TeleporterRole : CrewmateRole, ICustomRole
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;
}
}
+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, string[] values = null, 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, values, 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);
}
+66 -45
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());
@@ -123,51 +139,56 @@ public static class ModdedOptionsManager
// 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
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins.Values)
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.Values)
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
{
plugin.PluginConfig.Save();
plugin.PluginConfig.SaveOnConfigSet = true;
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,19 +1,34 @@
using MiraAPI.Networking;
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, string[] values = null, 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 = values is null ? Enum.GetNames(enumType) : values;
Values = values ?? Enum.GetNames(enumType);
Data = ScriptableObject.CreateInstance<StringGameSetting>();
var data = (StringGameSetting)Data;
@@ -26,6 +41,7 @@ public class ModdedEnumOption : ModdedOption<int>
data.Index = Value;
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
@@ -35,7 +51,7 @@ public class ModdedEnumOption : ModdedOption<int>
// 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;
@@ -43,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;
}
}
}
}
+126 -22
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; }
/// <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; }
public IMiraPlugin ParentMod
/// <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,18 +118,23 @@ 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)
@@ -70,24 +143,55 @@ public abstract class ModdedOption<T> : IModdedOption
{
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,
}
+39 -38
View File
@@ -1,5 +1,4 @@
#nullable enable
using MiraAPI.Utilities.Assets;
using MiraAPI.Utilities.Assets;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
@@ -7,67 +6,67 @@ 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>
public bool EffectActive { get; protected set; }
/// <summary>
/// Returns the amount of uses left.
/// Gets or sets the amount of uses left.
/// </summary>
public int UsesLeft { get; protected set; }
/// <summary>
/// A timer variable to measure cooldowns and effects.
/// Gets or sets the timer variable to measure cooldowns and effects.
/// </summary>
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; }
@@ -84,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();
@@ -116,7 +115,7 @@ public abstract class CustomActionButton
/// <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)
@@ -128,7 +127,7 @@ 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);
@@ -137,7 +136,7 @@ public abstract class CustomActionButton
/// <summary>
/// Set the button's timer.
/// </summary>
/// <param name="amount">The amount you want to set to.</param>
/// <param name="time">The time you want to set to.</param>
public void SetTimer(float time)
{
Timer = Mathf.Clamp(time, -1, float.MaxValue);
@@ -161,7 +160,6 @@ public abstract class CustomActionButton
SetTimer(Timer - amount);
}
/// <summary>
/// Set the amount of uses this button has left.
/// </summary>
@@ -174,7 +172,7 @@ public abstract class CustomActionButton
/// <summary>
/// Increase the amount of uses this button has left.
/// </summary>
/// <param name="amount">The amount you want to increase by. Default: 1</param>
/// <param name="amount">The amount you want to increase by. Default: 1.</param>
public void IncreaseUses(int amount = 1)
{
SetUses(UsesLeft + amount);
@@ -183,7 +181,7 @@ public abstract class CustomActionButton
/// <summary>
/// Decrease the amount of uses this button has left.
/// </summary>
/// <param name="amount">The amount you want to decrease by. Default: 1</param>
/// <param name="amount">The amount you want to decrease by. Default: 1.</param>
public void DecreaseUses(int amount = 1)
{
SetUses(UsesLeft - amount);
@@ -192,8 +190,10 @@ public abstract class CustomActionButton
/// <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) { }
/// <param name="playerControl">the local PlayerControl.</param>
protected virtual void FixedUpdate(PlayerControl playerControl)
{
}
/// <summary>
/// Callback method for the button click event.
@@ -205,13 +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 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.
@@ -229,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)
{
@@ -272,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)
@@ -308,23 +311,20 @@ 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>
/// <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;
@@ -333,15 +333,16 @@ public abstract class CustomActionButton<T> : CustomActionButton where T : MonoB
/// <summary>
/// The method used to get the target object.
/// </summary>
/// <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();
@@ -355,4 +356,4 @@ public abstract class CustomActionButton<T> : CustomActionButton where T : MonoB
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;
+3 -4
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,10 +16,10 @@ 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()
{
+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;
}
+139 -66
View File
@@ -1,31 +1,58 @@
using MiraAPI.Modifiers.Types;
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 System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
}
@@ -34,55 +61,80 @@ public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr)
_modifierText.verticalAlignment = VerticalAlignmentOptions.Top;
}
public void FixedUpdate()
private void FixedUpdate()
{
if (ActiveModifiers is null) return;
foreach (var modifier in ActiveModifiers)
foreach (var modifier in _toRemove)
{
if (modifier is null) continue;
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()
{
if (ActiveModifiers is null) return;
foreach (var modifier in ActiveModifiers)
foreach (var modifier in Modifiers)
{
if (modifier is null) continue;
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;
}
}
/// <summary>
/// Removes a modifier from the player.
/// </summary>
/// <param name="type">The modifier type.</param>
[HideFromIl2Cpp]
public void RemoveModifier(Type type)
{
var modifier = ActiveModifiers.Find(x => x.GetType() == type);
var modifier = Modifiers.Find(x => x.GetType() == type);
if (modifier is null)
{
@@ -93,14 +145,24 @@ public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr)
RemoveModifier(modifier);
}
/// <summary>
/// Removes a modifier from the player.
/// </summary>
/// <typeparam name="T">The modifier type.</typeparam>
[HideFromIl2Cpp]
public void RemoveModifier<T>() where T : BaseModifier
{
RemoveModifier(typeof(T));
}
/// <summary>
/// Removes a modifier from the player.
/// </summary>
/// <param name="modifierId">The modifier ID.</param>
[HideFromIl2Cpp]
public void RemoveModifier(uint modifierId)
{
var modifier = ActiveModifiers.Find(x => x.ModifierId == modifierId);
var modifier = Modifiers.Find(x => x.ModifierId == modifierId);
if (modifier is null)
{
@@ -111,68 +173,73 @@ public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr)
RemoveModifier(modifier);
}
/// <summary>
/// Removes a modifier from the player.
/// </summary>
/// <param name="modifier">The modifier object.</param>
[HideFromIl2Cpp]
public void RemoveModifier(BaseModifier modifier)
{
if (!ActiveModifiers.Contains(modifier))
if (!Modifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player.");
return;
}
modifier.OnDeactivate();
ActiveModifiers.Remove(modifier);
if (player.AmOwner)
{
HudManager.Instance.SetHudActive(true);
}
_toRemove.Add(modifier);
}
public BaseModifier AddModifier(BaseModifier 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 (ActiveModifiers.Contains(modifier))
if (Modifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Player already has modifier with id {modifier.ModifierId}!");
return null;
}
ActiveModifiers.Add(modifier);
modifier.Player = player;
modifier.ModifierId = ModifierManager.TypeToIdModifiers[modifier.GetType()];
modifier.OnActivate();
var modifierId = ModifierManager.GetModifierId(modifier.GetType());
if (!player.AmOwner)
if (modifierId == null)
{
return modifier;
Logger<MiraApiPlugin>.Error($"Cannot add modifier {modifier.GetType().Name} because it has no ID!");
return null;
}
if (modifier is TimedModifier { AutoStart: true } timer)
{
timer.StartTimer();
}
HudManager.Instance.SetHudActive(true);
_toAdd.Add(modifier);
modifier.Player = _player;
modifier.ModifierComponent = this;
modifier.ModifierId = modifierId.Value;
return modifier;
}
public BaseModifier AddModifier(Type type)
/// <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)
{
if (!ModifierManager.TypeToIdModifiers.TryGetValue(type, out var modifierId))
var modifierId = ModifierManager.GetModifierId(type);
if (modifierId == null)
{
Logger<MiraApiPlugin>.Error($"Cannot add modifier {type.Name} because it is not registered.");
return null;
}
if (ActiveModifiers.Find(x => x.ModifierId == modifierId) != null)
if (Modifiers.Find(x => x.ModifierId == modifierId) != null)
{
Logger<MiraApiPlugin>.Error($"Player already has modifier with id {modifierId}!");
return null;
}
var modifier = (BaseModifier)Activator.CreateInstance(type);
if (modifier is null)
if (Activator.CreateInstance(type) is not BaseModifier modifier)
{
Logger<MiraApiPlugin>.Error($"Cannot add modifier {type.Name} because it is null.");
return null;
@@ -183,8 +250,14 @@ public class ModifierComponent(IntPtr ptr) : MonoBehaviour(ptr)
return modifier;
}
public T AddModifier<T>() where T : BaseModifier
/// <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;
}
}
}
+65 -45
View File
@@ -1,21 +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.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
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;
@@ -25,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))
@@ -32,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)
@@ -42,34 +65,32 @@ public static class ModifierManager
List<uint> filteredModifiers = [];
foreach (var modifier in IdToTypeModifiers.Where(pair => pair.Value.IsAssignableTo(typeof(GameModifier))))
foreach (var modifier in IdToTypeModifierMap.Where(pair => pair.Value.IsAssignableTo(typeof(GameModifier))))
{
var mod = (GameModifier)Activator.CreateInstance(modifier.Value);
if (mod is null)
if (Activator.CreateInstance(modifier.Value) is not GameModifier mod)
{
Logger<MiraApiPlugin>.Error($"Failed to create instance of {modifier.Value.Name}");
continue;
}
if (!plrs.Any(x=>IsGameModifierValid(x, mod, modifier.Key)))
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 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++)
{
var randomNum = rand.Next(100);
if (randomNum < Math.Clamp(chance, 0, 100))
{
filteredModifiers.Add(TypeToIdModifiers[modifier.Value]);
filteredModifiers.Add(TypeToIdModifierMap[modifier.Value]);
}
}
}
@@ -79,28 +100,26 @@ public static class ModifierManager
{
shuffledModifiers = shuffledModifiers.GetRange(0, plrs.Count);
}
while (shuffledModifiers.Count > 0)
{
var id = shuffledModifiers[0];
var mod = (GameModifier)Activator.CreateInstance(IdToTypeModifiers[id]);
if (mod is null)
if (Activator.CreateInstance(IdToTypeModifierMap[id]) is not GameModifier mod)
{
Logger<MiraApiPlugin>.Error($"Failed to create instance of {IdToTypeModifiers[id].Name}");
Logger<MiraApiPlugin>.Error($"Failed to create instance of {IdToTypeModifierMap[id].Name}");
continue;
}
if (!plrs.Any(x=>IsGameModifierValid(x, mod, id)))
if (!plrs.Exists(x => IsGameModifierValid(x, mod, id)))
{
shuffledModifiers.RemoveAt(0);
continue;
}
var plr = plrs.Random();
if (!IsGameModifierValid(plr, mod, id))
if (plr == null || !IsGameModifierValid(plr, mod, id))
{
continue;
}
@@ -116,17 +135,17 @@ public static class ModifierManager
modifier.IsModifierValidOn(player.Data.Role) &&
!player.HasModifier(modifierId);
}
internal static void SyncAllModifiers(int targetId = -1)
{
var data = new List<NetData>();
foreach (var player in GameData.Instance.AllPlayers)
foreach (var player in PlayerControl.AllPlayerControls)
{
data.Add(GetPlayerModifiers(player.Object));
data.Add(GetPlayerModifiers(player));
}
Rpc<SyncModifiersRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, data.ToArray());
Rpc<SyncModifiersRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, [.. data]);
}
internal static void HandleSyncModifiers(NetData[] data)
@@ -144,39 +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)
{
if (!IdToTypeModifiers.TryGetValue(id, out var type))
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;
}
}
+44 -4
View File
@@ -1,22 +1,59 @@
using MiraAPI.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;
}
@@ -33,11 +70,14 @@ public abstract class TimedModifier : BaseModifier
if (RemoveOnComplete)
{
Player.RpcRemoveModifier(ModifierId);
ModifierComponent?.RemoveModifier(ModifierId);
}
}
}
/// <summary>
/// Starts the timer.
/// </summary>
public void StartTimer()
{
if (TimerActive)
@@ -49,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;
}
}
-186
View File
@@ -1,186 +0,0 @@
using AmongUs.GameOptions;
using Assets.CoreScripts;
using Reactor.Networking.Attributes;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using System.Collections;
using System.Linq;
using UnityEngine;
namespace MiraAPI.Networking;
public static class CustomMurderRpcs
{
[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)
{
MurderResultFlags murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError;
MurderResultFlags murderResultFlags2 = MurderResultFlags.DecisionByHost | murderResultFlags;
source.CustomMurder(target, murderResultFlags2, resetKillTimer, createDeadBody, teleportMurderer, showKillAnim, playKillSound);
}
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;
source.logger.Debug(string.Format("{0} trying to murder {1}", source.PlayerId, target.PlayerId), null);
NetworkedPlayerInfo data = target.Data;
if (resultFlags.HasFlag(MurderResultFlags.FailedError))
{
return;
}
if (resultFlags.HasFlag(MurderResultFlags.FailedProtected) || (resultFlags.HasFlag(MurderResultFlags.DecisionByHost) && target.protectedByGuardianId > -1))
{
target.protectedByGuardianThisRound = true;
bool 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();
}
source.logger.Debug(string.Format("{0} failed to murder {1} due to guardian angel protection", source.PlayerId, target.PlayerId), null);
return;
}
if (resultFlags.HasFlag(MurderResultFlags.Succeeded) || resultFlags.HasFlag(MurderResultFlags.DecisionByHost))
{
DestroyableSingleton<DebugAnalytics>.Instance.Analytics.Kill(target.Data, source.Data);
if (source.AmOwner)
{
if (GameManager.Instance.IsHideAndSeek())
{
StatsManager.Instance.IncrementStat(StringNames.StatsImpostorKills_HideAndSeek);
}
else
{
StatsManager.Instance.IncrementStat(StringNames.StatsImpostorKills);
}
if (source.CurrentOutfitType == PlayerOutfitType.Shapeshifted)
{
StatsManager.Instance.IncrementStat(StringNames.StatsShapeshifterShiftedKills);
}
if (Constants.ShouldPlaySfx() && playKillSound)
{
SoundManager.Instance.PlaySound(source.KillSfx, false, 0.8f, null);
}
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
{
}
}
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, (int)target.PlayerId);
Coroutines.Start(source.KillAnimations.Random().CoPerformCustomKill(source, target, createDeadBody, teleportMurderer));
source.logger.Debug(string.Format("{0} succeeded in murdering {1}", source.PlayerId, target.PlayerId), null);
}
}
public static IEnumerator CoPerformCustomKill(this KillAnimation anim, PlayerControl source, PlayerControl target,
bool createDeadBody = true, bool teleportMurderer = true)
{
FollowerCamera cam = Camera.main.GetComponent<FollowerCamera>();
bool isParticipant = PlayerControl.LocalPlayer == source || PlayerControl.LocalPlayer == target;
PlayerPhysics 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.ToArray().ToList().ForEach(target.SetPlayerMaterialColors);
target.SetPlayerMaterialColors(deadBody.bloodSplatter);
Vector3 vector = target.transform.position + anim.BodyOffset;
vector.z = vector.y / 1000f;
deadBody.transform.position = vector;
}
if (isParticipant)
{
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)
{
deadBody.enabled = true;
}
if (isParticipant)
{
cam.Locked = false;
PlayerControl.LocalPlayer.isKilling = false;
source.isKilling = false;
}
yield break;
}
}
+27 -1
View File
@@ -1,11 +1,37 @@
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,
/// <summary>
/// Syncs all modifiers at once.
/// </summary>
SyncModifiers,
/// <summary>
/// Custom Murder RPC.
/// </summary>
CustomMurder,
}
}
+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 -72
View File
@@ -1,6 +1,4 @@
using System;
using HarmonyLib;
using InnerNet;
using HarmonyLib;
using MiraAPI.Hud;
using MiraAPI.Roles;
using Reactor.Utilities.Extensions;
@@ -9,58 +7,14 @@ 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 +28,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 +61,14 @@ public static class HudManagerPatches
{
ButtonLocation.BottomLeft => _bottomLeft.transform,
ButtonLocation.BottomRight => bottomRight,
_ => null
_ => null,
};
if (location is null)
{
continue;
}
button.CreateButton(location);
}
@@ -124,30 +79,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.ResetCooldownAndOrEffect();
}
}
}
+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;
}
}
}
@@ -1,5 +1,4 @@
#nullable enable
using HarmonyLib;
using HarmonyLib;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
@@ -13,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; }
/// <summary>
/// Gets the currently selected mod.
/// </summary>
public static MiraPluginInfo? SelectedMod { get; private set; }
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)
@@ -57,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;
@@ -71,17 +84,18 @@ 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 (_text is not null && CurrentSelectedMod == 0)
if (_text is not null && SelectedModIdx == 0)
{
_text.text = "Default";
_text.fontSizeMax = 3.2f;
@@ -89,22 +103,17 @@ public static class GameSettingMenuPatches
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)
{
@@ -122,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)
{
@@ -145,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();
+268 -243
View File
@@ -1,5 +1,4 @@
#nullable enable
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
@@ -7,6 +6,7 @@ using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
@@ -17,11 +17,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,102 +35,104 @@ 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)
{
if (group.Options.Count == 0)
{
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 +148,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 +167,245 @@ public static class LobbyViewPanePatches
{
num2 = -3f;
}
viewSettingsInfoPanel.transform.localPosition = new Vector3(num2, num, -2f);
var data = option.Data;
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.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;
}
}
}
@@ -6,34 +6,40 @@ using System.Linq;
namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(NotificationPopper))]
public class NotificationPopperPatch
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,
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.Any(role => role.StringName == key))
if (CustomRoleManager.CustomRoles.Values.FirstOrDefault(x=>x.StringName==key) is not ICustomRole customRole)
{
string item = string.Empty;
string text = (teamType == RoleTeamTypes.Crewmate) ? Palette.CrewmateSettingChangeText.ToTextColor() : Palette.ImpostorRed.ToTextColor();
item = DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.LobbyChangeSettingNotificationRole, new Object[]
{
return true;
}
var text = teamType == RoleTeamTypes.Crewmate
? Palette.CrewmateSettingChangeText.ToTextColor()
: Palette.ImpostorRed.ToTextColor();
var item = DestroyableSingleton<TranslationController>.Instance.GetString(
StringNames.LobbyChangeSettingNotificationRole,
string.Concat(
[
"<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.ToString() + "</font>",
"<font=\"Barlow-Black SDF\" material=\"Barlow-Black Outline\">" + roleChance.ToString() + "%"
});
__instance.SettingsChangeMessageLogic(key, item, playSound);
return false;
}
),
"<font=\"Barlow-Black SDF\" material=\"Barlow-Black Outline\">" + roleCount + "</font>",
"<font=\"Barlow-Black SDF\" material=\"Barlow-Black Outline\">" + roleChance + "%"
);
return true;
__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();
}
}
}
@@ -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,12 +244,24 @@ 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();
@@ -244,15 +285,25 @@ public static class RoleSettingMenuPatches
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;
@@ -265,7 +316,8 @@ public static class RoleSettingMenuPatches
roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left;
if (GameSettingMenuPatches.SelectedMod is null ||
GameSettingMenuPatches.SelectedMod.Options.Any(x => x.AdvancedRole != null && x.AdvancedRole.IsInstanceOfType(role)))
GameSettingMenuPatches.SelectedMod.Options.Exists(
x => x.AdvancedRole != null && x.AdvancedRole.IsInstanceOfType(role)))
{
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
newButton.name = "ConfigButton";
@@ -289,4 +341,4 @@ public static class RoleSettingMenuPatches
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;
}
}
}
+30 -7
View File
@@ -1,14 +1,20 @@
using HarmonyLib;
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 component to the player on start.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.Start))]
public static void PlayerControlStartPostfix(PlayerControl __instance)
@@ -21,6 +27,24 @@ public static class PlayerControlPatches
__instance.gameObject.AddComponent<ModifierComponent>();
}
/// <summary>
/// Calls the OnDeath method for all active modifiers.
/// </summary>
[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>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.FixedUpdate))]
public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance)
@@ -29,26 +53,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();
}
}
}
@@ -0,0 +1,75 @@
using HarmonyLib;
using InnerNet;
using MiraAPI.Roles;
using Reactor.Utilities.Extensions;
namespace MiraAPI.Patches.Roles;
/// <summary>
/// HudManager patches for roles.
/// </summary>
[HarmonyPatch(typeof(HudManager))]
public static class HudManagerPatches
{
// Custom role tab.
private static TaskPanelBehaviour? _roleTab;
/// <summary>
/// Fixes Kill Button not showing for Neutral killing role.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(nameof(HudManager.SetHudActive), typeof(PlayerControl), typeof(RoleBehaviour), typeof(bool))]
public static void SetHudActivePostfix(
HudManager __instance,
PlayerControl localPlayer,
RoleBehaviour role,
bool isActive)
{
var flag = localPlayer.Data != null && localPlayer.Data.IsDead;
if (role is ICustomRole customRole)
{
__instance.KillButton.ToggleVisible(isActive && role.CanUseKillButton && !flag);
__instance.ImpostorVentButton.ToggleVisible(isActive && role.CanVent && !flag);
__instance.SabotageButton.gameObject.SetActive(isActive && customRole.CanUseSabotage);
}
if (_roleTab)
{
_roleTab?.gameObject.SetActive(isActive);
}
}
/// <summary>
/// Update custom role tab and custom role hud elements.
/// </summary>
[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;
}
var role = local?.Data?.Role;
if (role is ICustomRole { RoleHintType: RoleHintType.RoleTab } customRole)
{
customRole.HudUpdate(__instance);
if (_roleTab == null)
{
_roleTab = CustomRoleManager.CreateRoleTab(customRole);
}
CustomRoleManager.UpdateRoleTab(_roleTab, customRole);
}
else if (_roleTab)
{
_roleTab?.gameObject.Destroy();
}
}
}
@@ -0,0 +1,71 @@
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.Networking;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
/// <summary>
/// Fix kill button issues for neutral killers.
/// </summary>
[HarmonyPatch(typeof(KillButton))]
public static class KillButtonPatches
{
/// <summary>
/// SetTarget for custom roles.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(KillButton.SetTarget))]
public static bool SetTargetPrefix(KillButton __instance, PlayerControl target)
{
if (!PlayerControl.LocalPlayer || PlayerControl.LocalPlayer.Data == null || !PlayerControl.LocalPlayer.Data.Role)
{
return false;
}
if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole)
{
return true;
}
if (__instance.currentTarget && __instance.currentTarget != target)
{
__instance.currentTarget.cosmetics.SetOutline(false, new Nullable<Color>(Color.clear));
}
__instance.currentTarget = target;
if (__instance.currentTarget)
{
__instance.currentTarget.cosmetics.SetOutline(true, new Nullable<Color>(customRole.KillButtonOutlineColor));
__instance.SetEnabled();
return false;
}
__instance.SetDisabled();
return false;
}
/// <summary>
/// Use Custom Murder if player is custom role.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(KillButton.DoClick))]
public static bool DoClickPrefix(KillButton __instance)
{
if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole)
{
return true;
}
if (!__instance.isActiveAndEnabled || !__instance.currentTarget || __instance.isCoolingDown ||
PlayerControl.LocalPlayer.Data.IsDead || !PlayerControl.LocalPlayer.CanMove)
{
return false;
}
PlayerControl.LocalPlayer.RpcCustomMurder(__instance.currentTarget);
__instance.SetTarget(null);
return false;
}
}
+1 -1
View File
@@ -32,4 +32,4 @@ public static class NameTagPatch
return false;
}
}
}
@@ -22,4 +22,4 @@ public static class RoleBehaviourPatches
__result = behaviour.RoleColor;
return false;
}
}
}
+5 -5
View File
@@ -3,7 +3,6 @@ using HarmonyLib;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using System.Linq;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
@@ -19,7 +18,9 @@ public static class RoleManagerPatches
return;
}
ModifierManager.AssignModifiers(PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected).ToList());
ModifierManager.AssignModifiers(
PlayerControl.AllPlayerControls.ToArray().Where(plr => !plr.Data.IsDead && !plr.Data.Disconnected)
.ToList());
}
[HarmonyPrefix]
@@ -40,9 +41,8 @@ public static class RoleManagerPatches
{
return true;
}
plr.RpcSetRole(role.GhostRole);
return false;
}
}
}
@@ -14,20 +14,20 @@ public static class RoleOptionsCollectionPatch
[HarmonyPatch(nameof(RoleOptionsCollectionV08.GetChancePerGame))]
public static bool GetChancePrefix([HarmonyArgument(0)] RoleTypes roleType, ref int __result)
{
if (!CustomRoleManager.GetCustomRoleBehaviour(roleType, out var customRole))
if (!CustomRoleManager.GetCustomRoleBehaviour(roleType, out var customRole) || customRole == null)
{
return true;
}
if (customRole.HideSettings)
{
__result = 0;
return false;
}
customRole.ParentMod.PluginConfig.TryGetEntry<int>(customRole.ChanceConfigDefinition, out var entry);
__result = entry.Value;
return false;
}
/// <summary>
@@ -37,18 +37,19 @@ public static class RoleOptionsCollectionPatch
[HarmonyPatch(nameof(RoleOptionsCollectionV08.GetNumPerGame))]
public static bool GetNumPrefix([HarmonyArgument(0)] RoleTypes roleType, ref int __result)
{
if (!CustomRoleManager.GetCustomRoleBehaviour(roleType, out var customRole))
if (!CustomRoleManager.GetCustomRoleBehaviour(roleType, out var customRole) || customRole == null)
{
return true;
}
if (customRole.HideSettings)
{
__result = 0;
return false;
}
customRole.ParentMod.PluginConfig.TryGetEntry<int>(customRole.NumConfigDefinition, out var entry);
__result = entry.Value;
return false;
}
}
@@ -0,0 +1,62 @@
using HarmonyLib;
using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(SabotageButton))]
public class SabotageButtonPatch
{
/// <summary>
/// Patches the Sabotage button to check if the player's custom role can use sabotage.
/// </summary>
[HarmonyPatch(nameof(SabotageButton.DoClick))]
[HarmonyPrefix]
public static bool DoClickPrefix(SabotageButton __instance)
{
var player = PlayerControl.LocalPlayer;
if (player.Data.Role is not ICustomRole customRole)
{
return true;
}
if (!customRole.CanUseSabotage || PlayerControl.LocalPlayer.inVent || !GameManager.Instance.SabotagesEnabled())
{
return false;
}
DestroyableSingleton<HudManager>.Instance.ToggleMapVisible(
new MapOptions
{
Mode = MapOptions.Modes.Sabotage,
});
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(SabotageButton.Refresh))]
public static bool RefreshPrefix(SabotageButton __instance)
{
var player = PlayerControl.LocalPlayer;
if (GameManager.Instance == null || player == null)
{
__instance.ToggleVisible(false);
__instance.SetDisabled();
return false;
}
if (player.Data.Role is not ICustomRole customRole)
{
return true;
}
if (player.inVent || !GameManager.Instance.SabotagesEnabled() || player.petting)
{
__instance.ToggleVisible(customRole.CanUseSabotage && GameManager.Instance.SabotagesEnabled());
__instance.SetDisabled();
return false;
}
__instance.SetEnabled();
return false;
}
}
+138 -92
View File
@@ -2,7 +2,6 @@
using HarmonyLib;
using Il2CppSystem;
using Il2CppSystem.Collections.Generic;
using Il2CppSystem.Text;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
@@ -16,9 +15,8 @@ namespace MiraAPI.Patches.Roles;
[HarmonyPatch]
public static class TaskAdderPatch
{
private static TaskFolder _rolesFolder;
private static readonly System.Collections.Generic.Dictionary<string, string> ModsFolders = new();
private static Scroller _scroller;
private static readonly System.Collections.Generic.Dictionary<string, string> ModsFolders = [];
private static Scroller? _scroller;
[HarmonyPostfix]
[HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.Begin))]
@@ -34,17 +32,17 @@ public static class TaskAdderPatch
GameObject hitbox = new("Hitbox")
{
layer = 5
layer = 5,
};
hitbox.transform.SetParent(__instance.TaskParent.transform, false);
hitbox.transform.localScale = new Vector3(7.5f, 6.5f, 1);
hitbox.transform.localPosition = new Vector3(2.8f, -2.2f, 0);
SpriteMask mask = hitbox.AddComponent<SpriteMask>();
var mask = hitbox.AddComponent<SpriteMask>();
mask.sprite = MiraAssets.NextButton.LoadAsset();
mask.alphaCutoff = 0.0f;
BoxCollider2D collider = hitbox.AddComponent<BoxCollider2D>();
var collider = hitbox.AddComponent<BoxCollider2D>();
collider.size = new Vector2(1f, 1f);
collider.enabled = true;
@@ -54,21 +52,25 @@ public static class TaskAdderPatch
__instance.RoleButton.GetComponent<PassiveButton>().ClickMask = collider;
__instance.RootFolderPrefab.GetComponent<PassiveButton>().ClickMask = collider;
__instance.RootFolderPrefab.gameObject.SetActive(false);
__instance.TaskParent = inner.transform;
_rolesFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
_rolesFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
_rolesFolder.gameObject.SetActive(false);
_rolesFolder.FolderName = "Roles";
_rolesFolder.name = "RolesFolder";
var rolesFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
rolesFolder.gameObject.SetActive(false);
rolesFolder.FolderName = "Roles";
rolesFolder.name = "RolesFolder";
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins.Values)
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
{
if (plugin.CustomRoles.Count == 0)
{
continue;
}
var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
newFolder.FolderName = newFolder.name = plugin.PluginInfo.Metadata.Name;
newFolder.gameObject.SetActive(false);
_rolesFolder.SubFolders.Add(newFolder);
rolesFolder.SubFolders.Add(newFolder);
if (!ModsFolders.ContainsKey(plugin.PluginInfo.Metadata.Name))
{
@@ -76,7 +78,7 @@ public static class TaskAdderPatch
}
}
__instance.Root.SubFolders.Add(_rolesFolder);
__instance.Root.SubFolders.Add(rolesFolder);
__instance.GoToRoot();
}
@@ -85,15 +87,20 @@ public static class TaskAdderPatch
[HarmonyPatch(typeof(TaskAddButton), "Role", MethodType.Setter)]
public static void RoleGetterPatch(TaskAddButton __instance)
{
if (__instance.role is ICustomRole { IsNeutral: true })
if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Neutral })
{
__instance.FileImage.color = Color.gray;
}
__instance.RolloverHandler.OutColor = __instance.FileImage.color;
}
private static void AddFileAsChildCustom(this TaskAdderGame instance, TaskFolder taskFolder, TaskAddButton item, ref float xCursor, ref float yCursor, ref float maxHeight)
private static void AddFileAsChildCustom(
this TaskAdderGame instance,
TaskAddButton item,
ref float xCursor,
ref float yCursor,
ref float maxHeight)
{
item.transform.SetParent(instance.TaskParent);
item.transform.localPosition = new Vector3(xCursor, yCursor, 0f);
@@ -106,27 +113,29 @@ public static class TaskAdderPatch
yCursor -= maxHeight;
maxHeight = 0f;
}
instance.ActiveItems.Add(item.transform);
}
// yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry:
[HarmonyPrefix]
[HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.ShowFolder))]
public static bool ShowPatch(TaskAdderGame __instance, [HarmonyArgument(0)] TaskFolder taskFolder)
{
var stringBuilder = new StringBuilder(64);
var stringBuilder = new Il2CppSystem.Text.StringBuilder(64);
__instance.Hierarchy.Add(taskFolder);
for (var i = 0; i < __instance.Hierarchy.Count; i++)
{
stringBuilder.Append(__instance.Hierarchy.ToArray()[i].FolderName);
stringBuilder.Append("\\");
}
__instance.PathText.text = stringBuilder.ToString();
for (var j = 0; j < __instance.ActiveItems.Count; j++)
{
Object.Destroy(__instance.ActiveItems.ToArray()[j].gameObject);
}
__instance.ActiveItems.Clear();
var num = 0f;
var num2 = 0f;
@@ -149,6 +158,7 @@ public static class TaskAdderPatch
num2 -= num3;
num3 = 0f;
}
__instance.ActiveItems.Add(folderTransform);
if (!taskFolder2 || !taskFolder2.Button)
{
@@ -156,7 +166,8 @@ public static class TaskAdderPatch
}
ControllerManager.Instance.AddSelectableUiElement(taskFolder2.Button);
if (!string.IsNullOrEmpty(__instance.restorePreviousSelectionByFolderName) && taskFolder2.FolderName.Equals(__instance.restorePreviousSelectionByFolderName))
if (!string.IsNullOrEmpty(__instance.restorePreviousSelectionByFolderName) &&
taskFolder2.FolderName.Equals(__instance.restorePreviousSelectionByFolderName, System.StringComparison.Ordinal))
{
__instance.restorePreviousSelectionFound = taskFolder2.Button;
}
@@ -174,63 +185,90 @@ public static class TaskAdderPatch
{
var taskAddButton = Object.Instantiate(__instance.TaskPrefab);
taskAddButton.MyTask = list.ToArray()[l];
if (taskAddButton.MyTask.TaskType == TaskTypes.DivertPower)
switch (taskAddButton.MyTask.TaskType)
{
var targetSystem = taskAddButton.MyTask.Cast<DivertPowerTask>().TargetSystem;
taskAddButton.Text.text = DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.DivertPowerTo, DestroyableSingleton<TranslationController>.Instance.GetString(targetSystem));
}
else if (taskAddButton.MyTask.TaskType == TaskTypes.FixWeatherNode)
{
var nodeId = ((WeatherNodeTask)taskAddButton.MyTask).NodeId;
taskAddButton.Text.text = DestroyableSingleton<TranslationController>.Instance.GetString(StringNames.FixWeatherNode, Array.Empty<Il2CppSystem.Object>()) + " " + DestroyableSingleton<TranslationController>.Instance.GetString(WeatherSwitchGame.ControlNames[nodeId],
Array.Empty<Il2CppSystem.Object>());
}
else
{
taskAddButton.Text.text = DestroyableSingleton<TranslationController>.Instance.GetString(taskAddButton.MyTask.TaskType);
}
__instance.AddFileAsChildCustom(taskFolder, taskAddButton, ref num, ref num2, ref num3);
if (taskAddButton != null && taskAddButton.Button != null)
{
ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button);
if (__instance.Hierarchy.Count != 1 && !flag)
case TaskTypes.DivertPower:
{
var component = ControllerManager.Instance.CurrentUiState.CurrentSelection.GetComponent<TaskFolder>();
if (component != null)
{
__instance.restorePreviousSelectionByFolderName = component.FolderName;
}
ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button);
flag = true;
var targetSystem = taskAddButton.MyTask.Cast<DivertPowerTask>().TargetSystem;
taskAddButton.Text.text = DestroyableSingleton<TranslationController>.Instance.GetString(
StringNames.DivertPowerTo,
DestroyableSingleton<TranslationController>.Instance.GetString(targetSystem));
break;
}
case TaskTypes.FixWeatherNode:
{
var nodeId = ((WeatherNodeTask)taskAddButton.MyTask).NodeId;
taskAddButton.Text.text =
DestroyableSingleton<TranslationController>.Instance.GetString(
StringNames.FixWeatherNode,
Array.Empty<Il2CppSystem.Object>()) + " " +
DestroyableSingleton<TranslationController>.Instance.GetString(
WeatherSwitchGame.ControlNames[nodeId],
Array.Empty<Il2CppSystem.Object>());
break;
}
default:
taskAddButton.Text.text =
DestroyableSingleton<TranslationController>.Instance.GetString(taskAddButton.MyTask.TaskType);
break;
}
__instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3);
if (taskAddButton.Button == null)
{
continue;
}
ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button);
if (__instance.Hierarchy.Count == 1 || flag)
{
continue;
}
var component =
ControllerManager.Instance.CurrentUiState.CurrentSelection.GetComponent<TaskFolder>();
if (component != null)
{
__instance.restorePreviousSelectionByFolderName = component.FolderName;
}
ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button);
flag = true;
}
if (taskFolder.FolderName == "Roles")
{
for (var m = 0; m < DestroyableSingleton<RoleManager>.Instance.AllRoles.Length; m++)
{
var roleBehaviour = DestroyableSingleton<RoleManager>.Instance.AllRoles[m];
if (roleBehaviour.Role != RoleTypes.ImpostorGhost && roleBehaviour.Role != RoleTypes.CrewmateGhost && !CustomRoleManager.CustomRoles.ContainsKey((ushort)roleBehaviour.Role))
if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost ||
CustomRoleManager.CustomRoles.ContainsKey((ushort)roleBehaviour.Role))
{
var taskAddButton2 = Object.Instantiate(__instance.RoleButton);
taskAddButton2.SafePositionWorld = __instance.SafePositionWorld;
taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe";
__instance.AddFileAsChildCustom(_rolesFolder, taskAddButton2, ref num, ref num2, ref num3);
taskAddButton2.Role = roleBehaviour;
if (taskAddButton2 != null && taskAddButton2.Button != null)
{
ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button);
if (m == 0 && __instance.restorePreviousSelectionFound != null)
{
ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound);
__instance.restorePreviousSelectionByFolderName = string.Empty;
__instance.restorePreviousSelectionFound = null;
}
else if (m == 0)
{
ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button);
}
}
continue;
}
var taskAddButton2 = Object.Instantiate(__instance.RoleButton);
taskAddButton2.SafePositionWorld = __instance.SafePositionWorld;
taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe";
__instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3);
taskAddButton2.Role = roleBehaviour;
if (taskAddButton2.Button == null)
{
continue;
}
ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button);
switch (m)
{
case 0 when __instance.restorePreviousSelectionFound != null:
ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound);
__instance.restorePreviousSelectionByFolderName = string.Empty;
__instance.restorePreviousSelectionFound = null;
break;
case 0:
ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button);
break;
}
}
}
@@ -241,27 +279,33 @@ public static class TaskAdderPatch
for (var m = 0; m < plugin.CustomRoles.Count; m++)
{
var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value;
if (roleBehaviour.Role != RoleTypes.ImpostorGhost && roleBehaviour.Role != RoleTypes.CrewmateGhost && !roleBehaviour.IsDead)
if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost ||
roleBehaviour.IsDead)
{
var taskAddButton2 = Object.Instantiate(__instance.RoleButton);
taskAddButton2.SafePositionWorld = __instance.SafePositionWorld;
taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe";
__instance.AddFileAsChildCustom(_rolesFolder, taskAddButton2, ref num, ref num2, ref num3);
taskAddButton2.Role = roleBehaviour;
if (taskAddButton2 != null && taskAddButton2.Button != null)
{
ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button);
if (m == 0 && __instance.restorePreviousSelectionFound != null)
{
ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound);
__instance.restorePreviousSelectionByFolderName = string.Empty;
__instance.restorePreviousSelectionFound = null;
}
else if (m == 0)
{
ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button);
}
}
continue;
}
var taskAddButton2 = Object.Instantiate(__instance.RoleButton);
taskAddButton2.SafePositionWorld = __instance.SafePositionWorld;
taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe";
__instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3);
taskAddButton2.Role = roleBehaviour;
if (taskAddButton2.Button == null)
{
continue;
}
ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button);
switch (m)
{
case 0 when __instance.restorePreviousSelectionFound != null:
ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound);
__instance.restorePreviousSelectionByFolderName = string.Empty;
__instance.restorePreviousSelectionFound = null;
break;
case 0:
ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button);
break;
}
}
}
@@ -272,18 +316,20 @@ public static class TaskAdderPatch
chip.GetComponent<SpriteRenderer>().maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
}
if (_scroller)
if (_scroller && _scroller != null)
{
_scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 3, 1f);
_scroller.SetYBoundsMin(0.0f);
_scroller.ScrollToTop();
}
if (__instance.Hierarchy.Count == 1)
{
ControllerManager.Instance.SetBackButton(__instance.BackButton);
return false;
}
ControllerManager.Instance.SetBackButton(__instance.FolderBackButton);
return false;
}
}
}
+26 -17
View File
@@ -7,9 +7,9 @@ namespace MiraAPI.Patches.Roles;
public static class TaskPanelPatch
{
/// <summary>
/// This patch is to override the automatic updating of the y position on the tab (which is in base game),
/// This patch is to override the automatic updating of the y position on the tab (which is in base game)
/// because I can't change the custom tab y pos if it's being overriden every frame.
/// Im sure there is an easier/better way, but this is the fix that worked for me
/// Im sure there is an easier/better way, but this is the fix that worked for me.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(TaskPanelBehaviour.Update))]
@@ -24,7 +24,12 @@ public static class TaskPanelPatch
var vector = __instance.background.sprite.bounds.extents;
var vector2 = __instance.tab.sprite.bounds.extents;
transform.localScale = __instance.taskText.textBounds.size.x > 0f ? new Vector3(__instance.taskText.textBounds.size.x + 0.4f, __instance.taskText.textBounds.size.y + 0.3f, 1f) : Vector3.zero;
transform.localScale = __instance.taskText.textBounds.size.x > 0f
? new Vector3(
__instance.taskText.textBounds.size.x + 0.4f,
__instance.taskText.textBounds.size.y + 0.3f,
1f)
: Vector3.zero;
vector.y = -vector.y;
vector = vector.Mul(transform.localScale);
@@ -39,20 +44,24 @@ public static class TaskPanelPatch
{
return false;
}
var closePosition = new Vector3(-__instance.background.sprite.bounds.size.x * __instance.background.transform.localScale.x, __instance.closedPosition.y, __instance.closedPosition.z);
var closePosition = new Vector3(
-__instance.background.sprite.bounds.size.x * __instance.background.transform.localScale.x,
__instance.closedPosition.y,
__instance.closedPosition.z);
__instance.closedPosition = closePosition;
if (__instance.open)
{
__instance.timer = Mathf.Min(1f, __instance.timer + Time.deltaTime / __instance.animationTimeSeconds);
}
else
{
__instance.timer = Mathf.Max(0f, __instance.timer - Time.deltaTime / __instance.animationTimeSeconds);
}
Vector3 relativePos;
relativePos = new Vector3(Mathf.SmoothStep(__instance.closedPosition.x, __instance.openPosition.x, __instance.timer), Mathf.SmoothStep(__instance.closedPosition.y, __instance.openPosition.y, __instance.timer), __instance.openPosition.z);
__instance.transform.localPosition = AspectPosition.ComputePosition(AspectPosition.EdgeAlignments.LeftTop, relativePos);
__instance.timer = __instance.open
? Mathf.Min(1f, __instance.timer + Time.deltaTime / __instance.animationTimeSeconds)
: Mathf.Max(0f, __instance.timer - Time.deltaTime / __instance.animationTimeSeconds);
var relativePos = new Vector3(
Mathf.SmoothStep(__instance.closedPosition.x, __instance.openPosition.x, __instance.timer),
Mathf.SmoothStep(__instance.closedPosition.y, __instance.openPosition.y, __instance.timer),
__instance.openPosition.z);
__instance.transform.localPosition = AspectPosition.ComputePosition(
AspectPosition.EdgeAlignments.LeftTop,
relativePos);
return false;
}
}
}
+27
View File
@@ -0,0 +1,27 @@
using HarmonyLib;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
/// <summary>
/// Set vent outline color based on the player's role.
/// </summary>
[HarmonyPatch(typeof(Vent), nameof(Vent.SetOutline))]
public static class VentOutlinePatch
{
public static void Postfix(Vent __instance, bool on, bool mainTarget)
{
if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole)
{
return;
}
var color = customRole.RoleColor;
__instance.myRend.material.SetFloat(ShaderID.Outline, on ? 1 : 0);
__instance.myRend.material.SetColor(ShaderID.OutlineColor, color);
__instance.myRend.material.SetColor(ShaderID.AddColor, mainTarget ? color : Color.clear);
}
}
+1 -1
View File
@@ -6,4 +6,4 @@ public interface IMiraPlugin
{
string OptionsTitleText { get; }
public ConfigFile GetConfigFile();
}
}
+38 -10
View File
@@ -6,16 +6,44 @@ using System.Collections.Generic;
namespace MiraAPI.PluginLoading;
public class MiraPluginInfo(IMiraPlugin miraPlugin, PluginInfo info)
/// <summary>
/// Represents a Mira plugin.
/// </summary>
public class MiraPluginInfo
{
public string PluginId { get; set; } = info.Metadata.GUID;
public IMiraPlugin MiraPlugin { get; set; } = miraPlugin;
public PluginInfo PluginInfo { get; set; } = info;
public ConfigFile PluginConfig { get; set; } = miraPlugin.GetConfigFile();
internal MiraPluginInfo(IMiraPlugin miraPlugin, PluginInfo info)
{
MiraPlugin = miraPlugin;
PluginConfig = miraPlugin.GetConfigFile();
PluginInfo = info;
PluginId = info.Metadata.GUID;
}
public readonly List<AbstractOptionGroup> OptionGroups = [];
public readonly List<IModdedOption> Options = [];
internal List<AbstractOptionGroup> OptionGroups { get; } = [];
public readonly Dictionary<ushort, RoleBehaviour> CustomRoles = [];
internal readonly Dictionary<int, CustomGameMode> GameModes = [];
}
internal List<IModdedOption> Options { get; } = [];
internal Dictionary<ushort, RoleBehaviour> CustomRoles { get; } = [];
internal Dictionary<int, CustomGameMode> GameModes { get; } = [];
/// <summary>
/// Gets the plugin's ID, as defined in the plugin's BepInEx metadata.
/// </summary>
public string PluginId { get; }
/// <summary>
/// Gets the plugin's instance as an <see cref="IMiraPlugin"/>.
/// </summary>
public IMiraPlugin MiraPlugin { get; }
/// <summary>
/// Gets the plugin's BepInEx metadata.
/// </summary>
public PluginInfo PluginInfo { get; }
/// <summary>
/// Gets the plugin's configuration file.
/// </summary>
public ConfigFile PluginConfig { get; }
}
+30 -33
View File
@@ -1,61 +1,58 @@
using BepInEx.Unity.IL2CPP;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BepInEx.Unity.IL2CPP;
using MiraAPI.Colors;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using Reactor.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MiraAPI.Colors;
using MiraAPI.Utilities;
using Reactor.Networking;
using Reactor.Utilities;
namespace MiraAPI.PluginLoading;
internal class MiraPluginManager
internal sealed class MiraPluginManager
{
public readonly Dictionary<Assembly, MiraPluginInfo> RegisteredPlugins = [];
private readonly Dictionary<Assembly, MiraPluginInfo> _registeredPlugins = [];
private static MiraPluginManager _instance;
internal MiraPluginInfo[] RegisteredPlugins() => [.. _registeredPlugins.Values];
public static MiraPluginManager Instance
{
get => _instance ??= new MiraPluginManager();
private set => _instance = value;
}
public static MiraPluginManager Instance { get; private set; } = new();
internal void Initialize()
{
Instance = this;
IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) =>
{
if (!plugin.GetType().GetInterfaces().Contains(typeof(IMiraPlugin)))
if (plugin is not IMiraPlugin miraPlugin)
{
return;
}
var info = new MiraPluginInfo(plugin as IMiraPlugin, pluginInfo);
var info = new MiraPluginInfo(miraPlugin, pluginInfo);
RegisterModifierAttribute(assembly);
RegisterAllOptions(assembly, info);
RegisterRoleAttribute(assembly, info);
RegisterButtonAttribute(assembly);
RegisterColorClasses(assembly);
RegisteredPlugins.Add(assembly, info);
_registeredPlugins.Add(assembly, info);
Logger<MiraApiPlugin>.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
};
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
}
public MiraPluginInfo GetPluginByGuid(string guid)
{
return RegisteredPlugins.Values.First(plugin => plugin.PluginId == guid);
return _registeredPlugins.Values.First(plugin => plugin.PluginId == guid);
}
private static void RegisterAllOptions(Assembly assembly, MiraPluginInfo pluginInfo)
@@ -68,7 +65,7 @@ internal class MiraPluginManager
{
continue;
}
foreach (var property in type.GetProperties())
{
if (property.PropertyType.IsAssignableTo(typeof(IModdedOption)))
@@ -76,7 +73,7 @@ internal class MiraPluginManager
ModdedOptionsManager.RegisterPropertyOption(type, property, pluginInfo);
continue;
}
var attribute = property.GetCustomAttribute<ModdedOptionAttribute>();
if (attribute == null)
{
@@ -86,6 +83,8 @@ internal class MiraPluginManager
ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo);
}
}
pluginInfo.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
}
private static void RegisterRoleAttribute(Assembly assembly, MiraPluginInfo pluginInfo)
@@ -98,22 +97,22 @@ internal class MiraPluginManager
{
continue;
}
if (!(typeof(RoleBehaviour).IsAssignableFrom(type) && typeof(ICustomRole).IsAssignableFrom(type)))
{
Logger<MiraApiPlugin>.Error($"{type.Name} does not inherit from RoleBehaviour or ICustomRole.");
continue;
}
if (!ModList.GetById(pluginInfo.PluginId).IsRequiredOnAllClients)
{
Logger<MiraApiPlugin>.Error("Custom roles are only supported on all clients.");
return;
}
roles.Add(type);
}
CustomRoleManager.RegisterRoleTypes(roles, pluginInfo);
}
@@ -138,14 +137,13 @@ internal class MiraPluginManager
{
continue;
}
var color = (CustomColor)property.GetValue(null);
if (color == null)
if (property.GetValue(null) is not CustomColor color)
{
Logger<MiraApiPlugin>.Error($"Color property {property.Name} in {type.Name} is null.");
Logger<MiraApiPlugin>.Error($"Color property {property.Name} in {type.Name} is not a CustomColor.");
continue;
}
PaletteManager.CustomColors.Add(color);
}
}
@@ -163,7 +161,6 @@ internal class MiraPluginManager
}
}
private static void RegisterButtonAttribute(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
@@ -175,4 +172,4 @@ internal class MiraPluginManager
}
}
}
}
}
+44 -16
View File
@@ -17,11 +17,13 @@ using Object = UnityEngine.Object;
namespace MiraAPI.Roles;
/// <summary>
/// Custom role manager for handling custom roles.
/// </summary>
public static class CustomRoleManager
{
public static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = new();
public static readonly Dictionary<Type, ushort> RoleIds = new();
internal static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = [];
internal static readonly Dictionary<Type, ushort> RoleIds = [];
private static ushort _roleId = 100;
@@ -33,9 +35,14 @@ public static class CustomRoleManager
internal static void RegisterInRoleManager()
{
RoleManager.Instance.AllRoles = RoleManager.Instance.AllRoles.Concat(CustomRoles.Values).ToArray();
foreach (var role in CustomRoles.Values.Where(x=>x.IsDead))
{
RoleManager.GhostRoles.Add(role.Role);
}
}
public static void RegisterRoleTypes(List<Type> roles, MiraPluginInfo pluginInfo)
internal static void RegisterRoleTypes(List<Type> roles, MiraPluginInfo pluginInfo)
{
roles.ForEach(x => RoleIds.Add(x, GetNextRoleId()));
@@ -53,11 +60,11 @@ public static class CustomRoleManager
}
}
private static RoleBehaviour RegisterRole(Type roleType, MiraPluginInfo parentMod)
private static RoleBehaviour? RegisterRole(Type roleType, MiraPluginInfo parentMod)
{
if (!(typeof(RoleBehaviour).IsAssignableFrom(roleType) && typeof(ICustomRole).IsAssignableFrom(roleType)))
{
Logger<MiraApiPlugin>.Error($"{roleType?.Name} does not inherit from RoleBehaviour or ICustomRole.");
Logger<MiraApiPlugin>.Error($"{roleType.Name} does not inherit from RoleBehaviour or ICustomRole.");
return null;
}
@@ -77,10 +84,10 @@ public static class CustomRoleManager
roleBehaviour.StringName = CustomStringName.CreateAndRegister(customRole.RoleName);
roleBehaviour.BlurbName = CustomStringName.CreateAndRegister(customRole.RoleDescription);
roleBehaviour.BlurbNameLong = CustomStringName.CreateAndRegister(customRole.RoleLongDescription);
roleBehaviour.AffectedByLightAffectors = customRole.AffectedByLight;
roleBehaviour.AffectedByLightAffectors = customRole.AffectedByLightOnAirship;
roleBehaviour.CanBeKilled = customRole.CanGetKilled;
roleBehaviour.CanUseKillButton = customRole.CanKill;
roleBehaviour.TasksCountTowardProgress = customRole.TasksCount;
roleBehaviour.CanUseKillButton = customRole.UseVanillaKillButton;
roleBehaviour.TasksCountTowardProgress = customRole.TasksCountForProgress;
roleBehaviour.CanVent = customRole.CanUseVent;
roleBehaviour.DefaultGhostRole = customRole.GhostRole;
roleBehaviour.MaxCount = customRole.MaxPlayers;
@@ -91,6 +98,14 @@ public static class CustomRoleManager
RoleManager.GhostRoles.Add(roleBehaviour.Role);
}
var useTaskHint = customRole.RoleHintType is RoleHintType.TaskHint;
var overridesTaskText = customRole.GetType().GetMethod("SpawnTaskHeader")?.DeclaringType == customRole.GetType();
if (useTaskHint && !overridesTaskText)
{
Logger<MiraApiPlugin>.Error($"Role {customRole.RoleName} is using RoleHintType.TaskHint but does not override SpawnTaskHeader!");
}
CustomRoles.Add(roleId, roleBehaviour);
if (customRole.HideSettings)
@@ -105,12 +120,23 @@ public static class CustomRoleManager
return roleBehaviour;
}
/// <summary>
/// Finds the parent mod of a custom role.
/// </summary>
/// <param name="role">The ICustomRole object.</param>
/// <returns>A MiraPluginInfo object representing the parent mod of the role.</returns>
public static MiraPluginInfo FindParentMod(ICustomRole role)
{
return MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.Value.CustomRoles.ContainsValue(role as RoleBehaviour)).Value;
return MiraPluginManager.Instance.RegisteredPlugins().First(plugin => plugin.CustomRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException()));
}
public static bool GetCustomRoleBehaviour(RoleTypes roleType, out ICustomRole result)
/// <summary>
/// Gets a custom role behaviour by role type.
/// </summary>
/// <param name="roleType">The role type enum.</param>
/// <param name="result">The ICustomRole result.</param>
/// <returns>True if the role was found.</returns>
public static bool GetCustomRoleBehaviour(RoleTypes roleType, out ICustomRole? result)
{
CustomRoles.TryGetValue((ushort)roleType, out var temp);
if (temp)
@@ -176,7 +202,7 @@ public static class CustomRoleManager
// 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.Values)
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
{
oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet);
plugin.PluginConfig.SaveOnConfigSet = false;
@@ -202,9 +228,11 @@ public static class CustomRoleManager
try
{
customRole.ParentMod.PluginConfig.TryGetEntry<int>(customRole.NumConfigDefinition,
customRole.ParentMod.PluginConfig.TryGetEntry<int>(
customRole.NumConfigDefinition,
out var numEntry);
customRole.ParentMod.PluginConfig.TryGetEntry<int>(customRole.ChanceConfigDefinition,
customRole.ParentMod.PluginConfig.TryGetEntry<int>(
customRole.ChanceConfigDefinition,
out var chanceEntry);
numEntry.Value = num;
@@ -216,7 +244,7 @@ public static class CustomRoleManager
}
}
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins.Values)
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
{
plugin.PluginConfig.Save();
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
@@ -227,4 +255,4 @@ public static class CustomRoleManager
LobbyInfoPane.Instance.RefreshPane();
}
}
}
}
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more